Optimize memory footprint in outline conversion
This commit is contained in:
parent
f287d352da
commit
e29df67587
10 changed files with 411 additions and 378 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -4028,6 +4028,7 @@
|
||||||
"name": "@iosevka/geometry-cache",
|
"name": "@iosevka/geometry-cache",
|
||||||
"version": "28.0.2",
|
"version": "28.0.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iosevka/geometry": "28.0.2",
|
||||||
"@msgpack/msgpack": "^2.8.0"
|
"@msgpack/msgpack": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -347,7 +347,7 @@ glyph-block Mark-Above : begin
|
||||||
|
|
||||||
define cs : new BezToContoursSink
|
define cs : new BezToContoursSink
|
||||||
ShapeConv.transferGenericShapeAsBezier {{inner outer}} cs GEOMETRY_PRECISION
|
ShapeConv.transferGenericShapeAsBezier {{inner outer}} cs GEOMETRY_PRECISION
|
||||||
currentGlyph.includeContours cs.contours 0 0
|
currentGlyph.includeContours cs.contours
|
||||||
|
|
||||||
create-glyph 'tildeAbove' 0x303 : glyph-proc
|
create-glyph 'tildeAbove' 0x303 : glyph-proc
|
||||||
set-width 0
|
set-width 0
|
||||||
|
@ -408,7 +408,7 @@ glyph-block Mark-Above : begin
|
||||||
|
|
||||||
define cs : new BezToContoursSink
|
define cs : new BezToContoursSink
|
||||||
ShapeConv.transferGenericShapeAsBezier arcs cs GEOMETRY_PRECISION
|
ShapeConv.transferGenericShapeAsBezier arcs cs GEOMETRY_PRECISION
|
||||||
currentGlyph.includeContours cs.contours 0 0
|
currentGlyph.includeContours cs.contours
|
||||||
|
|
||||||
create-glyph : glyph-proc
|
create-glyph : glyph-proc
|
||||||
set-width 0
|
set-width 0
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import * as Geom from "@iosevka/geometry";
|
import * as Geom from "@iosevka/geometry";
|
||||||
import * as CurveUtil from "@iosevka/geometry/curve-util";
|
|
||||||
import { Point } from "@iosevka/geometry/point";
|
|
||||||
import { Transform } from "@iosevka/geometry/transform";
|
import { Transform } from "@iosevka/geometry/transform";
|
||||||
import * as TypoGeom from "typo-geom";
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -40,41 +37,12 @@ function regulateCompositeGlyph(glyphStore, memo, g) {
|
||||||
if (!gn) return memoSet(memo, g, false);
|
if (!gn) return memoSet(memo, g, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let refGeometries = [];
|
||||||
|
for (const sr of refs) refGeometries.push(new Geom.ReferenceGeometry(sr.glyph, sr.x, sr.y));
|
||||||
|
g.geometry = new Geom.CombineGeometry(refGeometries);
|
||||||
return memoSet(memo, g, true);
|
return memoSet(memo, g, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenSimpleGlyph(cache, skew, g) {
|
|
||||||
const ck = Geom.hashGeometry(g.geometry);
|
|
||||||
const cached = cache.getGF(ck);
|
|
||||||
if (ck && cached) {
|
|
||||||
g.clearGeometry();
|
|
||||||
g.includeContours(CurveUtil.repToShape(cached), 0, 0);
|
|
||||||
cache.refreshGF(ck);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
let gSimplified;
|
|
||||||
if (skew) {
|
|
||||||
const tfBack = g.gizmo ? g.gizmo.inverse() : new Transform(1, -skew, 0, 1, 0, 0);
|
|
||||||
const tfForward = g.gizmo ? g.gizmo : new Transform(1, +skew, 0, 1, 0, 0);
|
|
||||||
gSimplified = new Geom.TransformedGeometry(
|
|
||||||
new SimplifyGeometry(new Geom.TransformedGeometry(g.geometry, tfBack)),
|
|
||||||
tfForward
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
gSimplified = new SimplifyGeometry(g.geometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cs = gSimplified.asContours();
|
|
||||||
g.clearGeometry();
|
|
||||||
g.includeContours(cs, 0, 0);
|
|
||||||
if (ck) cache.saveGF(ck, CurveUtil.shapeToRep(cs));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Detected broken geometry when processing", g._m_identifier);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function memoSet(memo, g, v) {
|
function memoSet(memo, g, v) {
|
||||||
memo.set(g, v);
|
memo.set(g, v);
|
||||||
return v;
|
return v;
|
||||||
|
@ -82,309 +50,34 @@ function memoSet(memo, g, v) {
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
class SimplifyGeometry extends Geom.GeometryBase {
|
function flattenSimpleGlyph(cache, skew, g) {
|
||||||
constructor(g) {
|
const ck = Geom.hashGeometry(g.geometry);
|
||||||
super();
|
const cached = cache.getGF(ck);
|
||||||
this.m_geom = g;
|
if (ck && cached) {
|
||||||
}
|
g.clearGeometry();
|
||||||
asContours() {
|
g.includeContours(cached);
|
||||||
// Produce simplified arcs
|
cache.refreshGF(ck);
|
||||||
let arcs = CurveUtil.convertShapeToArcs(this.m_geom.asContours());
|
} else {
|
||||||
if (!this.m_geom.producesSimpleContours()) {
|
try {
|
||||||
arcs = TypoGeom.Boolean.removeOverlap(
|
let gSimplified;
|
||||||
arcs,
|
if (skew) {
|
||||||
TypoGeom.Boolean.PolyFillType.pftNonZero,
|
const tfBack = g.gizmo ? g.gizmo.inverse() : new Transform(1, -skew, 0, 1, 0, 0);
|
||||||
CurveUtil.BOOLE_RESOLUTION
|
const tfForward = g.gizmo ? g.gizmo : new Transform(1, +skew, 0, 1, 0, 0);
|
||||||
);
|
gSimplified = new Geom.TransformedGeometry(
|
||||||
}
|
new Geom.SimplifyGeometry(new Geom.TransformedGeometry(g.geometry, tfBack)),
|
||||||
|
tfForward
|
||||||
// Convert to TT curves
|
);
|
||||||
const sink = new QuadifySink();
|
|
||||||
TypoGeom.ShapeConv.transferGenericShape(
|
|
||||||
TypoGeom.Fairize.fairizeBezierShape(arcs),
|
|
||||||
sink,
|
|
||||||
CurveUtil.GEOMETRY_PRECISION
|
|
||||||
);
|
|
||||||
return sink.contours;
|
|
||||||
}
|
|
||||||
asReferences() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
getDependencies() {
|
|
||||||
return this.m_geom.getDependencies();
|
|
||||||
}
|
|
||||||
filterTag(fn) {
|
|
||||||
return this.m_geom.filterTag(fn);
|
|
||||||
}
|
|
||||||
isEmpty() {
|
|
||||||
return this.m_geom.isEmpty();
|
|
||||||
}
|
|
||||||
measureComplexity() {
|
|
||||||
return this.m_geom.measureComplexity();
|
|
||||||
}
|
|
||||||
toShapeStringOrNull() {
|
|
||||||
const sTarget = this.m_geom.unlinkReferences().toShapeStringOrNull();
|
|
||||||
if (!sTarget) return null;
|
|
||||||
return `SimplifyGeometry{${sTarget}}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuadifySink {
|
|
||||||
constructor() {
|
|
||||||
this.contours = [];
|
|
||||||
this.lastContour = [];
|
|
||||||
}
|
|
||||||
beginShape() {}
|
|
||||||
endShape() {
|
|
||||||
if (this.lastContour.length > 2) {
|
|
||||||
let c = this.lastContour;
|
|
||||||
c = this.alignHVKnots(c);
|
|
||||||
c = this.dropDuplicateFirstLast(c);
|
|
||||||
c = this.cleanupOccurrentKnots1(c);
|
|
||||||
c = this.cleanupOccurrentKnots2(c);
|
|
||||||
c = this.cleanupOccurrentKnots1(c);
|
|
||||||
c = this.removeColinearArc(c);
|
|
||||||
c = this.removeColinearCorners(c);
|
|
||||||
c = this.cleanupOccurrentKnots1(c);
|
|
||||||
if (c.length > 2) this.contours.push(c);
|
|
||||||
}
|
|
||||||
this.lastContour = [];
|
|
||||||
}
|
|
||||||
moveTo(x, y) {
|
|
||||||
this.endShape();
|
|
||||||
this.lineTo(x, y);
|
|
||||||
}
|
|
||||||
lineTo(x, y) {
|
|
||||||
this.lastContour.push(Point.fromXY(Point.Type.Corner, x, y));
|
|
||||||
}
|
|
||||||
arcTo(arc, x, y) {
|
|
||||||
const offPoints = TypoGeom.Quadify.auto(arc, 1, 8);
|
|
||||||
for (const z of offPoints) {
|
|
||||||
this.lastContour.push(Point.from(Point.Type.Quadratic, z));
|
|
||||||
}
|
|
||||||
this.lineTo(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contour cleaning code
|
|
||||||
alignHVKnots(c0) {
|
|
||||||
const c = c0.slice(0);
|
|
||||||
const alignX = new CoordinateAligner(c, GetX, SetX);
|
|
||||||
const alignY = new CoordinateAligner(c, GetY, SetY);
|
|
||||||
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
const iNext = (i + 1) % c.length,
|
|
||||||
zCurr = c[i],
|
|
||||||
zNext = c[iNext];
|
|
||||||
if (zCurr.type === Point.Type.Quadratic && zNext.type === Point.Type.Corner) {
|
|
||||||
alignX.tryAlign(i, iNext);
|
|
||||||
alignY.tryAlign(i, iNext);
|
|
||||||
} else {
|
} else {
|
||||||
alignX.tryAlign(iNext, i);
|
gSimplified = new Geom.SimplifyGeometry(g.geometry);
|
||||||
alignY.tryAlign(iNext, i);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
alignX.apply();
|
const cs = gSimplified.asContours();
|
||||||
alignY.apply();
|
g.clearGeometry();
|
||||||
return c;
|
g.includeContours(cs);
|
||||||
}
|
if (ck) cache.saveGF(ck, cs);
|
||||||
|
} catch (e) {
|
||||||
// Drop the duplicate point (first-last)
|
console.error("Detected broken geometry when processing", g._m_identifier);
|
||||||
dropDuplicateFirstLast(c) {
|
throw e;
|
||||||
while (c.length > 1) {
|
|
||||||
const first = c[0],
|
|
||||||
last = c[c.length - 1];
|
|
||||||
if (
|
|
||||||
first.type === Point.Type.Corner &&
|
|
||||||
last.type === Point.Type.Corner &&
|
|
||||||
isOccurrent(first, last)
|
|
||||||
) {
|
|
||||||
c.pop();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Occurrent cleanup -- corner-corner
|
|
||||||
cleanupOccurrentKnots1(c0) {
|
|
||||||
let drops = [];
|
|
||||||
for (let i = 0; i < c0.length; i++) drops[i] = false;
|
|
||||||
for (let i = 0; i < c0.length; i++) {
|
|
||||||
const iPost = (i + 1) % c0.length;
|
|
||||||
const pre = c0[i],
|
|
||||||
post = c0[iPost];
|
|
||||||
if (
|
|
||||||
iPost > 0 &&
|
|
||||||
pre.type === Point.Type.Corner &&
|
|
||||||
post.type === Point.Type.Corner &&
|
|
||||||
isOccurrent(pre, post)
|
|
||||||
) {
|
|
||||||
drops[iPost] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropBy(c0, drops);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Occurrent cleanup -- off points
|
|
||||||
// This function actually **INSERTS** points for occurrent off knots.
|
|
||||||
cleanupOccurrentKnots2(c0) {
|
|
||||||
let insertAfter = [];
|
|
||||||
for (let i = 0; i < c0.length; i++) insertAfter[i] = false;
|
|
||||||
for (let i = 0; i < c0.length; i++) {
|
|
||||||
const cur = c0[i];
|
|
||||||
if (cur.type !== Point.Type.Quadratic) continue;
|
|
||||||
|
|
||||||
const iPre = (i - 1 + c0.length) % c0.length;
|
|
||||||
const iPost = (i + 1) % c0.length;
|
|
||||||
const pre = c0[iPre];
|
|
||||||
const post = c0[iPost];
|
|
||||||
|
|
||||||
if (isOccurrent(pre, cur) && post.type === Point.Type.Quadratic) {
|
|
||||||
insertAfter[i] = true;
|
|
||||||
}
|
|
||||||
if (isOccurrent(cur, post) && pre.type === Point.Type.Quadratic) {
|
|
||||||
insertAfter[iPre] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let c1 = [];
|
|
||||||
for (let i = 0; i < c0.length; i++) {
|
|
||||||
const cur = c0[i];
|
|
||||||
c1.push(cur);
|
|
||||||
if (insertAfter[i]) {
|
|
||||||
const iPost = (i + 1) % c0.length;
|
|
||||||
const post = c0[iPost];
|
|
||||||
c1.push(Point.mix(Point.Type.Corner, cur, post, 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c1;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeColinearCorners(c0) {
|
|
||||||
const c = c0.slice(0);
|
|
||||||
let found = false;
|
|
||||||
do {
|
|
||||||
found = false;
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
const zPrev = c[(i - 1 + c.length) % c.length],
|
|
||||||
zCurr = c[i],
|
|
||||||
zNext = c[(i + 1) % c.length];
|
|
||||||
if (
|
|
||||||
zPrev.type === Point.Type.Corner &&
|
|
||||||
zNext.type === Point.Type.Corner &&
|
|
||||||
pointsColinear(zPrev, zCurr, zNext)
|
|
||||||
) {
|
|
||||||
found = true;
|
|
||||||
c.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (found);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeColinearArc(c) {
|
|
||||||
if (c[0].type !== Point.Type.Corner) throw new Error("Unreachable");
|
|
||||||
|
|
||||||
let front = 0,
|
|
||||||
shouldRemove = [],
|
|
||||||
middlePoints = [];
|
|
||||||
for (let rear = 1; rear <= c.length; rear++) {
|
|
||||||
let zFront = c[front],
|
|
||||||
zRear = c[rear % c.length];
|
|
||||||
if (zRear.type === Point.Type.Corner) {
|
|
||||||
let allColinear = true;
|
|
||||||
for (const z of middlePoints) {
|
|
||||||
if (!pointsColinear(zFront, z, zRear)) allColinear = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allColinear) for (let i = front + 1; i < rear; i++) shouldRemove[i] = true;
|
|
||||||
|
|
||||||
front = rear;
|
|
||||||
middlePoints.length = 0;
|
|
||||||
} else {
|
|
||||||
middlePoints.push(zRear);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropBy(c, shouldRemove);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disjoint set for coordinate alignment
|
|
||||||
|
|
||||||
class CoordinateAligner {
|
|
||||||
constructor(c, lens, lensSet) {
|
|
||||||
this.c = c;
|
|
||||||
this.lens = lens;
|
|
||||||
this.lensSet = lensSet;
|
|
||||||
this.rank = [];
|
|
||||||
this.up = [];
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
const x = lens(c[i]);
|
|
||||||
this.up[i] = i;
|
|
||||||
this.rank[i] = Math.abs(x - Math.round(x));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
find(i) {
|
|
||||||
if (this.up[i] !== i) {
|
|
||||||
this.up[i] = this.find(this.up[i]);
|
|
||||||
return this.up[i];
|
|
||||||
} else {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tryAlign(i, j) {
|
|
||||||
if (occurrentPrecisionEqual(this.lens(this.c[i]), this.lens(this.c[j]))) {
|
|
||||||
this.align(i, j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
align(i, j) {
|
|
||||||
i = this.find(i);
|
|
||||||
j = this.find(j);
|
|
||||||
if (this.rank[i] > this.rank[j]) [i, j] = [j, i];
|
|
||||||
this.up[j] = i;
|
|
||||||
}
|
|
||||||
apply() {
|
|
||||||
for (let i = 0; i < this.c.length; i++) {
|
|
||||||
this.lensSet(this.c[i], Math.round(this.lens(this.c[this.find(i)])));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lenses used by aligner
|
|
||||||
const GetX = z => z.x;
|
|
||||||
const SetX = (z, x) => (z.x = x);
|
|
||||||
const GetY = z => z.y;
|
|
||||||
const SetY = (z, y) => (z.y = y);
|
|
||||||
|
|
||||||
function isOccurrent(zFirst, zLast) {
|
|
||||||
return zFirst.x === zLast.x && zFirst.y === zLast.y;
|
|
||||||
}
|
|
||||||
function occurrentPrecisionEqual(a, b) {
|
|
||||||
return Math.abs(a - b) < CurveUtil.OCCURRENT_PRECISION;
|
|
||||||
}
|
|
||||||
function aligned(a, b, c) {
|
|
||||||
return a === b && b === c;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pointsColinear(zPrev, zCurr, zNext) {
|
|
||||||
// No need to check in-betweenness, we can safely remove the corner
|
|
||||||
if (aligned(zPrev.x, zCurr.x, zNext.x)) return true;
|
|
||||||
if (aligned(zPrev.y, zCurr.y, zNext.y)) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dropping helper
|
|
||||||
function dropBy(c, shouldRemove) {
|
|
||||||
let n = 0;
|
|
||||||
for (let i = 0; i < c.length; i++) {
|
|
||||||
if (!shouldRemove[i]) c[n++] = c[i];
|
|
||||||
}
|
|
||||||
c.length = n;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import * as Geom from "@iosevka/geometry";
|
||||||
import { Point } from "@iosevka/geometry/point";
|
import { Point } from "@iosevka/geometry/point";
|
||||||
import * as Gr from "@iosevka/glyph/relation";
|
import * as Gr from "@iosevka/glyph/relation";
|
||||||
import { Ot } from "ot-builder";
|
import { Ot } from "ot-builder";
|
||||||
|
@ -41,15 +42,18 @@ class MappedGlyphStore {
|
||||||
fill(name, source) {
|
fill(name, source) {
|
||||||
const g = this.queryBySourceGlyph(source);
|
const g = this.queryBySourceGlyph(source);
|
||||||
if (!g) throw new Error("Unreachable");
|
if (!g) throw new Error("Unreachable");
|
||||||
|
|
||||||
// Fill metrics
|
// Fill metrics
|
||||||
g.horizontal = { start: 0, end: source.advanceWidth };
|
g.horizontal = { start: 0, end: source.advanceWidth };
|
||||||
|
|
||||||
// Fill Geometry
|
// Fill Geometry
|
||||||
if (source.geometry.isEmpty()) return;
|
if (!source.geometry.isEmpty()) {
|
||||||
const rs = source.geometry.asReferences();
|
const rs = source.geometry.asReferences();
|
||||||
if (rs) {
|
if (rs) {
|
||||||
this.fillReferences(g, rs);
|
this.fillReferences(g, rs);
|
||||||
} else {
|
} else {
|
||||||
this.fillContours(g, source.geometry.asContours());
|
this.fillContours(g, source.geometry.asContours());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fillOtGlyphNames() {
|
fillOtGlyphNames() {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@iosevka/geometry-cache",
|
"name": "@iosevka/geometry-cache",
|
||||||
"version": "28.0.2",
|
"version": "28.0.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.mjs"
|
".": "./src/index.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@msgpack/msgpack": "^2.8.0"
|
"@iosevka/geometry": "28.0.2",
|
||||||
}
|
"@msgpack/msgpack": "^2.8.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import zlib from "zlib";
|
import zlib from "zlib";
|
||||||
|
|
||||||
|
import * as CurveUtil from "@iosevka/geometry/curve-util";
|
||||||
import { encode, decode } from "@msgpack/msgpack";
|
import { encode, decode } from "@msgpack/msgpack";
|
||||||
|
|
||||||
const Edition = 30;
|
const Edition = 31;
|
||||||
const MAX_AGE = 16;
|
const MAX_AGE = 16;
|
||||||
class GfEntry {
|
class GfEntry {
|
||||||
constructor(age, value) {
|
constructor(age, value) {
|
||||||
|
@ -23,7 +24,8 @@ class Cache {
|
||||||
this.historyAgeKeys = rep.ageKeys.slice(0, MAX_AGE);
|
this.historyAgeKeys = rep.ageKeys.slice(0, MAX_AGE);
|
||||||
const ageKeySet = new Set(this.historyAgeKeys);
|
const ageKeySet = new Set(this.historyAgeKeys);
|
||||||
for (const [k, e] of Object.entries(rep.gf)) {
|
for (const [k, e] of Object.entries(rep.gf)) {
|
||||||
if (ageKeySet.has(e.age)) this.gf.set(k, new GfEntry(e.age, e.value));
|
if (ageKeySet.has(e.age))
|
||||||
|
this.gf.set(k, new GfEntry(e.age, CurveUtil.repToShape(e.value)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toRep(version, diffOnly) {
|
toRep(version, diffOnly) {
|
||||||
|
|
|
@ -67,7 +67,6 @@ function convertContourToArcs(contour) {
|
||||||
return newContour;
|
return newContour;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SPIRO_PRECISION = 1 / 2;
|
|
||||||
export const OCCURRENT_PRECISION = 1 / 16;
|
export const OCCURRENT_PRECISION = 1 / 16;
|
||||||
export const GEOMETRY_PRECISION = 1 / 4;
|
export const GEOMETRY_PRECISION = 1 / 4;
|
||||||
export const BOOLE_RESOLUTION = 0x4000;
|
export const BOOLE_RESOLUTION = 0x4000;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as TypoGeom from "typo-geom";
|
||||||
|
|
||||||
import * as CurveUtil from "./curve-util.mjs";
|
import * as CurveUtil from "./curve-util.mjs";
|
||||||
import { Point } from "./point.mjs";
|
import { Point } from "./point.mjs";
|
||||||
|
import { QuadifySink } from "./quadify.mjs";
|
||||||
import { SpiroExpander } from "./spiro-expand.mjs";
|
import { SpiroExpander } from "./spiro-expand.mjs";
|
||||||
import { Transform } from "./transform.mjs";
|
import { Transform } from "./transform.mjs";
|
||||||
|
|
||||||
|
@ -39,19 +40,15 @@ export class GeometryBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContourGeometry extends GeometryBase {
|
export class InvalidGeometry extends GeometryBase {}
|
||||||
constructor(points) {
|
|
||||||
|
export class ContourSetGeometry extends GeometryBase {
|
||||||
|
constructor(contours) {
|
||||||
super();
|
super();
|
||||||
this.m_points = [];
|
this.m_contours = contours;
|
||||||
for (const z of points) {
|
|
||||||
this.m_points.push(Point.from(z.type, z));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
asContours() {
|
asContours() {
|
||||||
if (this.isEmpty()) return [];
|
return this.m_contours;
|
||||||
let c1 = [];
|
|
||||||
for (const z of this.m_points) c1.push(Point.from(z.type, z));
|
|
||||||
return [c1];
|
|
||||||
}
|
}
|
||||||
asReferences() {
|
asReferences() {
|
||||||
return null;
|
return null;
|
||||||
|
@ -63,16 +60,19 @@ export class ContourGeometry extends GeometryBase {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
isEmpty() {
|
isEmpty() {
|
||||||
return !this.m_points.length;
|
return !this.m_contours.length;
|
||||||
}
|
}
|
||||||
measureComplexity() {
|
measureComplexity() {
|
||||||
for (const z of this.m_points) {
|
for (const z of this.m_contours) {
|
||||||
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
|
if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff;
|
||||||
}
|
}
|
||||||
return this.m_points.length;
|
return this.m_contours.length;
|
||||||
}
|
}
|
||||||
toShapeStringOrNull() {
|
toShapeStringOrNull() {
|
||||||
return Format.struct(`ContourGeometry`, Format.list(this.m_points.map(Format.typedPoint)));
|
return Format.struct(
|
||||||
|
`ContourSetGeometry`,
|
||||||
|
Format.list(this.m_contours.map(c => Format.list(c.map(Format.typedPoint))))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,7 +90,12 @@ export class SpiroGeometry extends GeometryBase {
|
||||||
asContours() {
|
asContours() {
|
||||||
if (this.m_cachedContours) return this.m_cachedContours;
|
if (this.m_cachedContours) return this.m_cachedContours;
|
||||||
const s = new CurveUtil.BezToContoursSink(this.m_gizmo);
|
const s = new CurveUtil.BezToContoursSink(this.m_gizmo);
|
||||||
SpiroJs.spiroToBezierOnContext(this.m_knots, this.m_closed, s, CurveUtil.SPIRO_PRECISION);
|
SpiroJs.spiroToBezierOnContext(
|
||||||
|
this.m_knots,
|
||||||
|
this.m_closed,
|
||||||
|
s,
|
||||||
|
CurveUtil.GEOMETRY_PRECISION
|
||||||
|
);
|
||||||
this.m_cachedContours = s.contours;
|
this.m_cachedContours = s.contours;
|
||||||
return this.m_cachedContours;
|
return this.m_cachedContours;
|
||||||
}
|
}
|
||||||
|
@ -573,6 +578,58 @@ export class BooleanGeometry extends GeometryBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This special geometry type is used in the finalization phase to create TTF contours.
|
||||||
|
export class SimplifyGeometry extends GeometryBase {
|
||||||
|
constructor(g) {
|
||||||
|
super();
|
||||||
|
this.m_geom = g;
|
||||||
|
}
|
||||||
|
asContours() {
|
||||||
|
// Produce simplified arcs
|
||||||
|
let arcs = CurveUtil.convertShapeToArcs(this.m_geom.asContours());
|
||||||
|
if (!this.m_geom.producesSimpleContours()) {
|
||||||
|
arcs = TypoGeom.Boolean.removeOverlap(
|
||||||
|
arcs,
|
||||||
|
TypoGeom.Boolean.PolyFillType.pftNonZero,
|
||||||
|
CurveUtil.BOOLE_RESOLUTION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to TT curves
|
||||||
|
const sink = new QuadifySink();
|
||||||
|
TypoGeom.ShapeConv.transferGenericShape(
|
||||||
|
TypoGeom.Fairize.fairizeBezierShape(arcs),
|
||||||
|
sink,
|
||||||
|
CurveUtil.GEOMETRY_PRECISION
|
||||||
|
);
|
||||||
|
return sink.contours;
|
||||||
|
}
|
||||||
|
asReferences() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
getDependencies() {
|
||||||
|
return this.m_geom.getDependencies();
|
||||||
|
}
|
||||||
|
unlinkReferences() {
|
||||||
|
return new SimplifyGeometry(this.m_geom.unlinkReferences());
|
||||||
|
}
|
||||||
|
filterTag(fn) {
|
||||||
|
return new SimplifyGeometry(this.m_geom.filterTag(fn));
|
||||||
|
}
|
||||||
|
isEmpty() {
|
||||||
|
return this.m_geom.isEmpty();
|
||||||
|
}
|
||||||
|
measureComplexity() {
|
||||||
|
return this.m_geom.measureComplexity();
|
||||||
|
}
|
||||||
|
toShapeStringOrNull() {
|
||||||
|
const sTarget = this.m_geom.unlinkReferences().toShapeStringOrNull();
|
||||||
|
if (!sTarget) return null;
|
||||||
|
return `SimplifyGeometry{${sTarget}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
export function combineWith(a, b) {
|
export function combineWith(a, b) {
|
||||||
if (a instanceof CombineGeometry) {
|
if (a instanceof CombineGeometry) {
|
||||||
return a.with(b);
|
return a.with(b);
|
||||||
|
|
282
packages/geometry/src/quadify.mjs
Normal file
282
packages/geometry/src/quadify.mjs
Normal file
|
@ -0,0 +1,282 @@
|
||||||
|
import * as TypoGeom from "typo-geom";
|
||||||
|
|
||||||
|
import * as CurveUtil from "./curve-util.mjs";
|
||||||
|
import { Point } from "./point.mjs";
|
||||||
|
|
||||||
|
export class QuadifySink {
|
||||||
|
constructor() {
|
||||||
|
this.contours = [];
|
||||||
|
this.lastContour = [];
|
||||||
|
}
|
||||||
|
beginShape() {}
|
||||||
|
endShape() {
|
||||||
|
if (this.lastContour.length > 2) {
|
||||||
|
let c = this.lastContour;
|
||||||
|
c = this.alignHVKnots(c);
|
||||||
|
c = this.dropDuplicateFirstLast(c);
|
||||||
|
c = this.cleanupOccurrentKnots1(c);
|
||||||
|
c = this.cleanupOccurrentKnots2(c);
|
||||||
|
c = this.cleanupOccurrentKnots1(c);
|
||||||
|
c = this.removeColinearArc(c);
|
||||||
|
c = this.removeColinearCorners(c);
|
||||||
|
c = this.cleanupOccurrentKnots1(c);
|
||||||
|
if (c.length > 2) this.contours.push(c);
|
||||||
|
}
|
||||||
|
this.lastContour = [];
|
||||||
|
}
|
||||||
|
moveTo(x, y) {
|
||||||
|
this.endShape();
|
||||||
|
this.lineTo(x, y);
|
||||||
|
}
|
||||||
|
lineTo(x, y) {
|
||||||
|
this.lastContour.push(Point.fromXY(Point.Type.Corner, x, y));
|
||||||
|
}
|
||||||
|
arcTo(arc, x, y) {
|
||||||
|
const offPoints = TypoGeom.Quadify.auto(arc, 1, 8);
|
||||||
|
for (const z of offPoints) {
|
||||||
|
this.lastContour.push(Point.from(Point.Type.Quadratic, z));
|
||||||
|
}
|
||||||
|
this.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contour cleaning code
|
||||||
|
alignHVKnots(c0) {
|
||||||
|
const c = c0.slice(0);
|
||||||
|
const alignX = new CoordinateAligner(c, GetX, SetX);
|
||||||
|
const alignY = new CoordinateAligner(c, GetY, SetY);
|
||||||
|
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
const iNext = (i + 1) % c.length,
|
||||||
|
zCurr = c[i],
|
||||||
|
zNext = c[iNext];
|
||||||
|
if (zCurr.type === Point.Type.Quadratic && zNext.type === Point.Type.Corner) {
|
||||||
|
alignX.tryAlign(i, iNext);
|
||||||
|
alignY.tryAlign(i, iNext);
|
||||||
|
} else {
|
||||||
|
alignX.tryAlign(iNext, i);
|
||||||
|
alignY.tryAlign(iNext, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alignX.apply();
|
||||||
|
alignY.apply();
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop the duplicate point (first-last)
|
||||||
|
dropDuplicateFirstLast(c) {
|
||||||
|
while (c.length > 1) {
|
||||||
|
const first = c[0],
|
||||||
|
last = c[c.length - 1];
|
||||||
|
if (
|
||||||
|
first.type === Point.Type.Corner &&
|
||||||
|
last.type === Point.Type.Corner &&
|
||||||
|
isOccurrent(first, last)
|
||||||
|
) {
|
||||||
|
c.pop();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Occurrent cleanup -- corner-corner
|
||||||
|
cleanupOccurrentKnots1(c0) {
|
||||||
|
let drops = [];
|
||||||
|
for (let i = 0; i < c0.length; i++) drops[i] = false;
|
||||||
|
for (let i = 0; i < c0.length; i++) {
|
||||||
|
const iPost = (i + 1) % c0.length;
|
||||||
|
const pre = c0[i],
|
||||||
|
post = c0[iPost];
|
||||||
|
if (
|
||||||
|
iPost > 0 &&
|
||||||
|
pre.type === Point.Type.Corner &&
|
||||||
|
post.type === Point.Type.Corner &&
|
||||||
|
isOccurrent(pre, post)
|
||||||
|
) {
|
||||||
|
drops[iPost] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dropBy(c0, drops);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Occurrent cleanup -- off points
|
||||||
|
// This function actually **INSERTS** points for occurrent off knots.
|
||||||
|
cleanupOccurrentKnots2(c0) {
|
||||||
|
let insertAfter = [];
|
||||||
|
for (let i = 0; i < c0.length; i++) insertAfter[i] = false;
|
||||||
|
for (let i = 0; i < c0.length; i++) {
|
||||||
|
const cur = c0[i];
|
||||||
|
if (cur.type !== Point.Type.Quadratic) continue;
|
||||||
|
|
||||||
|
const iPre = (i - 1 + c0.length) % c0.length;
|
||||||
|
const iPost = (i + 1) % c0.length;
|
||||||
|
const pre = c0[iPre];
|
||||||
|
const post = c0[iPost];
|
||||||
|
|
||||||
|
if (isOccurrent(pre, cur) && post.type === Point.Type.Quadratic) {
|
||||||
|
insertAfter[i] = true;
|
||||||
|
}
|
||||||
|
if (isOccurrent(cur, post) && pre.type === Point.Type.Quadratic) {
|
||||||
|
insertAfter[iPre] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let c1 = [];
|
||||||
|
for (let i = 0; i < c0.length; i++) {
|
||||||
|
const cur = c0[i];
|
||||||
|
c1.push(cur);
|
||||||
|
if (insertAfter[i]) {
|
||||||
|
const iPost = (i + 1) % c0.length;
|
||||||
|
const post = c0[iPost];
|
||||||
|
c1.push(Point.mix(Point.Type.Corner, cur, post, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeColinearCorners(c0) {
|
||||||
|
const c = c0.slice(0);
|
||||||
|
let found = false;
|
||||||
|
do {
|
||||||
|
found = false;
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
const zPrev = c[(i - 1 + c.length) % c.length],
|
||||||
|
zCurr = c[i],
|
||||||
|
zNext = c[(i + 1) % c.length];
|
||||||
|
if (
|
||||||
|
zPrev.type === Point.Type.Corner &&
|
||||||
|
zNext.type === Point.Type.Corner &&
|
||||||
|
(pointsHVColinear(zPrev, zCurr, zNext) || pointsColinear(zPrev, zCurr, zNext))
|
||||||
|
) {
|
||||||
|
found = true;
|
||||||
|
c.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (found);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeColinearArc(c) {
|
||||||
|
if (c[0].type !== Point.Type.Corner) throw new Error("Unreachable");
|
||||||
|
|
||||||
|
let front = 0,
|
||||||
|
shouldRemove = [],
|
||||||
|
middlePoints = [];
|
||||||
|
for (let rear = 1; rear <= c.length; rear++) {
|
||||||
|
let zFront = c[front],
|
||||||
|
zRear = c[rear % c.length];
|
||||||
|
if (zRear.type === Point.Type.Corner) {
|
||||||
|
let allColinear = true;
|
||||||
|
for (const z of middlePoints) {
|
||||||
|
if (!pointsHVColinear(zFront, z, zRear)) allColinear = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allColinear) for (let i = front + 1; i < rear; i++) shouldRemove[i] = true;
|
||||||
|
|
||||||
|
front = rear;
|
||||||
|
middlePoints.length = 0;
|
||||||
|
} else {
|
||||||
|
middlePoints.push(zRear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dropBy(c, shouldRemove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disjoint set for coordinate alignment
|
||||||
|
class CoordinateAligner {
|
||||||
|
constructor(c, lens, lensSet) {
|
||||||
|
this.c = c;
|
||||||
|
this.lens = lens;
|
||||||
|
this.lensSet = lensSet;
|
||||||
|
this.rank = [];
|
||||||
|
this.up = [];
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
const x = lens(c[i]);
|
||||||
|
this.up[i] = i;
|
||||||
|
this.rank[i] = Math.abs(x - Math.round(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
find(i) {
|
||||||
|
if (this.up[i] !== i) {
|
||||||
|
this.up[i] = this.find(this.up[i]);
|
||||||
|
return this.up[i];
|
||||||
|
} else {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tryAlign(i, j) {
|
||||||
|
if (occurrentPrecisionEqual(this.lens(this.c[i]), this.lens(this.c[j]))) {
|
||||||
|
this.align(i, j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
align(i, j) {
|
||||||
|
i = this.find(i);
|
||||||
|
j = this.find(j);
|
||||||
|
if (this.rank[i] > this.rank[j]) [i, j] = [j, i];
|
||||||
|
this.up[j] = i;
|
||||||
|
}
|
||||||
|
apply() {
|
||||||
|
for (let i = 0; i < this.c.length; i++) {
|
||||||
|
this.lensSet(this.c[i], Math.round(this.lens(this.c[this.find(i)])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lenses used by aligner
|
||||||
|
const GetX = z => z.x;
|
||||||
|
const SetX = (z, x) => (z.x = x);
|
||||||
|
const GetY = z => z.y;
|
||||||
|
const SetY = (z, y) => (z.y = y);
|
||||||
|
|
||||||
|
function isOccurrent(zFirst, zLast) {
|
||||||
|
return zFirst.x === zLast.x && zFirst.y === zLast.y;
|
||||||
|
}
|
||||||
|
function occurrentPrecisionEqual(a, b) {
|
||||||
|
return Math.abs(a - b) < CurveUtil.OCCURRENT_PRECISION;
|
||||||
|
}
|
||||||
|
function aligned(a, b, c) {
|
||||||
|
return a === b && b === c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointsHVColinear(zPrev, zCurr, zNext) {
|
||||||
|
// No need to check in-between-ness, we can safely remove the corner
|
||||||
|
if (aligned(zPrev.x, zCurr.x, zNext.x)) return true;
|
||||||
|
if (aligned(zPrev.y, zCurr.y, zNext.y)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inBetween(a, b, c) {
|
||||||
|
return (a <= b && b <= c) || (c <= b && b <= a);
|
||||||
|
}
|
||||||
|
function pointsColinear(zPrev, zCurr, zNext) {
|
||||||
|
// If zCurr is not in between zPrev and zNext, they are not colinear
|
||||||
|
if (!inBetween(zPrev.x, zCurr.x, zNext.x)) return false;
|
||||||
|
if (!inBetween(zPrev.y, zCurr.y, zNext.y)) return false;
|
||||||
|
|
||||||
|
// Measure the distance of zCurr to the line zPrev--zNext
|
||||||
|
// If it is less than OCCURRENT_PRECISION, then we think it is colinear
|
||||||
|
// Use squared distance to avoid sqrt
|
||||||
|
const dx = zNext.x - zPrev.x,
|
||||||
|
dy = zNext.y - zPrev.y;
|
||||||
|
const t = (zCurr.y - zPrev.y) * dx - (zCurr.x - zPrev.x) * dy;
|
||||||
|
return (
|
||||||
|
t * t < CurveUtil.GEOMETRY_PRECISION * CurveUtil.GEOMETRY_PRECISION * (dx * dx + dy * dy)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropping helper
|
||||||
|
function dropBy(c, shouldRemove) {
|
||||||
|
let n = 0;
|
||||||
|
for (let i = 0; i < c.length; i++) {
|
||||||
|
if (!shouldRemove[i]) c[n++] = c[i];
|
||||||
|
}
|
||||||
|
c.length = n;
|
||||||
|
return c;
|
||||||
|
}
|
|
@ -123,14 +123,8 @@ export class Glyph {
|
||||||
if (this.ctxTag) g = new Geom.TaggedGeometry(g, this.ctxTag);
|
if (this.ctxTag) g = new Geom.TaggedGeometry(g, this.ctxTag);
|
||||||
this.geometry = Geom.combineWith(this.geometry, g);
|
this.geometry = Geom.combineWith(this.geometry, g);
|
||||||
}
|
}
|
||||||
includeContours(cs, shiftX, shiftY) {
|
includeContours(cs) {
|
||||||
let parts = [];
|
this.includeGeometry(new Geom.ContourSetGeometry(cs));
|
||||||
for (const contour of cs) {
|
|
||||||
let c = [];
|
|
||||||
for (const z of contour) c.push(Point.translated(z, shiftX, shiftY));
|
|
||||||
parts.push(new Geom.ContourGeometry(c));
|
|
||||||
}
|
|
||||||
this.includeGeometry(new Geom.CombineGeometry(parts));
|
|
||||||
}
|
}
|
||||||
applyTransform(tfm, alsoAnchors) {
|
applyTransform(tfm, alsoAnchors) {
|
||||||
this.geometry = new Geom.TransformedGeometry(this.geometry, tfm);
|
this.geometry = new Geom.TransformedGeometry(this.geometry, tfm);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue