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

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