Optimize the build speed by producing simpler arcs when converting spiro to outline (#2272)

This commit is contained in:
Belleve 2024-04-02 02:47:13 -10:00 committed by GitHub
parent 4f2f0d973c
commit a0c8c9be0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 154 additions and 40 deletions

8
package-lock.json generated
View file

@ -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"
}
},

View file

@ -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)]

View file

@ -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(

View file

@ -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"
}
}

View file

@ -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() {

View file

@ -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 {

View file

@ -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) {

View file

@ -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;
}