Iosevka/packages/geometry/src/spiro-pen-expand.mjs
2024-10-11 19:09:52 -07:00

246 lines
6.3 KiB
JavaScript

import { linreg } from "@iosevka/util";
import * as SpiroJs from "spiro";
import * as CurveUtil from "./curve-util.mjs";
import { Point } from "./point.mjs";
import { MonoKnot } from "./spiro-to-outline.mjs";
export class PenKnotCollector {
constructor(gizmo, defaultProfile, fProfileFixed) {
this.gizmo = gizmo;
this.m_profile = defaultProfile;
this.m_lastKnot = null;
this.m_finished = false;
this.m_profileFixed = fProfileFixed;
this.knots = [];
this.closed = false;
}
finish() {
this.m_finished = true;
}
pushKnot(c) {
if (this.m_finished) throw new Error("Cannot push knot after finish");
let k = new PenKnot(c.type, c.x, c.y, this.m_profile);
this.knots.push(k);
this.m_lastKnot = k;
c.applyTo(this);
}
setWidth() {}
headsTo() {}
setUnimportant() {
if (this.m_lastKnot) this.m_lastKnot.profile = null;
}
setImportant() {} // Not used
setContrast() {}
setProfile(profile) {
if (this.m_profileFixed) return;
if (profile.length !== this.m_profile.length)
throw new Error("Pen profile length mismatch");
if (this.m_lastKnot) this.m_lastKnot.profile = profile;
this.m_profile = profile;
}
}
export class PenKnot {
constructor(type, x, y, profile) {
this.type = type;
this.x = x;
this.y = y;
this.profile = profile;
}
clone() {
const k1 = new PenKnot(this.type, this.x, this.y, this.profile);
return k1;
}
withGizmo(gizmo) {
const tfZ = gizmo.applyXY(this.x, this.y);
const k1 = new PenKnot(this.type, tfZ.x, tfZ.y, this.profile);
return k1;
}
hash(h) {
h.beginStruct("PenKnot");
h.str(this.type);
h.f64(this.x);
h.f64(this.y);
h.bool(this.profile != null);
if (this.profile) {
h.beginArray(this.profile.length);
for (let i = 0; i < this.profile.length; i++) {
h.f64(this.profile[i].x);
h.f64(this.profile[i].y);
}
h.endArray();
}
h.endStruct();
}
}
export class PenSpiroExpander {
constructor(gizmo, profile, closed, knots) {
this.m_gizmo = gizmo;
this.m_closed = closed;
this.m_knotsU = Array.from(knots);
this.m_knotsT = knots.map(k => k.withGizmo(gizmo));
this.m_profileEdges = profile.length;
this.m_traces = [];
}
getGeometry() {
this.traceAll();
const contours = [];
for (let i = 0; i < this.m_traces.length; i++) {
const iNext = (i + 1) % this.m_traces.length;
if (this.m_traces[i].length !== this.m_traces[iNext].length) {
throw new Error("Different number of arcs in traces");
}
for (let j = 0; j < this.m_traces[i].length; j++) {
const arcForward = this.m_traces[i][j];
const arcBackward = this.m_traces[iNext][j];
makeProfiledStroke(contours, arcForward, arcBackward);
}
}
return contours;
}
traceAll() {
for (let i = 0; i < this.m_profileEdges; i++) {
this.calculateShiftedProfile(i);
}
}
calculateShiftedProfile(iEdge) {
let traceT = [],
traceU = [];
for (let i = 0; i < this.m_knotsT.length; i++) {
const trT = this.getTrace(this.m_knotsT[i], iEdge);
const trU = trT.clone();
this.m_gizmo.unapplyToSink(trT, trU);
traceT.push(trT), traceU.push(trU);
}
this.interpolateUnimportantTraceKnots(traceU, traceT);
let arcc = new SimplyCollectArcs();
SpiroJs.spiroToArcsOnContext(traceT, this.m_closed, arcc);
this.m_traces.push(arcc.arcs);
}
getTrace(k, iEdge) {
if (k.profile) {
return new MonoKnot(k.type, false, k.x + k.profile[iEdge].x, k.y + k.profile[iEdge].y);
} else {
return new MonoKnot(k.type, true, k.x, k.y);
}
}
interpolateUnimportantTraceKnots(traceU, traceT) {
let firstImportantIdx = -1;
let lastImportantIdx = -1;
for (let i = 0; i < traceU.length; i++) {
if (traceU[i].unimportant) continue;
if (lastImportantIdx !== -1) {
this.interpolateUnimportantTraceKnotsRg(traceU, traceT, lastImportantIdx, i);
}
if (firstImportantIdx === -1) firstImportantIdx = i;
lastImportantIdx = i;
}
if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
this.interpolateUnimportantTraceKnotsRg(
traceU,
traceT,
lastImportantIdx,
firstImportantIdx,
);
}
}
interpolateUnimportantTraceKnotsRg(traceU, traceT, last, next) {
let count = next > last ? next - last : traceU.length - last + next;
for (let offset = 1; offset < count; offset++) {
let i = (last + offset) % traceU.length;
this.interpolateKnot(
this.m_knotsU[last],
traceU[last],
this.m_knotsU[i],
traceU[i],
traceT[i],
this.m_knotsU[next],
traceU[next],
);
}
}
interpolateKnot(rBefore, uBefore, rCurr, uCurr, tCurr, rAfter, uAfter) {
uCurr.x = linreg(rBefore.x, uBefore.x, rAfter.x, uAfter.x, rCurr.x);
uCurr.y = linreg(rBefore.y, uBefore.y, rAfter.y, uAfter.y, rCurr.y);
this.m_gizmo.applyToSink(uCurr, tCurr);
}
}
class SimplyCollectArcs {
constructor() {
this.arcs = [];
}
beginShape() {}
endShape() {}
moveTo() {}
arcTo(arc) {
this.arcs.push(arc);
}
}
// arcForward and arcBackward must be spiro arcs.
function makeProfiledStroke(contours, arcForward, arcBackward) {
const [subdividesForward, subdividesBackward] = subdivideKnotPair(
arcForward,
arcBackward,
CurveUtil.GEOMETRY_PRECISION,
);
for (let i = 0; i < subdividesForward.length; i++) {
const [a1, b1, c1, d1] = subdividesForward[i].toCubicBezier();
const [a2, b2, c2, d2] = subdividesBackward[i].toCubicBezier();
contours.push([
Point.from(Point.Type.Corner, a1),
Point.from(Point.Type.CubicStart, b1),
Point.from(Point.Type.CubicEnd, c1),
Point.from(Point.Type.Corner, d1),
Point.from(Point.Type.Corner, d2),
Point.from(Point.Type.CubicStart, c2),
Point.from(Point.Type.CubicEnd, b2),
Point.from(Point.Type.Corner, a2),
]);
}
}
function subdivideKnotPair(arcForward, arcBackward, delta) {
const MAX_STOPS = 16;
let sinkForward = [],
sinkBackward = [];
for (let stops = 1; stops < MAX_STOPS; stops++) {
sinkForward.length = 0;
sinkBackward.length = 0;
uniformSubdivide(arcForward, stops, sinkForward);
uniformSubdivide(arcBackward, stops, sinkBackward);
let flatEnough = true;
for (const fwd of sinkForward) if (fwd.bend > delta) flatEnough = false;
for (const bwd of sinkBackward) if (bwd.bend > delta) flatEnough = false;
if (flatEnough) break;
}
return [sinkForward, sinkBackward];
}
function uniformSubdivide(arc, stops, sink) {
for (; stops > 1; stops--) {
const f = arc.subdivide(1 / stops);
sink.push(f[0]), (arc = f[1]);
}
sink.push(arc);
}