194 lines
4.9 KiB
JavaScript
194 lines
4.9 KiB
JavaScript
import * as Format from "../util/formatter.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.controls = []; // all the control items
|
|
this.closed = false; // whether the shape is closed
|
|
this.needsUnwrap = false; // whether there are interpolators
|
|
this.afterPreFunction = false; // whether we are really processing knots
|
|
}
|
|
add(c) {
|
|
if (c instanceof Function) {
|
|
if (this.afterPreFunction) throw new Error("Invalid spiro control sequence");
|
|
c.call(this);
|
|
} else if (Array.isArray(c)) {
|
|
for (const item of c) this.add(item);
|
|
} else if (c instanceof ControlKnot) {
|
|
this.afterPreFunction = true;
|
|
this.pushKnot(c);
|
|
} else if (c instanceof TerminateInstruction) {
|
|
this.afterPreFunction = true;
|
|
if (c.type === "close") this.closed = true;
|
|
c.applyTo(this);
|
|
} else if (c instanceof InterpolatorBase) {
|
|
this.afterPreFunction = true;
|
|
this.controls.push(c);
|
|
this.needsUnwrap = true;
|
|
} else {
|
|
throw new Error("Invalid spiro control type " + String(c));
|
|
}
|
|
}
|
|
unwrap() {
|
|
while (this.needsUnwrap) {
|
|
const cs = [...this.controls];
|
|
this.controls.length = 0;
|
|
this.needsUnwrap = false;
|
|
this.lastKnot = null;
|
|
this.unwrapImpl(cs);
|
|
}
|
|
for (const item of this.controls) {
|
|
if (!(item instanceof BiKnot)) throw new Error("Invalid control sequence");
|
|
item.originalKnot = null;
|
|
}
|
|
}
|
|
unwrapImpl(cs) {
|
|
let tmp = [];
|
|
for (let j = 0; j < cs.length; j++) {
|
|
if (cs[j] instanceof InterpolatorBase) {
|
|
const kBefore = cs[nCyclic(j - 1, cs.length)];
|
|
const kAfter = cs[nCyclic(j + 1, cs.length)];
|
|
const blended = cs[j].blender(kBefore.originalKnot, kAfter.originalKnot, cs[j]);
|
|
tmp.push(blended);
|
|
} else {
|
|
tmp.push(cs[j].originalKnot);
|
|
}
|
|
}
|
|
|
|
this.add(tmp);
|
|
}
|
|
|
|
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);
|
|
}
|
|
k.originalKnot = c;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
this.originalKnot = 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;
|
|
}
|
|
toShapeString() {
|
|
return Format.tuple(
|
|
this.type,
|
|
this.unimportant,
|
|
Format.n(this.x),
|
|
Format.n(this.y),
|
|
this.d1 == null ? "" : Format.n(this.d1),
|
|
this.d2 == null ? "" : Format.n(this.d2),
|
|
this.proposedNormal
|
|
? Format.tuple(Format.n(this.proposedNormal.x), Format.n(this.proposedNormal.y))
|
|
: ""
|
|
);
|
|
}
|
|
}
|
|
|
|
function nCyclic(p, n) {
|
|
return (p + n + n) % n;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
export class ControlKnot {
|
|
constructor(type, x, y, af) {
|
|
this.type = type;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.af = af;
|
|
}
|
|
applyTo(ctx) {
|
|
if (this.af) this.af.call(ctx);
|
|
}
|
|
}
|
|
export class TerminateInstruction {
|
|
constructor(type, af) {
|
|
this.type = type;
|
|
this.af = af;
|
|
}
|
|
applyTo(ctx) {
|
|
if (this.af) throw new Error("Unreachable");
|
|
// if (this.af) this.af.call(ctx);
|
|
}
|
|
}
|
|
export class InterpolatorBase {
|
|
constructor(blender) {
|
|
this.type = "interpolate";
|
|
this.blender = blender;
|
|
}
|
|
}
|
|
export function Interpolator(blender, restParameters) {
|
|
const base = new InterpolatorBase(blender);
|
|
const interpolator = Object.create(base);
|
|
for (const prop in restParameters) interpolator[prop] = restParameters[prop];
|
|
return interpolator;
|
|
}
|
|
|
|
export class ImportanceControlKnot extends ControlKnot {
|
|
constructor(type, x, y, unimportant) {
|
|
super(type, x, y, null);
|
|
this.unimportant = unimportant;
|
|
}
|
|
}
|