Optimize the build speed by producing simpler arcs when converting spiro to outline (#2272)
This commit is contained in:
parent
4f2f0d973c
commit
a0c8c9be0b
8 changed files with 154 additions and 40 deletions
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue