diff --git a/package-lock.json b/package-lock.json index aaf6ee564..33b13a957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3670,9 +3670,9 @@ } }, "node_modules/spiro": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spiro/-/spiro-3.0.0.tgz", - "integrity": "sha512-UEhtLWA8fDQuExOKpT3FLa7Rk238G5Bm3wGAxbvnah3H2X6yEL4blIkAsc38wNwMXBwQFRYE6l0Q9X0t1izOxA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spiro/-/spiro-3.0.1.tgz", + "integrity": "sha512-lqnP2ng7lDrJXWSvO29FZ6zKpAkzCH6F0zmFjxQKFN4DhkoZUQDBWhwv5G8a22iSWGL2pjrbjWusp2eK3Jaj9g==", "dependencies": { "tslib": "^2.1.0" } @@ -4360,7 +4360,7 @@ "version": "29.0.6", "dependencies": { "@iosevka/util": "29.0.6", - "spiro": "^3.0.0", + "spiro": "^3.0.1", "typo-geom": "^0.15.1" } }, diff --git a/packages/font-glyphs/src/symbol/geometric/plain.ptl b/packages/font-glyphs/src/symbol/geometric/plain.ptl index c0560097a..deeaaf966 100644 --- a/packages/font-glyphs/src/symbol/geometric/plain.ptl +++ b/packages/font-glyphs/src/symbol/geometric/plain.ptl @@ -1,5 +1,6 @@ $$include '../../meta/macros.ptl' +import [OCCURRENT_PRECISION] from "@iosevka/geometry/curve-util" import [mix linreg clamp fallback] from "@iosevka/util" import [DesignParameters] from "../../meta/aesthetics.mjs" @@ -38,6 +39,8 @@ glyph-block Symbol-Geometric-Plain : for-width-kinds WideWidth1 s * Geom.Size * [fallback pp.size 1] - in * sw in * sw + define [pointsAreNotClose a b] : begin + return : [Math.abs (a.x - b.x)] > OCCURRENT_PRECISION || [Math.abs (a.y - b.y)] > OCCURRENT_PRECISION define [ConvexWhitePolygonImpl fn props] : begin local pp : fallback props {.} local sh : new-glyph : fn @@ -50,7 +53,7 @@ glyph-block Symbol-Geometric-Plain : for-width-kinds WideWidth1 foreach c [items-of : sh.geometry.toContours] : foreach j [range 0 c.length] : begin local a c.[if j (j - 1) (c.length - 1)] local b c.(j) - include : dispiro + if [pointsAreNotClose a b] : include : dispiro disable-contrast widths.center ([fallback pp.sw GeometryStroke] * 2) corner [mix a.x b.x (-2)] [mix a.y b.y (-2)] diff --git a/packages/font/src/cleanup/glyphs.mjs b/packages/font/src/cleanup/glyphs.mjs index 94bfddb07..913513240 100644 --- a/packages/font/src/cleanup/glyphs.mjs +++ b/packages/font/src/cleanup/glyphs.mjs @@ -21,7 +21,8 @@ function regulateGlyphStore(cache, skew, glyphStore) { function flattenSimpleGlyph(cache, skew, g) { try { let gSimplified; - if (skew) { + const needsTransform = g.gizmo ? !Transform.isTranslate(g.gizmo) : skew != 0; + if (needsTransform) { 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( diff --git a/packages/geometry/package.json b/packages/geometry/package.json index d5f3e1675..82453cdf8 100644 --- a/packages/geometry/package.json +++ b/packages/geometry/package.json @@ -1,20 +1,20 @@ { - "name": "@iosevka/geometry", - "version": "29.0.6", - "private": true, - "exports": { - ".": "./src/index.mjs", - "./anchor": "./src/anchor.mjs", - "./box": "./src/box.mjs", - "./segment": "./src/segment.mjs", - "./curve-util": "./src/curve-util.mjs", - "./point": "./src/point.mjs", - "./transform": "./src/transform.mjs", - "./spiro-control": "./src/spiro-control.mjs" - }, - "dependencies": { - "@iosevka/util": "29.0.6", - "spiro": "^3.0.0", - "typo-geom": "^0.15.1" - } + "name": "@iosevka/geometry", + "version": "29.0.6", + "private": true, + "exports": { + ".": "./src/index.mjs", + "./anchor": "./src/anchor.mjs", + "./box": "./src/box.mjs", + "./segment": "./src/segment.mjs", + "./curve-util": "./src/curve-util.mjs", + "./point": "./src/point.mjs", + "./transform": "./src/transform.mjs", + "./spiro-control": "./src/spiro-control.mjs" + }, + "dependencies": { + "@iosevka/util": "29.0.6", + "spiro": "^3.0.1", + "typo-geom": "^0.15.1" + } } diff --git a/packages/geometry/src/index.mjs b/packages/geometry/src/index.mjs index 6f13b8602..332829605 100644 --- a/packages/geometry/src/index.mjs +++ b/packages/geometry/src/index.mjs @@ -1,5 +1,3 @@ -import crypto from "crypto"; - import * as Format from "@iosevka/util/formatter"; import * as TypoGeom from "typo-geom"; @@ -7,7 +5,7 @@ import * as CurveUtil from "./curve-util.mjs"; import { Point } from "./point.mjs"; import { QuadifySink } from "./quadify.mjs"; import { SpiroExpander } from "./spiro-expand.mjs"; -import { spiroToOutline } from "./spiro-to-outline.mjs"; +import { spiroToOutlineWithSimplification } from "./spiro-to-outline.mjs"; import { strokeArcs } from "./stroke.mjs"; import { Transform } from "./transform.mjs"; @@ -114,7 +112,7 @@ export class SpiroGeometry extends CachedGeometry { this.m_gizmo = gizmo; } toContoursImpl() { - return spiroToOutline(this.m_knots, this.m_closed, this.m_gizmo); + return spiroToOutlineWithSimplification(this.m_knots, this.m_closed, this.m_gizmo); } toReferences() { return null; @@ -182,10 +180,10 @@ export class DiSpiroGeometry extends CachedGeometry { this.m_biKnots, ); expander.initializeNormals(); - expander.iterateNormals(); - expander.iterateNormals(); - expander.iterateNormals(); - expander.iterateNormals(); + for (let r = 0; r < 8; r++) { + let d = expander.iterateNormals(); + if (d < 1e-8) break; + } return expander.expand(); } toReferences() { diff --git a/packages/geometry/src/point.mjs b/packages/geometry/src/point.mjs index ddf07a8db..e08d36e8d 100644 --- a/packages/geometry/src/point.mjs +++ b/packages/geometry/src/point.mjs @@ -9,6 +9,10 @@ export class Vec2 { static from(z) { return new Vec2(z.x, z.y); } + + static scaleFrom(s, z) { + return new Vec2(s * z.x, s * z.y); + } } export class Point { diff --git a/packages/geometry/src/spiro-expand.mjs b/packages/geometry/src/spiro-expand.mjs index 57b4f8555..bd4fe808c 100644 --- a/packages/geometry/src/spiro-expand.mjs +++ b/packages/geometry/src/spiro-expand.mjs @@ -23,6 +23,7 @@ export class SpiroExpander { const centerBone = this.getPass2Knots(); const normalRectifier = new NormalRectifier(this.m_biKnotsT, this.m_gizmo); SpiroJs.spiroToArcsOnContext(centerBone, this.m_closed, normalRectifier); + return normalRectifier.totalDelta / normalRectifier.nKnotsProcessed; } getPass2Knots() { const expanded = this.expand(this.m_contrast); @@ -105,36 +106,49 @@ class NormalRectifier { constructor(stage1ControlKnots, gizmo) { this.m_gizmo = gizmo; this.m_biKnots = stage1ControlKnots; - this.m_nKnotsProcessed = 0; + + this.nKnotsProcessed = 0; + this.totalDelta = 0; } beginShape() {} endShape() {} moveTo(x, y) { - this.m_nKnotsProcessed += 1; + this.nKnotsProcessed += 1; } arcTo(arc, x, y) { - if (this.m_nKnotsProcessed === 1) { + if (this.nKnotsProcessed === 1) { const d = new Vec2(arc.deriveX0, arc.deriveY0); if (isTangentValid(d)) { - this.m_biKnots[0].origTangent = d; + this.updateKnotTangent(this.m_biKnots[0], d); } else { throw new Error("NaN angle detected."); } } - if (this.m_biKnots[this.m_nKnotsProcessed]) { + if (this.m_biKnots[this.nKnotsProcessed]) { const d = new Vec2(arc.deriveX1, arc.deriveY1); if (isTangentValid(d)) { - this.m_biKnots[this.m_nKnotsProcessed].origTangent = d; + this.updateKnotTangent(this.m_biKnots[this.nKnotsProcessed], d); } else { throw new Error("NaN angle detected."); } } - this.m_nKnotsProcessed += 1; + this.nKnotsProcessed += 1; + } + + updateKnotTangent(knot, d) { + if (isTangentValid(knot.origTangent)) { + this.totalDelta += + (d.x - knot.origTangent.x) * (d.x - knot.origTangent.x) + + (d.y - knot.origTangent.y) * (d.y - knot.origTangent.y); + } else { + this.totalDelta += 4; + } + knot.origTangent = d; } } function isTangentValid(d) { - return isFinite(d.x) && isFinite(d.y); + return d && isFinite(d.x) && isFinite(d.y); } function normalX(tangent, contrast) { diff --git a/packages/geometry/src/spiro-to-outline.mjs b/packages/geometry/src/spiro-to-outline.mjs index b3ee64308..5e0016e76 100644 --- a/packages/geometry/src/spiro-to-outline.mjs +++ b/packages/geometry/src/spiro-to-outline.mjs @@ -1,9 +1,103 @@ import * as SpiroJs from "spiro"; +import * as TypoGeom from "typo-geom"; import * as CurveUtil from "./curve-util.mjs"; +import { Vec2 } from "./point.mjs"; export function spiroToOutline(knots, fClosed, gizmo) { const s = new CurveUtil.BezToContoursSink(gizmo); SpiroJs.spiroToBezierOnContext(knots, fClosed, s, CurveUtil.GEOMETRY_PRECISION); return s.contours; } + +export function spiroToOutlineWithSimplification(knots, fClosed, gizmo) { + const simplifier = new SpiroSimplifier(knots); + SpiroJs.spiroToArcsOnContext(knots, fClosed, simplifier); + const sink = new CurveUtil.BezToContoursSink(gizmo); + TypoGeom.ShapeConv.transferGenericShapeAsBezier( + [simplifier.combinedArcs], + sink, + CurveUtil.GEOMETRY_PRECISION, + ); + return sink.contours; +} + +class SpiroSimplifier { + constructor(knots) { + this.m_knots = knots; + this.m_ongoingArcs = []; + this.m_nKnotsProcessed = 0; + + this.combinedArcs = []; + } + beginShape() {} + endShape() { + this.flushArcs(); + } + moveTo(x, y) { + this.m_nKnotsProcessed += 1; + } + arcTo(arc) { + this.m_ongoingArcs.push(arc); + if ( + this.m_knots[this.m_nKnotsProcessed] && + !this.m_knots[this.m_nKnotsProcessed].unimportant + ) { + this.flushArcs(); + } + this.m_nKnotsProcessed += 1; + } + flushArcs() { + if (!this.m_ongoingArcs.length) return; + if (this.m_ongoingArcs.length === 1) { + this.combinedArcs.push(this.m_ongoingArcs[0]); + } else { + this.combinedArcs.push(new SpiroSequenceArc(this.m_ongoingArcs)); + } + this.m_ongoingArcs = []; + } +} + +class SpiroSequenceArc { + constructor(segments) { + let totalLength = 0; + let stops = []; + for (let j = 0; j < segments.length; j++) { + stops[j] = totalLength; + totalLength += segments[j].arcLength; + } + for (let j = 0; j < segments.length; j++) { + stops[j] = stops[j] / totalLength; + } + this.m_segments = segments; + this.m_stops = stops; + } + + eval(t) { + const j = segTSearch(this.m_stops, t); + const tBefore = this.m_stops[j]; + const tNext = j < this.m_stops.length - 1 ? this.m_stops[j + 1] : 1; + const tRelative = (t - tBefore) / (tNext - tBefore); + return this.m_segments[j].eval(tRelative); + } + + derivative(t) { + const j = segTSearch(this.m_stops, t); + const tBefore = this.m_stops[j]; + const tNext = j < this.m_stops.length - 1 ? this.m_stops[j + 1] : 1; + const tRelative = (t - tBefore) / (tNext - tBefore); + return Vec2.scaleFrom(1 / (tNext - tBefore), this.m_segments[j].derivative(tRelative)); + } +} + +function segTSearch(stops, t) { + if (t < 0) return 0; + let l = 0, + r = stops.length; + while (l < r) { + let m = (l + r) >>> 1; + if (stops[m] > t) r = m; + else l = m + 1; + } + return r - 1; +}