import * as TypoGeom from "typo-geom"; import * as CurveUtil from "../../support/geometry/curve-util.mjs"; import * as Geom from "../../support/geometry/index.mjs"; import { Point } from "../../support/geometry/point.mjs"; import { Transform } from "../../support/geometry/transform.mjs"; /////////////////////////////////////////////////////////////////////////////////////////////////// function regulateGlyphStore(cache, skew, glyphStore) { const compositeMemo = new Map(); for (const g of glyphStore.glyphs()) { if (g.geometry.isEmpty()) continue; if (!regulateCompositeGlyph(glyphStore, compositeMemo, g)) { g.geometry = g.geometry.unlinkReferences(); } } for (const g of glyphStore.glyphs()) { if (!compositeMemo.get(g)) flattenSimpleGlyph(cache, skew, g); } } /////////////////////////////////////////////////////////////////////////////////////////////////// function memoSet(memo, g, v) { memo.set(g, v); return v; } function regulateCompositeGlyph(glyphStore, memo, g) { if (memo.has(g)) return memo.get(g); let refs = g.geometry.asReferences(); if (!refs) return memoSet(memo, g, false); for (const sr of refs) { const gn = glyphStore.queryNameOf(sr.glyph); if (!gn) return memoSet(memo, g, false); } // De-doppelganger while (refs.length === 1 && regulateCompositeGlyph(glyphStore, memo, refs[0].glyph)) { const sr = refs[0]; const targetRefs = sr.glyph.geometry.asReferences(); g.clearGeometry(); for (const tr of targetRefs) { g.includeGeometry(new Geom.ReferenceGeometry(tr.glyph, tr.x + sr.x, tr.y + sr.y)); } refs = g.geometry.asReferences(); } 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 { 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); const g1 = new Geom.TransformedGeometry( new SimplifyGeometry(new Geom.TransformedGeometry(g.geometry, tfBack)), tfForward ); const cs = g1.asContours(); g.clearGeometry(); g.includeContours(cs, 0, 0); if (ck) cache.saveGF(ck, CurveUtil.shapeToRep(cs)); } } /////////////////////////////////////////////////////////////////////////////////////////////////// class SimplifyGeometry extends Geom.GeometryBase { constructor(g) { super(); this.m_geom = g; } asContours() { const source = this.m_geom.asContours(); const sink = new FairizedShapeSink(); TypoGeom.ShapeConv.transferGenericShape( TypoGeom.Fairize.fairizeBezierShape( TypoGeom.Boolean.removeOverlap( CurveUtil.convertShapeToArcs(source), TypoGeom.Boolean.PolyFillType.pftNonZero, CurveUtil.BOOLE_RESOLUTION ) ), sink, CurveUtil.GEOMETRY_PRECISION ); return sink.contours; } asReferences() { return null; } 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 FairizedShapeSink { constructor() { this.contours = []; this.lastContour = []; } beginShape() {} endShape() { if (this.lastContour.length > 2) { // TT use CW for outline, being different from Clipper 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 = []; } 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++) { if (c[i].type === Point.Type.Corner) { alignX.tryAlign(i, (i + 1) % c.length); alignY.tryAlign(i, (i + 1) % c.length); } } 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) { alignX.tryAlign(i, (i + 1) % c.length); alignY.tryAlign(i, (i + 1) % c.length); } } alignX.apply(); alignY.apply(); return c; } cleanupOccurrentKnots1(c0) { const c = [c0[0]]; for (let i = 1; i < c0.length; i++) { if ( !( c0[i].type === Point.Type.Corner && c0[i - 1].type === Point.Type.Corner && isOccurrent(c0[i], c0[i - 1]) ) ) { c.push(c0[i]); } } 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); let lengthBefore = c.length, lengthAfter = c.length; do { lengthBefore = c.length; const 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 && 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; } } let n = 0; for (let i = 0; i < c.length; i++) { if (!shouldRemove[i]) c[n++] = c[i]; } c.length = n; lengthAfter = c.length; } while (lengthAfter < lengthBefore); return c; } } // 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], this.lens(this.c[this.find(i)])); } } } 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.type === Point.Type.Corner && zLast.type === Point.Type.Corner && 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 between(a, b, c) { return (a <= b && b <= c) || (a >= b && b >= c); } export function finalizeGlyphs(cache, para, glyphStore) { const skew = Math.tan(((para.slopeAngle || 0) / 180) * Math.PI); regulateGlyphStore(cache, skew, glyphStore); return glyphStore; }