373 lines
10 KiB
JavaScript
373 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
const Transform = require("./transform.js");
|
|
const quadify = require("primitive-quadify-off-curves");
|
|
|
|
const ANGLES = 12;
|
|
const SMALL = 1e-4;
|
|
function solveTS(a, b, c, out, flag) {
|
|
const delta = b * b - 4 * a * c;
|
|
if (delta > 0) {
|
|
const t1 = (Math.sqrt(delta) - b) / (2 * a);
|
|
const t2 = (-Math.sqrt(delta) - b) / (2 * a);
|
|
if (flag) {
|
|
if (t1 >= 0 && t1 <= 1) out.push(t1);
|
|
if (t2 >= 0 && t2 <= 1) out.push(t2);
|
|
} else {
|
|
if (t1 > 0 && t1 < 1) out.push(t1);
|
|
if (t2 > 0 && t2 < 1) out.push(t2);
|
|
}
|
|
} else if (delta === 0) {
|
|
const t = -b / (2 * a);
|
|
if (flag) {
|
|
if (t >= 0 && t <= 1) out.push(t);
|
|
} else {
|
|
if (t > 0 && t < 1) out.push(t);
|
|
}
|
|
}
|
|
}
|
|
function findExtrema(z1, z2, z3, z4, out) {
|
|
const a = 3 * (-z1.y + 3 * z2.y - 3 * z3.y + z4.y);
|
|
const b = 6 * (z1.y - 2 * z2.y + z3.y);
|
|
const c = 3 * (z2.y - z1.y);
|
|
solveTS(a, b, c, out);
|
|
}
|
|
// function findInflections(z1, z2, z3, z4, out) {
|
|
// const ax = z2.x - z1.x;
|
|
// const ay = z2.y - z1.y;
|
|
// const bx = z3.x - z2.x - ax;
|
|
// const by = z3.y - z2.y - ay;
|
|
// const cx = z4.x - z3.x - ax - 2 * bx;
|
|
// const cy = z4.y - z3.y - ay - 2 * by;
|
|
// solveTS(bx * cy - by * cx, ax * cy - ay * cx, ax * by - ay * bx, out, true);
|
|
// }
|
|
function rotate(z, angle) {
|
|
const c = Math.cos(angle),
|
|
s = Math.sin(angle);
|
|
return {
|
|
x: c * z.x + s * z.y,
|
|
y: -s * z.x + c * z.y
|
|
};
|
|
}
|
|
function ASCEND(a, b) {
|
|
return a - b;
|
|
}
|
|
function fineAllExtrema(z1, z2, z3, z4, angles) {
|
|
let exs = [];
|
|
// findInflections(z1, z2, z3, z4, exs);
|
|
for (let a = 0; a < angles; a += 1) {
|
|
findExtrema(z1, z2, z3, z4, exs);
|
|
z1 = rotate(z1, Math.PI / angles);
|
|
z2 = rotate(z2, Math.PI / angles);
|
|
z3 = rotate(z3, Math.PI / angles);
|
|
z4 = rotate(z4, Math.PI / angles);
|
|
}
|
|
return exs.sort(ASCEND);
|
|
}
|
|
function mix(z1, z2, t) {
|
|
if (t <= 0) return z1;
|
|
if (t >= 1) return z2;
|
|
let x = (1 - t) * z1.x + t * z2.x,
|
|
y = (1 - t) * z1.y + t * z2.y;
|
|
return { x: x, y: y };
|
|
}
|
|
function bez2(z1, z2, z3, t) {
|
|
if (t <= 0) return z1;
|
|
if (t >= 1) return z3;
|
|
let c1 = (1 - t) * (1 - t),
|
|
c2 = 2 * (1 - t) * t,
|
|
c3 = t * t;
|
|
return {
|
|
x: c1 * z1.x + c2 * z2.x + c3 * z3.x,
|
|
y: c1 * z1.y + c2 * z2.y + c3 * z3.y
|
|
};
|
|
}
|
|
function bez3(z1, z2, z3, z4, t) {
|
|
if (t <= 0) return z1;
|
|
if (t >= 1) return z4;
|
|
let c1 = (1 - t) * (1 - t) * (1 - t),
|
|
c2 = 3 * t * (1 - t) * (1 - t),
|
|
c3 = 3 * t * t * (1 - t),
|
|
c4 = t * t * t;
|
|
return {
|
|
x: c1 * z1.x + c2 * z2.x + c3 * z3.x + c4 * z4.x,
|
|
y: c1 * z1.y + c2 * z2.y + c3 * z3.y + c4 * z4.y
|
|
};
|
|
}
|
|
function splitBefore(z1, z2, z3, z4, t) {
|
|
return [z1, mix(z1, z2, t), bez2(z1, z2, z3, t), bez3(z1, z2, z3, z4, t)];
|
|
}
|
|
function splitAfter(z1, z2, z3, z4, t) {
|
|
return [bez3(z1, z2, z3, z4, t), bez2(z2, z3, z4, t), mix(z3, z4, t), z4];
|
|
}
|
|
function splitAtExtrema(z1, z2, z3, z4, angles, curve) {
|
|
const ts = fineAllExtrema(z1, z2, z3, z4, angles);
|
|
if (ts[0] < SMALL) {
|
|
ts[0] = 0;
|
|
} else {
|
|
ts.unshift(0);
|
|
}
|
|
if (ts[ts.length - 1] > 1 - SMALL) {
|
|
ts[ts.length - 1] = 1;
|
|
} else {
|
|
ts.push(1);
|
|
}
|
|
for (let k = 0; k < ts.length; k++) {
|
|
if (k > 0) {
|
|
const t1 = ts[k - 1];
|
|
const t2 = ts[k];
|
|
const bef = splitBefore(z1, z2, z3, z4, t2);
|
|
const seg = splitAfter(bef[0], bef[1], bef[2], bef[3], t1 / t2);
|
|
seg[1].on = seg[2].on = false;
|
|
seg[1].cubic = seg[2].cubic = true;
|
|
seg[3].on = true;
|
|
curve.push(seg[1], seg[2], seg[3]);
|
|
}
|
|
}
|
|
}
|
|
// function splitSegment(z1, z2, z3, z4, angles, curve) {
|
|
// let ts = [];
|
|
// let inflectAtEnd = false;
|
|
// // findInflections(z1, z2, z3, z4, ts);
|
|
// // ts = ts.sort(ASCEND);
|
|
// if (ts[0] < SMALL) {
|
|
// ts[0] = 0;
|
|
// curve[curve.length - 1].inflect = true;
|
|
// } else {
|
|
// ts.unshift(0);
|
|
// }
|
|
// if (ts[ts.length - 1] > 1 - SMALL) {
|
|
// inflectAtEnd = true;
|
|
// ts[ts.length - 1] = 1;
|
|
// } else {
|
|
// ts.push(1);
|
|
// }
|
|
// for (let k = 0; k < ts.length; k++) {
|
|
// if (k > 0) {
|
|
// const t1 = ts[k - 1];
|
|
// const t2 = ts[k];
|
|
// const bef = splitBefore(z1, z2, z3, z4, t2);
|
|
// const seg = splitAfter(bef[0], bef[1], bef[2], bef[3], t1 / t2);
|
|
// splitAtExtrema(seg[0], seg[1], seg[2], seg[3], angles, curve);
|
|
// if (t2 < 1 || inflectAtEnd) curve[curve.length - 1].inflect = true;
|
|
// }
|
|
// }
|
|
// }
|
|
function veryClose(z1, z2) {
|
|
return (z1.x - z2.x) * (z1.x - z2.x) + (z1.y - z2.y) * (z1.y - z2.y) <= SMALL;
|
|
}
|
|
|
|
function splitCurve(sourceCurve) {
|
|
const curve = [sourceCurve[0]];
|
|
let last = sourceCurve[0];
|
|
for (let j = 1; j < sourceCurve.length; j++) {
|
|
if (sourceCurve[j].on) {
|
|
const z1 = last,
|
|
z4 = sourceCurve[j];
|
|
// const z2 = mix(z1, z4, 1 / 3);
|
|
//const z3 = mix(z1, z4, 2 / 3);
|
|
if (!veryClose(z1, z4)) {
|
|
curve.push(z4);
|
|
// splitAtExtrema(z1, z2, z3, z4, ANGLES, curve);
|
|
last = z4;
|
|
}
|
|
} else if (sourceCurve[j].cubic) {
|
|
const z1 = last,
|
|
z2 = sourceCurve[j],
|
|
z3 = sourceCurve[j + 1],
|
|
z4 = sourceCurve[j + 2];
|
|
if (!(veryClose(z1, z2) && veryClose(z2, z3) && veryClose(z3, z4))) {
|
|
splitAtExtrema(z1, z2, z3, z4, ANGLES, curve);
|
|
last = z4;
|
|
}
|
|
j += 2;
|
|
} else {
|
|
const z1 = last,
|
|
zm = sourceCurve[j],
|
|
z4 = sourceCurve[j + 1];
|
|
if (!(veryClose(z1, zm) && veryClose(zm, z4))) {
|
|
const z2 = mix(zm, z1, 1 / 3);
|
|
const z3 = mix(zm, z4, 1 / 3);
|
|
splitAtExtrema(z1, z2, z3, z4, ANGLES, curve);
|
|
last = z4;
|
|
}
|
|
j += 1;
|
|
}
|
|
}
|
|
return curve;
|
|
}
|
|
|
|
function cross(z1, z2, z3) {
|
|
return (z2.x - z1.x) * (z3.y - z1.y) - (z3.x - z1.x) * (z2.y - z1.y);
|
|
}
|
|
function dot(z1, z2, z3) {
|
|
return (z2.x - z1.x) * (z3.x - z1.x) + (z3.y - z1.y) * (z2.y - z1.y);
|
|
}
|
|
|
|
function markCorners(curve) {
|
|
for (let j = 0; j < curve.length; j++) {
|
|
if (!curve[j].on) continue;
|
|
const z1 = curve[j],
|
|
z0 = curve[(j - 1 + curve.length) % curve.length],
|
|
z2 = curve[(j + 1) % curve.length];
|
|
if (Math.abs(cross(z1, z0, z2)) < 1e-6) {
|
|
// Z0 -- Z1 -- Z2 are linear
|
|
if (!z0.on && !z2.on && dot(z1, z0, z2) < 0) {
|
|
const angle0 = Math.atan2(z2.y - z0.y, z2.x - z0.x);
|
|
const angle = Math.abs(((angle0 / Math.PI) * 2) % 1);
|
|
if (
|
|
Math.abs(Math.abs(angle0) - Math.PI / 2) <= SMALL ||
|
|
angle <= SMALL ||
|
|
angle >= 1 - SMALL
|
|
) {
|
|
z1.mark = true; // curve extremum
|
|
}
|
|
} else if (z0.on && z2.on && dot(z1, z0, z2) < 0) {
|
|
// Colinear on-knots
|
|
// Remove
|
|
} else {
|
|
z1.mark = true; // also corner
|
|
}
|
|
} else {
|
|
z1.mark = true; // corner
|
|
}
|
|
}
|
|
}
|
|
|
|
class BezierCurveCluster {
|
|
constructor(zs) {
|
|
let segments = [];
|
|
let lengths = [];
|
|
let last = zs[0];
|
|
for (let j = 1; j < zs.length; j++) {
|
|
if (zs[j].on) {
|
|
const z1 = last,
|
|
z4 = zs[j];
|
|
const z2 = mix(z1, z4, 1 / 3);
|
|
const z3 = mix(z1, z4, 2 / 3);
|
|
segments.push(new quadify.CubicBezierCurve(z1, z2, z3, z4));
|
|
lengths.push(Math.hypot(z4.x - z1.x, z4.y - z1.y));
|
|
last = z4;
|
|
} else if (zs[j].cubic) {
|
|
const z1 = last,
|
|
z2 = zs[j],
|
|
z3 = zs[j + 1],
|
|
z4 = zs[j + 2];
|
|
segments.push(new quadify.CubicBezierCurve(z1, z2, z3, z4));
|
|
lengths.push(Math.hypot(z4.x - z1.x, z4.y - z1.y));
|
|
last = z4;
|
|
j += 2;
|
|
} else {
|
|
const z1 = last,
|
|
zm = zs[j],
|
|
z4 = zs[j + 1];
|
|
const z2 = mix(zm, z1, 1 / 3);
|
|
const z3 = mix(zm, z4, 1 / 3);
|
|
segments.push(new quadify.CubicBezierCurve(z1, z2, z3, z4));
|
|
lengths.push(Math.hypot(z4.x - z1.x, z4.y - z1.y));
|
|
last = z4;
|
|
j += 1;
|
|
}
|
|
}
|
|
|
|
let totalLength = 0;
|
|
for (let j = 0; j < lengths.length; j++) totalLength += lengths[j];
|
|
let lengthSofar = 0;
|
|
for (let j = 0; j < lengths.length; j++) {
|
|
let segLen = lengths[j];
|
|
lengths[j] = lengthSofar / totalLength;
|
|
lengthSofar += segLen;
|
|
}
|
|
this.segments = segments;
|
|
this.lengths = lengths;
|
|
// console.log(this.eval(0), this.eval(1 / 2), this.eval(1));
|
|
// console.log(this.derivative(0), this.derivative(1 / 2), this.derivative(1));
|
|
}
|
|
getIndex(t) {
|
|
let j = this.lengths.length - 1;
|
|
while (j > 0 && this.lengths[j] > t) j--;
|
|
return j;
|
|
}
|
|
eval(t) {
|
|
const j = this.getIndex(t);
|
|
const tBefore = this.lengths[j];
|
|
const tNext = j < this.lengths.length - 1 ? this.lengths[j + 1] : 1;
|
|
const tRelative = (t - tBefore) / (tNext - tBefore);
|
|
return this.segments[j].eval(tRelative);
|
|
}
|
|
derivative(t) {
|
|
const j = this.getIndex(t);
|
|
const tBefore = this.lengths[j];
|
|
const tNext = j < this.lengths.length - 1 ? this.lengths[j + 1] : 1;
|
|
const tRelative = (t - tBefore) / (tNext - tBefore);
|
|
// console.log(
|
|
// t,
|
|
// tRelative,
|
|
// tNext,
|
|
// tBefore,
|
|
// tNext - tBefore,
|
|
// this.segments[j].derivative(tRelative)
|
|
// );
|
|
const d = this.segments[j].derivative(tRelative);
|
|
d.x /= tNext - tBefore;
|
|
d.y /= tNext - tBefore;
|
|
return d;
|
|
}
|
|
}
|
|
|
|
function buildCurve(curve) {
|
|
let exitPoints = [];
|
|
for (let j = 0; j < curve.length; j++) {
|
|
if (!curve[j].mark) continue;
|
|
let k = j;
|
|
for (; k < curve.length && (k === j || !curve[k].mark); k++);
|
|
exitPoints.push(curve[j]);
|
|
const pts = curve.slice(j, k + 1);
|
|
let nPtsOffPoints = 0;
|
|
for (const z of pts) {
|
|
if (!z.on) nPtsOffPoints += 1;
|
|
}
|
|
if (nPtsOffPoints > 0) {
|
|
const curve = new BezierCurveCluster(pts);
|
|
const offPoints = quadify.autoQuadify(curve, 1 / 4);
|
|
if (!offPoints) continue;
|
|
for (let k = 0; k < offPoints.length; k++) {
|
|
const z = offPoints[k];
|
|
if (k > 0) {
|
|
const z0 = offPoints[k - 1];
|
|
exitPoints.push({
|
|
x: (z.x + z0.x) / 2,
|
|
y: (z.y + z0.y) / 2,
|
|
on: true
|
|
});
|
|
}
|
|
exitPoints.push({
|
|
x: z.x,
|
|
y: z.y,
|
|
cubic: false,
|
|
on: false
|
|
});
|
|
}
|
|
}
|
|
j = k - 1;
|
|
}
|
|
return exitPoints;
|
|
}
|
|
|
|
module.exports = function(sourceCurve, gizmo) {
|
|
for (let j = 0; j < sourceCurve.length; j++) {
|
|
if (!isFinite(sourceCurve[j].x)) sourceCurve[j].x = 0;
|
|
if (!isFinite(sourceCurve[j].y)) sourceCurve[j].y = 0;
|
|
sourceCurve[j] = Transform.untransform(gizmo, sourceCurve[j]);
|
|
}
|
|
const curve = splitCurve(sourceCurve);
|
|
markCorners(curve);
|
|
const builtCurve = buildCurve(curve);
|
|
const ans = [];
|
|
for (let j = 0; j < builtCurve.length; j++) {
|
|
if (builtCurve[j] && !builtCurve[j].remove) {
|
|
ans.push(Transform.transformPoint(gizmo, builtCurve[j]));
|
|
}
|
|
}
|
|
return ans;
|
|
};
|