diff --git a/package-lock.json b/package-lock.json index 996ccf5d1..209577ce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4028,6 +4028,7 @@ "name": "@iosevka/geometry-cache", "version": "28.0.2", "dependencies": { + "@iosevka/geometry": "28.0.2", "@msgpack/msgpack": "^2.8.0" } }, diff --git a/packages/font-glyphs/src/marks/above.ptl b/packages/font-glyphs/src/marks/above.ptl index 8381270cc..ba4fa4736 100644 --- a/packages/font-glyphs/src/marks/above.ptl +++ b/packages/font-glyphs/src/marks/above.ptl @@ -347,7 +347,7 @@ glyph-block Mark-Above : begin define cs : new BezToContoursSink ShapeConv.transferGenericShapeAsBezier {{inner outer}} cs GEOMETRY_PRECISION - currentGlyph.includeContours cs.contours 0 0 + currentGlyph.includeContours cs.contours create-glyph 'tildeAbove' 0x303 : glyph-proc set-width 0 @@ -408,7 +408,7 @@ glyph-block Mark-Above : begin define cs : new BezToContoursSink ShapeConv.transferGenericShapeAsBezier arcs cs GEOMETRY_PRECISION - currentGlyph.includeContours cs.contours 0 0 + currentGlyph.includeContours cs.contours create-glyph : glyph-proc set-width 0 diff --git a/packages/font/src/finalize/glyphs.mjs b/packages/font/src/finalize/glyphs.mjs index adb787cb3..f43efe7cf 100644 --- a/packages/font/src/finalize/glyphs.mjs +++ b/packages/font/src/finalize/glyphs.mjs @@ -1,8 +1,5 @@ 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 * as TypoGeom from "typo-geom"; /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -40,41 +37,12 @@ function regulateCompositeGlyph(glyphStore, memo, g) { 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); } -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) { memo.set(g, v); return v; @@ -82,309 +50,34 @@ function memoSet(memo, g, v) { /////////////////////////////////////////////////////////////////////////////////////////////////// -class SimplifyGeometry extends Geom.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(); - } - 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); +function flattenSimpleGlyph(cache, skew, g) { + const ck = Geom.hashGeometry(g.geometry); + const cached = cache.getGF(ck); + if (ck && cached) { + g.clearGeometry(); + g.includeContours(cached); + 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 Geom.SimplifyGeometry(new Geom.TransformedGeometry(g.geometry, tfBack)), + tfForward + ); } else { - alignX.tryAlign(iNext, i); - alignY.tryAlign(iNext, i); + gSimplified = new Geom.SimplifyGeometry(g.geometry); } - } - 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 && - 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)]))); + const cs = gSimplified.asContours(); + g.clearGeometry(); + g.includeContours(cs); + if (ck) cache.saveGF(ck, cs); + } catch (e) { + console.error("Detected broken geometry when processing", g._m_identifier); + throw e; } } } - -// 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; -} diff --git a/packages/font/src/otd-conv/glyphs.mjs b/packages/font/src/otd-conv/glyphs.mjs index 10395c66c..c87e1bc48 100644 --- a/packages/font/src/otd-conv/glyphs.mjs +++ b/packages/font/src/otd-conv/glyphs.mjs @@ -1,3 +1,4 @@ +import * as Geom from "@iosevka/geometry"; import { Point } from "@iosevka/geometry/point"; import * as Gr from "@iosevka/glyph/relation"; import { Ot } from "ot-builder"; @@ -41,15 +42,18 @@ class MappedGlyphStore { fill(name, source) { const g = this.queryBySourceGlyph(source); if (!g) throw new Error("Unreachable"); + // Fill metrics g.horizontal = { start: 0, end: source.advanceWidth }; + // Fill Geometry - if (source.geometry.isEmpty()) return; - const rs = source.geometry.asReferences(); - if (rs) { - this.fillReferences(g, rs); - } else { - this.fillContours(g, source.geometry.asContours()); + if (!source.geometry.isEmpty()) { + const rs = source.geometry.asReferences(); + if (rs) { + this.fillReferences(g, rs); + } else { + this.fillContours(g, source.geometry.asContours()); + } } } fillOtGlyphNames() { diff --git a/packages/geometry-cache/package.json b/packages/geometry-cache/package.json index e1aa95ed3..815ae27da 100644 --- a/packages/geometry-cache/package.json +++ b/packages/geometry-cache/package.json @@ -1,11 +1,12 @@ { - "name": "@iosevka/geometry-cache", - "version": "28.0.2", - "private": true, - "exports": { - ".": "./src/index.mjs" - }, - "dependencies": { - "@msgpack/msgpack": "^2.8.0" - } + "name": "@iosevka/geometry-cache", + "version": "28.0.2", + "private": true, + "exports": { + ".": "./src/index.mjs" + }, + "dependencies": { + "@iosevka/geometry": "28.0.2", + "@msgpack/msgpack": "^2.8.0" + } } diff --git a/packages/geometry-cache/src/index.mjs b/packages/geometry-cache/src/index.mjs index b72a7c7b1..ee91738ac 100644 --- a/packages/geometry-cache/src/index.mjs +++ b/packages/geometry-cache/src/index.mjs @@ -1,9 +1,10 @@ import fs from "fs"; import zlib from "zlib"; +import * as CurveUtil from "@iosevka/geometry/curve-util"; import { encode, decode } from "@msgpack/msgpack"; -const Edition = 30; +const Edition = 31; const MAX_AGE = 16; class GfEntry { constructor(age, value) { @@ -23,7 +24,8 @@ class Cache { this.historyAgeKeys = rep.ageKeys.slice(0, MAX_AGE); const ageKeySet = new Set(this.historyAgeKeys); 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) { diff --git a/packages/geometry/src/curve-util.mjs b/packages/geometry/src/curve-util.mjs index 5077926fc..8035d1cd2 100644 --- a/packages/geometry/src/curve-util.mjs +++ b/packages/geometry/src/curve-util.mjs @@ -67,7 +67,6 @@ function convertContourToArcs(contour) { return newContour; } -export const SPIRO_PRECISION = 1 / 2; export const OCCURRENT_PRECISION = 1 / 16; export const GEOMETRY_PRECISION = 1 / 4; export const BOOLE_RESOLUTION = 0x4000; diff --git a/packages/geometry/src/index.mjs b/packages/geometry/src/index.mjs index b48019203..58541f3ab 100644 --- a/packages/geometry/src/index.mjs +++ b/packages/geometry/src/index.mjs @@ -6,6 +6,7 @@ import * as TypoGeom from "typo-geom"; import * as CurveUtil from "./curve-util.mjs"; import { Point } from "./point.mjs"; +import { QuadifySink } from "./quadify.mjs"; import { SpiroExpander } from "./spiro-expand.mjs"; import { Transform } from "./transform.mjs"; @@ -39,19 +40,15 @@ export class GeometryBase { } } -export class ContourGeometry extends GeometryBase { - constructor(points) { +export class InvalidGeometry extends GeometryBase {} + +export class ContourSetGeometry extends GeometryBase { + constructor(contours) { super(); - this.m_points = []; - for (const z of points) { - this.m_points.push(Point.from(z.type, z)); - } + this.m_contours = contours; } asContours() { - if (this.isEmpty()) return []; - let c1 = []; - for (const z of this.m_points) c1.push(Point.from(z.type, z)); - return [c1]; + return this.m_contours; } asReferences() { return null; @@ -63,16 +60,19 @@ export class ContourGeometry extends GeometryBase { return this; } isEmpty() { - return !this.m_points.length; + return !this.m_contours.length; } measureComplexity() { - for (const z of this.m_points) { + for (const z of this.m_contours) { if (!isFinite(z.x) || !isFinite(z.y)) return 0xffff; } - return this.m_points.length; + return this.m_contours.length; } 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() { if (this.m_cachedContours) return this.m_cachedContours; 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; 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) { if (a instanceof CombineGeometry) { return a.with(b); diff --git a/packages/geometry/src/quadify.mjs b/packages/geometry/src/quadify.mjs new file mode 100644 index 000000000..f6611d991 --- /dev/null +++ b/packages/geometry/src/quadify.mjs @@ -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; +} diff --git a/packages/glyph/src/glyph.mjs b/packages/glyph/src/glyph.mjs index f756b2538..744b62f1f 100644 --- a/packages/glyph/src/glyph.mjs +++ b/packages/glyph/src/glyph.mjs @@ -123,14 +123,8 @@ export class Glyph { if (this.ctxTag) g = new Geom.TaggedGeometry(g, this.ctxTag); this.geometry = Geom.combineWith(this.geometry, g); } - includeContours(cs, shiftX, shiftY) { - let parts = []; - 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)); + includeContours(cs) { + this.includeGeometry(new Geom.ContourSetGeometry(cs)); } applyTransform(tfm, alsoAnchors) { this.geometry = new Geom.TransformedGeometry(this.geometry, tfm);