Add a new spiro control flattener, and a new mechanism to propagate coordinates through the spiro construction (#2442)

This commit is contained in:
Belleve 2024-07-29 01:38:31 -10:00 committed by GitHub
parent 6dff364caf
commit 6f7c864faa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 474 additions and 133 deletions

View file

@ -1,3 +1,393 @@
// This class is used to "flatten" the spiro controls into a plain list of UserControlKnot
export class SpiroFlattener {
constructor() {
this.preControlFunctions = [];
this.controls = [];
this.postControls = [];
}
add(c) {
if (Array.isArray(c)) {
for (const item of c) this.add(item);
} else if (c instanceof Function) {
if (!this.controls.length) this.preControlFunctions.push(c);
else throw new Error("Invalid spiro control sequence");
} else if (c instanceof TerminateInstruction) {
this.postControls.push(c);
} else {
if (this.postControls.length) throw new Error("Invalid spiro control sequence");
this.controls.push(c);
}
}
flatten() {
for (let cycle = 0; cycle < 32; cycle++) {
const nd = this.flattenImpl();
if (!nd) break;
}
let final = [];
for (const c of this.controls) {
this.addToSink(final, c.resolveNonInterpolated());
}
this.controls = final;
}
pipe(collector) {
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);
}
/// Add a control object (or list) to a sink
/// Return the total number of items that may have dependencies
addToSink(sink, c) {
if (Array.isArray(c)) {
let nd = 0;
for (const item of c) nd += this.addToSink(sink, item);
return nd;
} else {
if (!c.getDependency) {
console.error(c);
}
sink.push(c);
const cHasDependency =
c.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_X) ||
c.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_Y) ||
c.getDependency(RES_DEP_STAGE_INTERPOLATION);
return cHasDependency ? 1 : 0;
}
}
flattenImpl() {
this.propagateCoordinates();
return this.doInterpolate();
}
propagateCoordinates() {
const propagator = new CoordinatePropagator(this.controls);
if (!propagator.nDependencies) return;
propagator.solveAll();
}
doInterpolate() {
let nd = 0;
let sink = [];
const dr = this.getDependenciesForInterpolation();
for (let i = 0; i < this.controls.length; i++) {
if (dr.deps[i] <= DEP_SKIP) {
nd += this.addToSink(sink, this.controls[i]);
} else {
nd += this.addToSink(
sink,
this.controls[i].resolveInterpolation(
this.controls[dr.prevNonDependentIdx[i]].getKernelKnot(),
this.controls[dr.nextNonDependentIdx[i]].getKernelKnot(),
),
);
}
}
this.controls = sink;
return nd;
}
getDependenciesForInterpolation(skipKind) {
let nNonDependent = 0;
let nDependent = 0;
let deps = [];
/// Index to the next non-dependent control
let nextNonDependentIdx = [];
let prevNonDependentIdx = [];
for (let i = 0; i < this.controls.length; i++) {
let s = this.controls[i].getDependency(RES_DEP_STAGE_INTERPOLATION);
if (s) {
nDependent += 1;
} else {
nNonDependent += 1;
}
deps.push(s);
nextNonDependentIdx.push(-1);
prevNonDependentIdx.push(-1);
}
let iFirstNonDependent = -1;
let iLastNonDependent = -1;
for (let i = 0; i < this.controls.length; i++) {
if (deps[i] === 0) {
if (iFirstNonDependent < 0) iFirstNonDependent = i;
if (iLastNonDependent >= 0) {
nextNonDependentIdx[iLastNonDependent] = i;
prevNonDependentIdx[i] = iLastNonDependent;
}
iLastNonDependent = i;
} else if (iLastNonDependent >= 0) {
prevNonDependentIdx[i] = iLastNonDependent;
}
}
if (iFirstNonDependent < 0 || iLastNonDependent < 0) {
console.log(this.controls, deps);
throw new Error("A control sequence must have at least one non-dependent control");
} else {
nextNonDependentIdx[iLastNonDependent] = iFirstNonDependent;
prevNonDependentIdx[iFirstNonDependent] = iLastNonDependent;
}
for (let i = 0; i < iFirstNonDependent; i++) {
prevNonDependentIdx[i] = iLastNonDependent;
}
for (let i = 0; i < this.controls.length; i++) {
if (deps[i] != 0) {
nextNonDependentIdx[i] = nextNonDependentIdx[prevNonDependentIdx[i]];
}
}
return { nDependent, deps, prevNonDependentIdx, nextNonDependentIdx };
}
}
/// Utility class to propagate coordinates
class CoordinatePropagator {
constructor(subjects) {
this.nDependencies = 0;
this.subjects = [];
this.depX = [];
this.stateX = [];
this.depY = [];
this.stateY = [];
for (const subject of subjects) {
let dx = subject.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_X);
let dy = subject.getDependency(RES_DEP_STAGE_COORDINATE_PROPOGATION_Y);
if (dx === DEP_SKIP && dy === DEP_SKIP) continue;
this.subjects.push(subject);
this.depX.push(dx), this.depY.push(dy);
this.stateX.push(dx > DEP_SKIP ? CR_UNRESOLVED : CR_RESOLVED);
this.stateY.push(dy > DEP_SKIP ? CR_UNRESOLVED : CR_RESOLVED);
if (dx > DEP_SKIP) this.nDependencies += 1;
if (dy > DEP_SKIP) this.nDependencies += 1;
}
}
solveAll() {
for (let i = 0; i < this.subjects.length; i++) {
this.solve(i, 0);
this.solve(i, 1);
}
}
solve(i, ic) {
const depC = ic ? this.depY : this.depX;
const stateC = ic ? this.stateY : this.stateX;
if (stateC[i] === CR_RESOLVED) return;
if (stateC[i] === CR_RESOLVING) throw new Error("Circular dependency detected");
stateC[i] = CR_RESOLVING;
if (depC[i] & DEP_PRE_X) this.solve(this.cycI(i - 1), 0);
if (depC[i] & DEP_PRE_Y) this.solve(this.cycI(i - 1), 1);
if (depC[i] & DEP_POST_X) this.solve(this.cycI(i + 1), 0);
if (depC[i] & DEP_POST_Y) this.solve(this.cycI(i + 1), 1);
// console.log(i, ic, this);
this.subjects[i].resolveCoordiantePropogation(
ic,
this.subjects[this.cycI(i - 1)].getKernelKnot(),
this.subjects[this.cycI(i + 1)].getKernelKnot(),
);
stateC[i] = CR_RESOLVED;
}
cycI(i) {
return (i + this.subjects.length) % this.subjects.length;
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
const RES_DEP_STAGE_COORDINATE_PROPOGATION_X = 0;
const RES_DEP_STAGE_COORDINATE_PROPOGATION_Y = 1;
const RES_DEP_STAGE_INTERPOLATION = 2;
export const DEP_SKIP = 0x1;
export const DEP_PRE_X = 0x2;
export const DEP_PRE_Y = 0x4;
export const DEP_POST_X = 0x8;
export const DEP_POST_Y = 0x10;
const DEP_PRE = DEP_PRE_X | DEP_PRE_Y;
const DEP_POST = DEP_POST_X | DEP_POST_Y;
const CR_UNRESOLVED = 0;
const CR_RESOLVING = 1;
const CR_RESOLVED = 2;
export class UserControlKnot {
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);
}
getDependency(stage) {
switch (stage) {
case RES_DEP_STAGE_COORDINATE_PROPOGATION_X:
return typeof this.x === "number" ? 0 : this.x.getDependency(stage);
case RES_DEP_STAGE_COORDINATE_PROPOGATION_Y:
return typeof this.y === "number" ? 0 : this.y.getDependency(stage);
case RES_DEP_STAGE_INTERPOLATION:
return 0;
default:
return 0;
}
}
getKernelKnot() {
return this;
}
resolveCoordiantePropogation(ic, pre, post) {
// console.log(this, ic, pre, post);
switch (ic) {
case 0:
this.x = this.x.resolve(pre, this, post);
break;
case 1:
this.y = this.y.resolve(pre, this, post);
break;
}
}
resolveNonInterpolated() {
return this;
}
resolveInterpolation() {
throw new Error("Unreachable");
}
static isCoordinateValid(x) {
return (typeof x === "number" && isFinite(x)) || x instanceof DerivedCoordinateBase;
}
}
export class UserCloseKnotPair {
constructor(center, tyPre, tyPost, dirX, dirY, dPre, dPost) {
this.center = center;
this.tyPre = tyPre;
this.tyPost = tyPost;
this.dirX = dirX;
this.dirY = dirY;
this.dPre = dPre;
this.dPost = dPost;
}
getDependency(stage) {
return this.center.getDependency(stage);
}
getKernelKnot() {
return this.center;
}
resolveCoordiantePropogation(ic, pre, post) {
this.center.resolveCoordiantePropogation(ic, pre, post);
}
resolveNonInterpolated() {
return [
new UserControlKnot(
this.tyPre,
this.center.x + this.dirX * this.dPre,
this.center.y + this.dirY * this.dPre,
this.center.af,
),
new UserControlKnot(
this.tyPost,
this.center.x + this.dirX * this.dPost,
this.center.y + this.dirY * this.dPost,
this.center.af,
),
];
}
resolveInterpolation() {
throw new Error("Unreachable");
}
}
export class InterpolatorBase {
constructor() {}
getDependency(stage) {
switch (stage) {
case RES_DEP_STAGE_COORDINATE_PROPOGATION_X:
case RES_DEP_STAGE_COORDINATE_PROPOGATION_Y:
return DEP_SKIP;
case RES_DEP_STAGE_INTERPOLATION:
return DEP_PRE | DEP_POST;
default:
return 0;
}
}
getKernelKnot() {
throw new Error("Unreachable");
}
resolveCoordiantePropogation(pre, post) {
throw new Error("Unreachable");
}
resolveNonInterpolated() {
throw new Error("Unreachable: All interpolations shall be resolved now");
}
resolveInterpolation(pre, post) {
throw new Error("Unimplemented");
}
}
class FunctionInterpolator extends InterpolatorBase {
constructor(blendFn, extraArgs) {
super();
this.blendFn = blendFn;
this.extraArgs = extraArgs;
}
resolveInterpolation(pre, post) {
return this.blendFn(pre, post, this.extraArgs);
}
}
export function Interpolator(blender, restParameters) {
return new FunctionInterpolator(blender, restParameters);
}
export class TerminateInstruction {
constructor(type, af) {
this.type = type;
this.af = af;
}
applyTo(ctx) {
if (this.type === "close") ctx.closed = true;
if (this.af) throw new Error("Unreachable");
// if (this.af) this.af.call(ctx);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
export class DerivedCoordinateBase {
getDependency() {
throw new Error("Unimplemented");
}
resolve(pre, curr, post) {
throw new Error("Unimplemented");
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
export class BiKnotCollector {
constructor(contrast) {
this.contrast = contrast; // stroke contrast
@ -7,57 +397,6 @@ export class BiKnotCollector {
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 UserControlKnot) {
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) {
@ -67,7 +406,6 @@ export class BiKnotCollector {
} else {
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
}
k.originalKnot = c;
this.controls.push(k);
this.lastKnot = k;
@ -143,7 +481,6 @@ class BiKnot {
// Derived properties
this.origTangent = null;
this.originalKnot = null;
}
clone() {
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
@ -184,47 +521,3 @@ class BiKnot {
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
}
}
function nCyclic(p, n) {
return (p + n + n) % n;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
export class UserControlKnot {
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;
}