Add fraktur E and G (#2448)
* Add - MATHEMATICAL FRAKTUR CAPITAL E (`U+1D508`) (#444). - MATHEMATICAL FRAKTUR CAPITAL G (`U+1D50A`) (#444). * Cleanup * Refine
This commit is contained in:
parent
d08e7db129
commit
a9a0556b17
14 changed files with 716 additions and 361 deletions
|
@ -5,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 { createSpiroPenGeometry } from "./spiro-pen-expander.mjs";
|
||||
import { PenSpiroExpander } from "./spiro-pen-expand.mjs";
|
||||
import { spiroToOutlineWithSimplification } from "./spiro-to-outline.mjs";
|
||||
import { strokeArcs } from "./stroke.mjs";
|
||||
import { Transform } from "./transform.mjs";
|
||||
|
@ -141,23 +141,23 @@ export class SpiroGeometry extends CachedGeometry {
|
|||
}
|
||||
|
||||
export class SpiroPenGeometry extends CachedGeometry {
|
||||
constructor(gizmo, closed, pen, knots) {
|
||||
constructor(gizmo, penProfile, closed, knots) {
|
||||
super();
|
||||
this.m_gizmo = gizmo;
|
||||
this.m_penProfile = penProfile;
|
||||
this.m_closed = closed;
|
||||
this.m_knots = knots;
|
||||
this.m_pen = pen;
|
||||
}
|
||||
|
||||
toContoursImpl() {
|
||||
let contours = createSpiroPenGeometry(
|
||||
const expander = new PenSpiroExpander(
|
||||
this.m_gizmo,
|
||||
this.m_penProfile,
|
||||
this.m_closed,
|
||||
this.m_knots,
|
||||
this.m_pen,
|
||||
);
|
||||
|
||||
if (!contours.length) return [];
|
||||
let contours = expander.getGeometry();
|
||||
if (!contours || !contours.length) return [];
|
||||
|
||||
let stack = [];
|
||||
for (const [i, c] of contours.entries()) {
|
||||
|
@ -189,7 +189,7 @@ export class SpiroPenGeometry extends CachedGeometry {
|
|||
|
||||
measureComplexity() {
|
||||
let cplx = CPLX_NON_EMPTY | CPLX_NON_SIMPLE;
|
||||
for (const z of this.m_pen) {
|
||||
for (const z of this.m_penProfile) {
|
||||
if (!isFinite(z.x) || !isFinite(z.y)) cplx |= CPLX_BROKEN;
|
||||
}
|
||||
for (const z of this.m_knots) {
|
||||
|
@ -204,8 +204,8 @@ export class SpiroPenGeometry extends CachedGeometry {
|
|||
h.bool(this.m_closed);
|
||||
|
||||
// Serialize the pen
|
||||
h.beginArray(this.m_pen.length);
|
||||
for (const z of this.m_pen) h.point(z);
|
||||
h.beginArray(this.m_penProfile.length);
|
||||
for (const z of this.m_penProfile) h.point(z);
|
||||
h.endArray();
|
||||
|
||||
// Serialize the knots
|
||||
|
|
|
@ -37,6 +37,7 @@ export class SpiroFlattener {
|
|||
for (const fn of this.preControlFunctions) fn.call(collector);
|
||||
for (const control of this.controls) collector.pushKnot(control);
|
||||
for (const postControl of this.postControls) postControl.applyTo(collector);
|
||||
collector.finish();
|
||||
}
|
||||
|
||||
/// Add a control object (or list) to a sink
|
||||
|
@ -326,6 +327,28 @@ export class UserCloseKnotPair {
|
|||
}
|
||||
}
|
||||
|
||||
export class VirtualControlKnot {
|
||||
constructor(x, y, af) {
|
||||
this.center = new UserControlKnot("corner", x, y, af);
|
||||
}
|
||||
|
||||
getDependency(stage) {
|
||||
return this.center.getDependency(stage);
|
||||
}
|
||||
getKernelKnot() {
|
||||
return this.center.getKernelKnot();
|
||||
}
|
||||
resolveCoordiantePropogation(ic, pre, post) {
|
||||
this.center.resolveCoordiantePropogation(ic, pre, post);
|
||||
}
|
||||
resolveNonInterpolated() {
|
||||
return [];
|
||||
}
|
||||
resolveInterpolation() {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
export class InterpolatorBase {
|
||||
constructor() {}
|
||||
|
||||
|
@ -415,6 +438,8 @@ export function WithKnotProxy(proxy, actual) {
|
|||
return new KnotProxyInterpolator(proxy, actual);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class TerminateInstruction {
|
||||
constructor(type, af) {
|
||||
this.type = type;
|
||||
|
@ -443,139 +468,3 @@ export class DerivedCoordinateBase {
|
|||
throw new Error("Unimplemented");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class BiKnotCollector {
|
||||
constructor(contrast) {
|
||||
this.contrast = contrast; // stroke contrast
|
||||
this.defaultD1 = 0; // default LHS
|
||||
this.defaultD2 = 0; // default RHS sw
|
||||
this.lastKnot = null; // last knot in the processed items
|
||||
|
||||
this.controls = []; // all the control items
|
||||
this.closed = false; // whether the shape is closed
|
||||
}
|
||||
|
||||
pushKnot(c) {
|
||||
let k;
|
||||
if (this.lastKnot) {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2);
|
||||
} else {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
||||
}
|
||||
|
||||
this.controls.push(k);
|
||||
this.lastKnot = k;
|
||||
|
||||
c.applyTo(this);
|
||||
}
|
||||
setWidth(l, r) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.d1 = l;
|
||||
this.lastKnot.d2 = r;
|
||||
} else {
|
||||
this.defaultD1 = l;
|
||||
this.defaultD2 = r;
|
||||
}
|
||||
}
|
||||
headsTo(direction) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.proposedNormal = direction;
|
||||
}
|
||||
}
|
||||
setUnimportant() {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.unimportant = 1;
|
||||
}
|
||||
}
|
||||
setContrast(c) {
|
||||
this.contrast = c;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class MonoKnot {
|
||||
constructor(type, unimportant, x, y) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.unimportant = unimportant;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new MonoKnot(this.type, this.x, this.y, this.unimportant);
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("MonoKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
reverseType() {
|
||||
if (this.type === "left") {
|
||||
this.type = "right";
|
||||
} else if (this.type === "right") {
|
||||
this.type = "left";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class BiKnot {
|
||||
constructor(type, x, y, d1, d2) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.d1 = d1;
|
||||
this.d2 = d2;
|
||||
this.proposedNormal = null;
|
||||
this.unimportant = 0;
|
||||
|
||||
// Derived properties
|
||||
this.origTangent = null;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent;
|
||||
k1.proposedNormal = this.proposedNormal;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
withGizmo(gizmo) {
|
||||
const tfZ = gizmo.applyXY(this.x, this.y);
|
||||
const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null;
|
||||
k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("BiKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
|
||||
h.bool(this.d1 != null);
|
||||
if (this.d1 != null) h.f64(this.d1);
|
||||
h.bool(this.d2 != null);
|
||||
if (this.d2 != null) h.f64(this.d2);
|
||||
|
||||
h.bool(this.proposedNormal != null);
|
||||
if (this.proposedNormal) {
|
||||
h.f64(this.proposedNormal.x);
|
||||
h.f64(this.proposedNormal.y);
|
||||
}
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
toMono() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,129 @@ import { linreg, mix } from "@iosevka/util";
|
|||
import * as SpiroJs from "spiro";
|
||||
|
||||
import { Vec2 } from "./point.mjs";
|
||||
import { MonoKnot } from "./spiro-control.mjs";
|
||||
import { MonoKnot } from "./spiro-to-outline.mjs";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class BiKnotCollector {
|
||||
constructor(contrast) {
|
||||
this.contrast = contrast; // stroke contrast
|
||||
this.defaultD1 = 0; // default LHS
|
||||
this.defaultD2 = 0; // default RHS sw
|
||||
this.lastKnot = null; // last knot in the processed items
|
||||
|
||||
this.knots = []; // all the control items
|
||||
this.closed = false; // whether the shape is closed
|
||||
|
||||
this.m_finished = false;
|
||||
}
|
||||
|
||||
get controls() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.m_finished = true;
|
||||
}
|
||||
pushKnot(c) {
|
||||
if (this.m_finished) throw new Error("Cannot push knot after finish");
|
||||
|
||||
let k;
|
||||
if (this.lastKnot) {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2);
|
||||
} else {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
||||
}
|
||||
|
||||
this.knots.push(k);
|
||||
this.lastKnot = k;
|
||||
|
||||
c.applyTo(this);
|
||||
}
|
||||
setWidth(l, r) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.d1 = l;
|
||||
this.lastKnot.d2 = r;
|
||||
} else {
|
||||
this.defaultD1 = l;
|
||||
this.defaultD2 = r;
|
||||
}
|
||||
}
|
||||
headsTo(direction) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.proposedNormal = direction;
|
||||
}
|
||||
}
|
||||
setUnimportant() {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.unimportant = 1;
|
||||
}
|
||||
}
|
||||
setContrast(c) {
|
||||
this.contrast = c;
|
||||
}
|
||||
|
||||
getMonoKnots() {
|
||||
let a = [];
|
||||
for (const c of this.knots) {
|
||||
a.push(c.toMono());
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
export class BiKnot {
|
||||
constructor(type, x, y, d1, d2) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.d1 = d1;
|
||||
this.d2 = d2;
|
||||
this.proposedNormal = null;
|
||||
this.unimportant = 0;
|
||||
|
||||
// Derived properties
|
||||
this.origTangent = null;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent;
|
||||
k1.proposedNormal = this.proposedNormal;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
withGizmo(gizmo) {
|
||||
const tfZ = gizmo.applyXY(this.x, this.y);
|
||||
const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null;
|
||||
k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("BiKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
|
||||
h.bool(this.d1 != null);
|
||||
if (this.d1 != null) h.f64(this.d1);
|
||||
h.bool(this.d2 != null);
|
||||
if (this.d2 != null) h.f64(this.d2);
|
||||
|
||||
h.bool(this.proposedNormal != null);
|
||||
if (this.proposedNormal) {
|
||||
h.f64(this.proposedNormal.x);
|
||||
h.f64(this.proposedNormal.y);
|
||||
}
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
toMono() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -78,19 +200,47 @@ export class SpiroExpander {
|
|||
return { lhs: lhsT, rhs: rhsT, lhsUntransformed: lhsU, rhsUntransformed: rhsU };
|
||||
}
|
||||
interpolateUnimportantKnots(lhsT, rhsT, lhsU, rhsU) {
|
||||
for (let j = 0; j < this.m_biKnotsU.length; j++) {
|
||||
const knotU = this.m_biKnotsU[j];
|
||||
if (!knotU.unimportant) continue;
|
||||
let jBefore, jAfter;
|
||||
for (jBefore = j - 1; cyNth(this.m_biKnotsU, jBefore).unimportant; jBefore--);
|
||||
for (jAfter = j + 1; cyNth(this.m_biKnotsU, jAfter).unimportant; jAfter++);
|
||||
let firstImportantIdx = -1;
|
||||
let lastImportantIdx = -1;
|
||||
|
||||
const knotUBefore = cyNth(this.m_biKnotsU, jBefore),
|
||||
knotUAfter = cyNth(this.m_biKnotsU, jAfter),
|
||||
lhsUBefore = cyNth(lhsU, jBefore),
|
||||
lhsUAfter = cyNth(lhsU, jAfter),
|
||||
rhsUBefore = cyNth(rhsU, jBefore),
|
||||
rhsUAfter = cyNth(rhsU, jAfter);
|
||||
for (let j = 0; j < this.m_biKnotsU.length; j++) {
|
||||
// If the current knot is unimportant, skip it
|
||||
if (this.m_biKnotsU[j].unimportant) continue;
|
||||
|
||||
// If we've scanned an important knot before, interpolate the unimportant knots between
|
||||
if (lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, lastImportantIdx, j);
|
||||
}
|
||||
|
||||
if (firstImportantIdx === -1) firstImportantIdx = j;
|
||||
lastImportantIdx = j;
|
||||
}
|
||||
|
||||
// Handle the last important ... first important wraparound
|
||||
if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantKnotsRg(
|
||||
lhsT,
|
||||
rhsT,
|
||||
lhsU,
|
||||
rhsU,
|
||||
lastImportantIdx,
|
||||
firstImportantIdx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, jBefore, jAfter) {
|
||||
let count = jAfter > jBefore ? jAfter - jBefore : lhsT.length - jBefore + jAfter;
|
||||
for (let offset = 1; offset < count; offset++) {
|
||||
let j = (jBefore + offset) % lhsT.length;
|
||||
|
||||
const knotUBefore = this.m_biKnotsU[jBefore],
|
||||
knotU = this.m_biKnotsU[j],
|
||||
knotUAfter = this.m_biKnotsU[jAfter],
|
||||
lhsUBefore = lhsU[jBefore],
|
||||
lhsUAfter = lhsU[jAfter],
|
||||
rhsUBefore = rhsU[jBefore],
|
||||
rhsUAfter = rhsU[jAfter];
|
||||
|
||||
lhsU[j].x = linreg(knotUBefore.x, lhsUBefore.x, knotUAfter.x, lhsUAfter.x, knotU.x);
|
||||
lhsU[j].y = linreg(knotUBefore.y, lhsUBefore.y, knotUAfter.y, lhsUAfter.y, knotU.y);
|
||||
|
|
243
packages/geometry/src/spiro-pen-expand.mjs
Normal file
243
packages/geometry/src/spiro-pen-expand.mjs
Normal file
|
@ -0,0 +1,243 @@
|
|||
import * as SpiroJs from "spiro";
|
||||
|
||||
import { linreg } from "@iosevka/util";
|
||||
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) {
|
||||
this.gizmo = gizmo;
|
||||
this.m_profile = defaultProfile;
|
||||
this.m_lastKnot = null;
|
||||
this.m_finished = false;
|
||||
|
||||
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;
|
||||
}
|
||||
setContrast() {}
|
||||
|
||||
setProfile(profile) {
|
||||
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);
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import * as SpiroJs from "spiro";
|
||||
import * as CurveUtil from "./curve-util.mjs";
|
||||
|
||||
import { Point } from "./point.mjs";
|
||||
|
||||
export function createSpiroPenGeometry(gizmo, closed, knots, pen) {
|
||||
const collector = new ArcCollector(gizmo, pen);
|
||||
SpiroJs.spiroToBezierOnContext(knots, closed, collector, CurveUtil.GEOMETRY_PRECISION);
|
||||
return collector.contoursCollected;
|
||||
}
|
||||
|
||||
class ArcCollector {
|
||||
constructor(gizmo, pen) {
|
||||
this.gizmo = gizmo;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.m_pen = pen;
|
||||
this.contoursCollected = [];
|
||||
}
|
||||
|
||||
beginShape() {}
|
||||
endShape() {}
|
||||
|
||||
moveTo(x, y) {
|
||||
const lastTf = this.gizmo.applyXY(x, y);
|
||||
this.lastX = lastTf.x;
|
||||
this.lastY = lastTf.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
lineTo(x1, y1) {
|
||||
const z1 = this.gizmo.applyXY(x1, y1);
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let penPrev = this.m_pen[i];
|
||||
let penNext = this.m_pen[(i + 1) % this.m_pen.length];
|
||||
|
||||
const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y);
|
||||
const l2 = new Point(Point.Type.Corner, z1.x + penPrev.x, z1.y + penPrev.y);
|
||||
const r2 = new Point(Point.Type.Corner, z1.x + penNext.x, z1.y + penNext.y);
|
||||
const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y);
|
||||
|
||||
this.contoursCollected.push([l1, l2, r2, r1]);
|
||||
}
|
||||
this.lastX = z1.x;
|
||||
this.lastY = z1.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
cubicTo(x2, y2, x3, y3, x4, y4) {
|
||||
const z2 = this.gizmo.applyXY(x2, y2);
|
||||
const z3 = this.gizmo.applyXY(x3, y3);
|
||||
const z4 = this.gizmo.applyXY(x4, y4);
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let penPrev = this.m_pen[i];
|
||||
let penNext = this.m_pen[(i + 1) % this.m_pen.length];
|
||||
|
||||
const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y);
|
||||
const l2 = new Point(Point.Type.CubicStart, z2.x + penPrev.x, z2.y + penPrev.y);
|
||||
const l3 = new Point(Point.Type.CubicEnd, z3.x + penPrev.x, z3.y + penPrev.y);
|
||||
const l4 = new Point(Point.Type.Corner, z4.x + penPrev.x, z4.y + penPrev.y);
|
||||
const r4 = new Point(Point.Type.Corner, z4.x + penNext.x, z4.y + penNext.y);
|
||||
const r3 = new Point(Point.Type.CubicStart, z3.x + penNext.x, z3.y + penNext.y);
|
||||
const r2 = new Point(Point.Type.CubicEnd, z2.x + penNext.x, z2.y + penNext.y);
|
||||
const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y);
|
||||
|
||||
this.contoursCollected.push([l1, l2, l3, l4, r4, r3, r2, r1]);
|
||||
}
|
||||
this.lastX = z4.x;
|
||||
this.lastY = z4.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
addPenProfileAt(x, y) {
|
||||
let c = [];
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let pen = this.m_pen[i];
|
||||
c.push(new Point(Point.Type.Corner, x + pen.x, y + pen.y));
|
||||
}
|
||||
this.contoursCollected.push(c);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,38 @@ export function spiroToOutlineWithSimplification(knots, fClosed, gizmo) {
|
|||
return sink.contours;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class MonoKnot {
|
||||
constructor(type, unimportant, x, y) {
|
||||
this.type = type;
|
||||
this.unimportant = unimportant;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
clone() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("MonoKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
reverseType() {
|
||||
if (this.type === "left") {
|
||||
this.type = "right";
|
||||
} else if (this.type === "right") {
|
||||
this.type = "left";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class SpiroSimplifier {
|
||||
constructor(knots) {
|
||||
this.m_knots = knots;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue