diff --git a/font-src/gen/finalize/glyphs.js b/font-src/gen/finalize/glyphs.js index 57f609a8f..95c5413f9 100644 --- a/font-src/gen/finalize/glyphs.js +++ b/font-src/gen/finalize/glyphs.js @@ -20,9 +20,12 @@ function regulateGlyphStore(cache, skew, glyphStore) { for (const g of glyphStore.glyphs()) { if (g.geometry.isEmpty()) continue; if (!regulateCompositeGlyph(glyphStore, compositeMemo, g)) { - flattenSimpleGlyph(cache, skew, g); + g.geometry = g.geometry.unlinkReferences(); } } + for (const g of glyphStore.glyphs()) { + if (!compositeMemo.get(g)) flattenSimpleGlyph(cache, skew, g); + } } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -113,7 +116,7 @@ class SimplifyGeometry extends Geom.GeometryBase { return this.m_geom.measureComplexity(); } toShapeStringOrNull() { - const sTarget = this.m_geom.unwrapShapeIdentity().toShapeStringOrNull(); + const sTarget = this.m_geom.unlinkReferences().toShapeStringOrNull(); if (!sTarget) return null; return `SimplifyGeometry{${sTarget}}`; } @@ -121,83 +124,104 @@ class SimplifyGeometry extends Geom.GeometryBase { class FairizedShapeSink { constructor() { - this.lastReferenceZ = null; this.contours = []; this.lastContour = []; } beginShape() {} endShape() { - this.lastReferenceZ = null; if (this.lastContour.length > 2) { // TT use CW for outline, being different from Clipper - const c = this.lastContour.reverse(); - const zFirst = c[0], - zLast = c[c.length - 1]; - if (isOccurrent(zFirst, zLast)) c.pop(); + let c = this.lastContour.reverse(); + c = this.alignHVKnots(c); + c = this.cleanupOccurrentKnots1(c); + c = this.cleanupOccurrentKnots2(c); + c = this.removeColinearKnots(c); this.contours.push(c); } this.lastContour = []; } - tryAlignWithPreviousKnot(z) { - if (!this.lastReferenceZ) return z; - let x1 = z.x, - y1 = z.y; - if (geometryPrecisionEqual(x1, this.lastReferenceZ.x)) x1 = this.lastReferenceZ.x; - if (geometryPrecisionEqual(y1, this.lastReferenceZ.y)) y1 = this.lastReferenceZ.y; - return Point.fromXY(z.type, x1, y1).round(CurveUtil.GEOMETRY_PRECISION); - } moveTo(x, y) { this.endShape(); this.lineTo(x, y); } lineTo(x, y) { - const z0 = Point.fromXY(Point.Type.Corner, x, y); - const z = this.tryAlignWithPreviousKnot(z0); - this.popOccurrentKnots(z); - this.popColinearKnots(z); - this.lastContour.push(z); - this.lastReferenceZ = z0; + 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( - this.tryAlignWithPreviousKnot(Point.from(Point.Type.Quadratic, z)) - ); + this.lastContour.push(Point.from(Point.Type.Quadratic, z)); } this.lineTo(x, y); } - popOccurrentKnots(z) { - if (this.lastContour.length <= 0) return; - const last = this.lastContour[this.lastContour.length - 1]; - if (last.type === Point.Type.Corner && last.x === z.x && last.y === z.y) { - this.lastContour.pop(); + + // Contour cleaning code + alignHVKnots(c0) { + const c = c0.slice(0); + for (let i = 0; i < c.length; i++) { + const zPrev = c[i], + zCurr = c[(i + 1) % c.length]; + if (zPrev.type === Point.Type.Corner) { + if (occurrentPrecisionEqual(zPrev.x, zCurr.x)) zCurr.x = zPrev.x; + if (occurrentPrecisionEqual(zPrev.y, zCurr.y)) zCurr.y = zPrev.y; + } } + for (let i = 0; i < c.length; i++) { + const zCurr = c[i], + zNext = c[(i + 1) % c.length]; + if (zCurr.type === Point.Type.Quadratic && zNext.type === Point.Type.Corner) { + if (occurrentPrecisionEqual(zCurr.x, zNext.x)) zCurr.x = zNext.x; + if (occurrentPrecisionEqual(zCurr.y, zNext.y)) zCurr.y = zNext.y; + } + } + return c; } - popColinearKnots(z) { - let kArcStart = this.lastContour.length - 2; - if (kArcStart >= 0) { - const kLast = kArcStart + 1; + cleanupOccurrentKnots1(c0) { + const c = [c0[0]]; + for (let i = 1; i < c0.length; i++) { if ( - this.lastContour[kArcStart].type !== Point.Type.Corner && - this.lastContour[kLast].type === Point.Type.Corner + !( + c0[i].type === Point.Type.Corner && + c0[i - 1].type === Point.Type.Corner && + isOccurrent(c0[i], c0[i - 1]) + ) ) { - return; + c.push(c0[i]); } } - while (kArcStart >= 0 && this.lastContour[kArcStart].type !== Point.Type.Corner) - kArcStart--; - if (kArcStart >= 0) { - const a = this.lastContour[kArcStart]; - let fColinearH = true; - let fColinearV = true; - for (let m = kArcStart + 1; m < this.lastContour.length; m++) { - const b = this.lastContour[m]; - if (!(aligned(a.y, b.y, z.y) && between(a.x, b.x, z.x))) fColinearH = false; - if (!(aligned(a.x, b.x, z.x) && between(a.y, b.y, z.y))) fColinearV = false; + return c; + } + cleanupOccurrentKnots2(c0) { + const c = c0.slice(0); + const zFirst = c[0], + zLast = c[c.length - 1]; + if (isOccurrent(zFirst, zLast)) c.pop(); + return c; + } + removeColinearKnots(c0) { + const c = c0.slice(0), + shouldRemove = []; + 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 && + zCurr.type === Point.Type.Corner && + zNext.type === Point.Type.Corner + ) { + if (aligned(zPrev.x, zCurr.x, zNext.x) && between(zPrev.y, zCurr.y, zNext.y)) + shouldRemove[i] = true; + if (aligned(zPrev.y, zCurr.y, zNext.y) && between(zPrev.x, zCurr.x, zNext.x)) + shouldRemove[i] = true; } - if (fColinearH || fColinearV) this.lastContour.length = kArcStart + 1; } + + const c2 = []; + for (let i = 0; i < c.length; i++) { + if (!shouldRemove[i]) c2.push(c[i]); + } + return c2; } } function isOccurrent(zFirst, zLast) { @@ -208,14 +232,11 @@ function isOccurrent(zFirst, zLast) { zFirst.y === zLast.y ); } -function geometryPrecisionEqual(a, b) { - return ( - Math.round(a * CurveUtil.RECIP_GEOMETRY_PRECISION) === - Math.round(b * CurveUtil.RECIP_GEOMETRY_PRECISION) - ); +function occurrentPrecisionEqual(a, b) { + return Math.abs(a - b) < CurveUtil.OCCURRENT_PRECISION; } function aligned(a, b, c) { - return geometryPrecisionEqual(a, b) && geometryPrecisionEqual(b, c); + return occurrentPrecisionEqual(a, b) && occurrentPrecisionEqual(b, c); } function between(a, b, c) { return (a <= b && b <= c) || (a >= b && b >= c); diff --git a/font-src/support/curve-util.js b/font-src/support/curve-util.js index 495324bc3..3f933891f 100644 --- a/font-src/support/curve-util.js +++ b/font-src/support/curve-util.js @@ -5,8 +5,8 @@ const Point = require("./point"); const Transform = require("./transform"); exports.SPIRO_PRECISION = 1 / 2; +exports.OCCURRENT_PRECISION = 1 / 16; exports.GEOMETRY_PRECISION = 1 / 4; -exports.RECIP_GEOMETRY_PRECISION = 4; exports.BOOLE_RESOLUTION = 0x4000; exports.OffsetCurve = class OffsetCurve { diff --git a/font-src/support/geometry.js b/font-src/support/geometry.js index f1caa441a..609531b7c 100644 --- a/font-src/support/geometry.js +++ b/font-src/support/geometry.js @@ -14,7 +14,7 @@ class GeometryBase { asReferences() { throw new Error("Unimplemented"); } - unwrapShapeIdentity() { + unlinkReferences() { return this; } filterTag(fn) { @@ -103,8 +103,8 @@ class ReferenceGeometry extends GeometryBase { measureComplexity() { return this.m_glyph.geometry.measureComplexity(); } - unwrapShapeIdentity() { - return this.unwrap().unwrapShapeIdentity(); + unlinkReferences() { + return this.unwrap().unlinkReferences(); } toShapeStringOrNull() { let sTarget = this.m_glyph.geometry.toShapeStringOrNull(); @@ -135,8 +135,8 @@ class TaggedGeometry extends GeometryBase { measureComplexity() { return this.m_geom.measureComplexity(); } - unwrapShapeIdentity() { - return this.m_geom.unwrapShapeIdentity(); + unlinkReferences() { + return this.m_geom.unlinkReferences(); } toShapeStringOrNull() { return this.m_geom.toShapeStringOrNull(); @@ -179,8 +179,8 @@ class TransformedGeometry extends GeometryBase { measureComplexity() { return this.m_geom.measureComplexity(); } - unwrapShapeIdentity() { - const unwrapped = this.m_geom.unwrapShapeIdentity(); + unlinkReferences() { + const unwrapped = this.m_geom.unlinkReferences(); if (Transform.isIdentity(this.m_transform)) { return unwrapped; } else if ( @@ -261,10 +261,10 @@ class CombineGeometry extends GeometryBase { for (const part of this.m_parts) s += part.measureComplexity(); } - unwrapShapeIdentity() { + unlinkReferences() { let parts = []; for (const part of this.m_parts) { - const unwrapped = part.unwrapShapeIdentity(); + const unwrapped = part.unlinkReferences(); if (unwrapped instanceof CombineGeometry) { for (const p of unwrapped.m_parts) parts.push(p); } else { @@ -340,13 +340,13 @@ class BooleanGeometry extends GeometryBase { let s = 0; for (const operand of this.m_operands) s += operand.measureComplexity(); } - unwrapShapeIdentity() { + unlinkReferences() { if (this.m_operands.length === 0) return new CombineGeometry([]); - if (this.m_operands.length === 1) return this.m_operands[0].unwrapShapeIdentity(); + if (this.m_operands.length === 1) return this.m_operands[0].unlinkReferences(); let operands = []; for (const operand of this.m_operands) { - operands.push(operand.unwrapShapeIdentity()); + operands.push(operand.unlinkReferences()); } return new BooleanGeometry(this.m_operator, operands); } diff --git a/font-src/support/glyph.js b/font-src/support/glyph.js index b8a81b83c..33940c5ef 100644 --- a/font-src/support/glyph.js +++ b/font-src/support/glyph.js @@ -131,8 +131,8 @@ module.exports = class Glyph { tryBecomeMirrorOf(dst, rankSet) { if (rankSet.has(this) || rankSet.has(dst)) return; - const csThis = this.geometry.unwrapShapeIdentity().toShapeStringOrNull(); - const csDst = dst.geometry.unwrapShapeIdentity().toShapeStringOrNull(); + const csThis = this.geometry.unlinkReferences().toShapeStringOrNull(); + const csDst = dst.geometry.unlinkReferences().toShapeStringOrNull(); if (csThis && csDst && csThis === csDst) { this.geometry = new Geom.CombineGeometry([new Geom.ReferenceGeometry(dst, 0, 0)]); rankSet.add(this);