partforge 0.121.0 → 0.121.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Pure arc math, paper-free: circumcircle center + signed sweep for the arc through three
|
|
2
|
+
// points. Split out of paper-bridge.js so callers that only need this number crunching (the
|
|
3
|
+
// oracle's shape-probe.js, contour-ops.js's corner/tangent math) don't pull in paper-core —
|
|
4
|
+
// paper-bridge.js still re-exports arcCenterAndSweep so every existing importer of it is
|
|
5
|
+
// unaffected.
|
|
6
|
+
|
|
7
|
+
// Circumcircle center + signed sweep for the arc through (p0, via, to) — the sweep is the
|
|
8
|
+
// one passing through `via` (sign-free, winding-free), same recovery as profile.js's
|
|
9
|
+
// sampleArc. Returns null for a collinear (degenerate) triple. Shared by arcToCubicSegments
|
|
10
|
+
// (paper-bridge.js) and contour-ops.js's jointTangents (arc tangents are ⊥ radius, oriented by dA's sign).
|
|
11
|
+
export function arcCenterAndSweep(p0, via, to) {
|
|
12
|
+
const [ax, ay] = p0, [bx, by] = via, [cx, cy] = to;
|
|
13
|
+
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
14
|
+
if (Math.abs(d) < 1e-12) return null;
|
|
15
|
+
const sa = ax*ax + ay*ay, sb = bx*bx + by*by, sc = cx*cx + cy*cy;
|
|
16
|
+
const ux = (sa * (by - cy) + sb * (cy - ay) + sc * (ay - by)) / d;
|
|
17
|
+
const uy = (sa * (cx - bx) + sb * (ax - cx) + sc * (bx - ax)) / d;
|
|
18
|
+
const r = Math.hypot(ax - ux, ay - uy);
|
|
19
|
+
const a0 = Math.atan2(ay - uy, ax - ux);
|
|
20
|
+
const av = Math.atan2(by - uy, bx - ux);
|
|
21
|
+
const a1 = Math.atan2(cy - uy, cx - ux);
|
|
22
|
+
const twoPi = 2 * Math.PI;
|
|
23
|
+
const ccw = (x) => { let v = x % twoPi; if (v < 0) v += twoPi; return v; };
|
|
24
|
+
const dCCW = ccw(a1 - a0), vCCW = ccw(av - a0);
|
|
25
|
+
const dA = vCCW <= dCCW ? dCCW : dCCW - twoPi;
|
|
26
|
+
return { center: [ux, uy], r, dA };
|
|
27
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Paper-free corner/tangent math for the contour IR, split out of contour-ops.js so the
|
|
2
|
+
// oracle's shape-probe.js (and anything else that only needs a shape's corners/tangents, not
|
|
3
|
+
// its booleans/transforms) doesn't pull in paper-core through contour-ops.js's own
|
|
4
|
+
// paper-bridge.js import. Imports only profile.js, shape2d-regions.js and arc-math.js — never
|
|
5
|
+
// paper-bridge.js or contour-ops.js, which is what keeps this leaf paper-free.
|
|
6
|
+
import { isPathContour, tessellateContour, pointsToContour, closeContourGap } from "./profile.js";
|
|
7
|
+
import { ringArea } from "./shape2d-regions.js";
|
|
8
|
+
import { arcCenterAndSweep } from "./arc-math.js";
|
|
9
|
+
|
|
10
|
+
export const WINDING_SEGS = 64; // tessellation LOD for orientation/containment sampling
|
|
11
|
+
|
|
12
|
+
export const isPointList = (x) => Array.isArray(x) && x.length > 0 && Array.isArray(x[0]);
|
|
13
|
+
// pointsToContour always closes explicitly; a raw {start,segments} contour (hand-authored
|
|
14
|
+
// via pathProfile, or handed back from a Shape2D's own stored regions) might not —
|
|
15
|
+
// closeContourGap is a no-op when it's already closed, so every ring liftProfile hands to
|
|
16
|
+
// contour-ops' corner/transform/query functions is guaranteed explicitly closed.
|
|
17
|
+
export const liftContour = (c) => (isPointList(c) ? pointsToContour(c) : closeContourGap(c));
|
|
18
|
+
|
|
19
|
+
export function liftProfile(input) {
|
|
20
|
+
if (input && input._shape2d) return { kind: "regions", regions: input.toContours() };
|
|
21
|
+
if (isPointList(input)) return { kind: "points", regions: [{ outer: pointsToContour(input), holes: [] }] };
|
|
22
|
+
if (isPathContour(input)) return { kind: "contour", regions: [{ outer: closeContourGap(input), holes: [] }] };
|
|
23
|
+
if (Array.isArray(input) && input.every((r) => r && r.outer))
|
|
24
|
+
return { kind: "regions", regions: input.map((r) => ({ outer: liftContour(r.outer), holes: (r.holes ?? []).map(liftContour) })) };
|
|
25
|
+
if (input && input.outer)
|
|
26
|
+
return { kind: "region", regions: [{ outer: liftContour(input.outer), holes: (input.holes ?? []).map(liftContour) }] };
|
|
27
|
+
throw new Error("contour-ops: input must be [[x,y],…], a {start,segments} contour, {outer,holes}, or a region array");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const contourIsCCW = (c) => ringArea(tessellateContour(c, WINDING_SEGS)) >= 0;
|
|
31
|
+
|
|
32
|
+
// ── Corner model ────────────────────────────────────────────────────────────
|
|
33
|
+
// profileCorners() walks a contour's joints and reports each non-smooth one: the interior
|
|
34
|
+
// angle, whether it's convex (material-relative — see below), and the segment kinds either
|
|
35
|
+
// side of it. jointTangents() is the shared per-vertex tangent computation, reused by Task 6.
|
|
36
|
+
|
|
37
|
+
export const SMOOTH_JOINT_DEG = 1;
|
|
38
|
+
|
|
39
|
+
// Unit tangent of segment `s` (from `from`) at its start (dir=+1) or end (dir=-1 → arrival direction).
|
|
40
|
+
// Exported for contour-winding.js's _chain: junction ordering at a curved pinch point needs
|
|
41
|
+
// the exact endpoint tangent, not an approximation (via/c1/c2 are NOT control points that
|
|
42
|
+
// happen to sit near the tangent — via in particular is a THROUGH point near mid-sweep, so
|
|
43
|
+
// from->via is systematically biased by about sweep/4). This recovers it exactly for arcs
|
|
44
|
+
// (perpendicular to the radius at the recovered center, oriented by the sweep's sign) and
|
|
45
|
+
// cubics (including the degenerate c1===from case, where the true tangent comes from c2).
|
|
46
|
+
export function segTangent(from, s, atStart) {
|
|
47
|
+
const norm = ([x, y]) => { const L = Math.hypot(x, y) || 1; return [x / L, y / L]; };
|
|
48
|
+
if (s.c1) {
|
|
49
|
+
if (atStart) {
|
|
50
|
+
const d = [s.c1[0] - from[0], s.c1[1] - from[1]];
|
|
51
|
+
return norm(Math.hypot(d[0], d[1]) > 1e-9 ? d : [s.c2[0] - from[0], s.c2[1] - from[1]]);
|
|
52
|
+
}
|
|
53
|
+
const d = [s.to[0] - s.c2[0], s.to[1] - s.c2[1]];
|
|
54
|
+
return norm(Math.hypot(d[0], d[1]) > 1e-9 ? d : [s.to[0] - s.c1[0], s.to[1] - s.c1[1]]);
|
|
55
|
+
}
|
|
56
|
+
if (s.via) {
|
|
57
|
+
// tangent ⊥ radius, oriented along the sweep (recover center like arcToCubicSegments)
|
|
58
|
+
const c = arcCenterAndSweep(from, s.via, s.to); // {center:[x,y], dA} or null
|
|
59
|
+
if (!c) return norm([s.to[0] - from[0], s.to[1] - from[1]]);
|
|
60
|
+
const p = atStart ? from : s.to;
|
|
61
|
+
const r = [p[0] - c.center[0], p[1] - c.center[1]];
|
|
62
|
+
const t = c.dA >= 0 ? [-r[1], r[0]] : [r[1], -r[0]];
|
|
63
|
+
return norm(t);
|
|
64
|
+
}
|
|
65
|
+
return norm([s.to[0] - from[0], s.to[1] - from[1]]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function jointTangents(contour) { // per vertex i: tangent arriving at and leaving vertex i
|
|
69
|
+
const n = contour.segments.length;
|
|
70
|
+
const pts = [contour.start, ...contour.segments.map((s) => s.to)];
|
|
71
|
+
return contour.segments.map((_, i) => {
|
|
72
|
+
const prevSeg = contour.segments[(i - 1 + n) % n];
|
|
73
|
+
const prevFrom = pts[(i - 1 + n) % n];
|
|
74
|
+
return {
|
|
75
|
+
point: pts[i],
|
|
76
|
+
inTan: segTangent(prevFrom, prevSeg, false),
|
|
77
|
+
outTan: segTangent(pts[i], contour.segments[i], true),
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const segType = (s) => (s.c1 ? "cubic" : s.via ? "arc" : "line");
|
|
83
|
+
|
|
84
|
+
export function contourCorners(contour) {
|
|
85
|
+
const ccw = contourIsCCW(contour);
|
|
86
|
+
const n = contour.segments.length;
|
|
87
|
+
const out = [];
|
|
88
|
+
jointTangents(contour).forEach(({ point, inTan, outTan }, i) => {
|
|
89
|
+
const cross = inTan[0] * outTan[1] - inTan[1] * outTan[0];
|
|
90
|
+
const dot = Math.min(1, Math.max(-1, inTan[0] * outTan[0] + inTan[1] * outTan[1]));
|
|
91
|
+
const turnDeg = (Math.atan2(Math.abs(cross), dot) * 180) / Math.PI;
|
|
92
|
+
if (turnDeg < SMOOTH_JOINT_DEG) return;
|
|
93
|
+
const leftTurn = cross > 0;
|
|
94
|
+
out.push({
|
|
95
|
+
index: i, point: [point[0], point[1]],
|
|
96
|
+
interiorAngleDeg: ccw === leftTurn ? 180 - turnDeg : 180 + turnDeg,
|
|
97
|
+
convex: leftTurn === ccw,
|
|
98
|
+
segTypes: [segType(contour.segments[(i - 1 + n) % n]), segType(contour.segments[i])],
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Two numbers on every corner, and they are NOT interchangeable. `index` is the joint's
|
|
105
|
+
// vertex number within its own contour (what buildCornerOpRing, simplify and the lofts
|
|
106
|
+
// key on); `position` is the corner's place in THIS returned list — the one
|
|
107
|
+
// `{corners: {indices}}` selects by. They diverge as soon as a contour has a smooth
|
|
108
|
+
// joint (a collinear midpoint, a G1 arc-line join), and on region input `index`
|
|
109
|
+
// restarts per ring while `position` runs on through the flattened order.
|
|
110
|
+
const withPositions = (corners) => corners.map((c, position) => ({ ...c, position }));
|
|
111
|
+
|
|
112
|
+
export function profileCorners(input) {
|
|
113
|
+
const { kind, regions } = liftProfile(input);
|
|
114
|
+
if (kind === "points" || kind === "contour") return withPositions(contourCorners(regions[0].outer));
|
|
115
|
+
const out = [];
|
|
116
|
+
regions.forEach((rg, regionIndex) => {
|
|
117
|
+
for (const c of contourCorners(rg.outer)) out.push({ regionIndex, ring: "outer", ...c });
|
|
118
|
+
rg.holes.forEach((h, hi) => { for (const c of contourCorners(h)) out.push({ regionIndex, ring: { hole: hi }, ...c }); });
|
|
119
|
+
});
|
|
120
|
+
return withPositions(out);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function cubicAt(p0, c1, c2, p1, t) {
|
|
124
|
+
const u = 1 - t;
|
|
125
|
+
return [0, 1].map((k) => u * u * u * p0[k] + 3 * u * u * t * c1[k] + 3 * u * t * t * c2[k] + t * t * t * p1[k]);
|
|
126
|
+
}
|
|
127
|
+
export function cubicDeriv(p0, c1, c2, p1, t) {
|
|
128
|
+
const u = 1 - t;
|
|
129
|
+
return [0, 1].map((k) => 3 * u * u * (c1[k] - p0[k]) + 6 * u * t * (c2[k] - c1[k]) + 3 * t * t * (p1[k] - c2[k]));
|
|
130
|
+
}
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reverseContour, closeContourGap } from "./profile.js";
|
|
2
2
|
import { ringArea, pointInRing } from "./shape2d-regions.js";
|
|
3
3
|
import { arcToCubicSegments, arcCenterAndSweep, paperScope, toPaperPath, toContour, toOpenContour } from "./paper-bridge.js";
|
|
4
|
+
import { liftProfile, contourIsCCW, contourCorners, cubicAt, cubicDeriv } from "./contour-corners.js";
|
|
4
5
|
|
|
5
6
|
// Re-exported so existing importers (test/contour-ops-lift.test.js and others) keep
|
|
6
|
-
// working unchanged — the
|
|
7
|
-
// paper-bridge.js can use
|
|
7
|
+
// working unchanged — the definition lives in profile.js (the contour-IR home) so
|
|
8
|
+
// paper-bridge.js can use it without importing this module (which already imports
|
|
8
9
|
// paper-bridge.js; importing back would cycle).
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
// closeContourGap is a no-op when it's already closed, so every ring liftProfile hands to
|
|
17
|
-
// contour-ops' corner/transform/query functions is guaranteed explicitly closed.
|
|
18
|
-
const liftContour = (c) => (isPointList(c) ? pointsToContour(c) : closeContourGap(c));
|
|
19
|
-
|
|
20
|
-
export function liftProfile(input) {
|
|
21
|
-
if (input && input._shape2d) return { kind: "regions", regions: input.toContours() };
|
|
22
|
-
if (isPointList(input)) return { kind: "points", regions: [{ outer: pointsToContour(input), holes: [] }] };
|
|
23
|
-
if (isPathContour(input)) return { kind: "contour", regions: [{ outer: closeContourGap(input), holes: [] }] };
|
|
24
|
-
if (Array.isArray(input) && input.every((r) => r && r.outer))
|
|
25
|
-
return { kind: "regions", regions: input.map((r) => ({ outer: liftContour(r.outer), holes: (r.holes ?? []).map(liftContour) })) };
|
|
26
|
-
if (input && input.outer)
|
|
27
|
-
return { kind: "region", regions: [{ outer: liftContour(input.outer), holes: (input.holes ?? []).map(liftContour) }] };
|
|
28
|
-
throw new Error("contour-ops: input must be [[x,y],…], a {start,segments} contour, {outer,holes}, or a region array");
|
|
29
|
-
}
|
|
10
|
+
export { reverseContour };
|
|
11
|
+
|
|
12
|
+
// The paper-free corner/tangent/lift math now lives in contour-corners.js — so the oracle's
|
|
13
|
+
// shape-probe.js can use profileCorners()/cubicAt() without pulling in paper-core through
|
|
14
|
+
// this module's own paper-bridge.js import below — re-exported here so every existing
|
|
15
|
+
// importer of these names from contour-ops.js keeps working unchanged.
|
|
16
|
+
export { liftProfile, contourIsCCW, SMOOTH_JOINT_DEG, segTangent, jointTangents, profileCorners, cubicAt } from "./contour-corners.js";
|
|
30
17
|
|
|
31
18
|
export function restoreProfile(kind, regions) {
|
|
32
19
|
if (kind === "regions") return regions;
|
|
@@ -43,8 +30,6 @@ export function restoreProfile(kind, regions) {
|
|
|
43
30
|
return outer; // curves were introduced — upgrade to a contour
|
|
44
31
|
}
|
|
45
32
|
|
|
46
|
-
export const contourIsCCW = (c) => ringArea(tessellateContour(c, WINDING_SEGS)) >= 0;
|
|
47
|
-
|
|
48
33
|
export function ensureRegionWinding(region) {
|
|
49
34
|
return {
|
|
50
35
|
outer: contourIsCCW(region.outer) ? region.outer : reverseContour(region.outer),
|
|
@@ -116,97 +101,6 @@ export function mirrorProfile(input, axis) {
|
|
|
116
101
|
return transformProfile(input, [a, b, b, -a, px - a * px - b * py, py - b * px + a * py]);
|
|
117
102
|
}
|
|
118
103
|
|
|
119
|
-
// ── Corner model ────────────────────────────────────────────────────────────
|
|
120
|
-
// profileCorners() walks a contour's joints and reports each non-smooth one: the interior
|
|
121
|
-
// angle, whether it's convex (material-relative — see below), and the segment kinds either
|
|
122
|
-
// side of it. jointTangents() is the shared per-vertex tangent computation, reused by Task 6.
|
|
123
|
-
|
|
124
|
-
export const SMOOTH_JOINT_DEG = 1;
|
|
125
|
-
|
|
126
|
-
// Unit tangent of segment `s` (from `from`) at its start (dir=+1) or end (dir=-1 → arrival direction).
|
|
127
|
-
// Exported for contour-winding.js's _chain: junction ordering at a curved pinch point needs
|
|
128
|
-
// the exact endpoint tangent, not an approximation (via/c1/c2 are NOT control points that
|
|
129
|
-
// happen to sit near the tangent — via in particular is a THROUGH point near mid-sweep, so
|
|
130
|
-
// from->via is systematically biased by about sweep/4). This recovers it exactly for arcs
|
|
131
|
-
// (perpendicular to the radius at the recovered center, oriented by the sweep's sign) and
|
|
132
|
-
// cubics (including the degenerate c1===from case, where the true tangent comes from c2).
|
|
133
|
-
export function segTangent(from, s, atStart) {
|
|
134
|
-
const norm = ([x, y]) => { const L = Math.hypot(x, y) || 1; return [x / L, y / L]; };
|
|
135
|
-
if (s.c1) {
|
|
136
|
-
if (atStart) {
|
|
137
|
-
const d = [s.c1[0] - from[0], s.c1[1] - from[1]];
|
|
138
|
-
return norm(Math.hypot(d[0], d[1]) > 1e-9 ? d : [s.c2[0] - from[0], s.c2[1] - from[1]]);
|
|
139
|
-
}
|
|
140
|
-
const d = [s.to[0] - s.c2[0], s.to[1] - s.c2[1]];
|
|
141
|
-
return norm(Math.hypot(d[0], d[1]) > 1e-9 ? d : [s.to[0] - s.c1[0], s.to[1] - s.c1[1]]);
|
|
142
|
-
}
|
|
143
|
-
if (s.via) {
|
|
144
|
-
// tangent ⊥ radius, oriented along the sweep (recover center like arcToCubicSegments)
|
|
145
|
-
const c = arcCenterAndSweep(from, s.via, s.to); // {center:[x,y], dA} or null
|
|
146
|
-
if (!c) return norm([s.to[0] - from[0], s.to[1] - from[1]]);
|
|
147
|
-
const p = atStart ? from : s.to;
|
|
148
|
-
const r = [p[0] - c.center[0], p[1] - c.center[1]];
|
|
149
|
-
const t = c.dA >= 0 ? [-r[1], r[0]] : [r[1], -r[0]];
|
|
150
|
-
return norm(t);
|
|
151
|
-
}
|
|
152
|
-
return norm([s.to[0] - from[0], s.to[1] - from[1]]);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function jointTangents(contour) { // per vertex i: tangent arriving at and leaving vertex i
|
|
156
|
-
const n = contour.segments.length;
|
|
157
|
-
const pts = [contour.start, ...contour.segments.map((s) => s.to)];
|
|
158
|
-
return contour.segments.map((_, i) => {
|
|
159
|
-
const prevSeg = contour.segments[(i - 1 + n) % n];
|
|
160
|
-
const prevFrom = pts[(i - 1 + n) % n];
|
|
161
|
-
return {
|
|
162
|
-
point: pts[i],
|
|
163
|
-
inTan: segTangent(prevFrom, prevSeg, false),
|
|
164
|
-
outTan: segTangent(pts[i], contour.segments[i], true),
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const segType = (s) => (s.c1 ? "cubic" : s.via ? "arc" : "line");
|
|
170
|
-
|
|
171
|
-
function contourCorners(contour) {
|
|
172
|
-
const ccw = contourIsCCW(contour);
|
|
173
|
-
const n = contour.segments.length;
|
|
174
|
-
const out = [];
|
|
175
|
-
jointTangents(contour).forEach(({ point, inTan, outTan }, i) => {
|
|
176
|
-
const cross = inTan[0] * outTan[1] - inTan[1] * outTan[0];
|
|
177
|
-
const dot = Math.min(1, Math.max(-1, inTan[0] * outTan[0] + inTan[1] * outTan[1]));
|
|
178
|
-
const turnDeg = (Math.atan2(Math.abs(cross), dot) * 180) / Math.PI;
|
|
179
|
-
if (turnDeg < SMOOTH_JOINT_DEG) return;
|
|
180
|
-
const leftTurn = cross > 0;
|
|
181
|
-
out.push({
|
|
182
|
-
index: i, point: [point[0], point[1]],
|
|
183
|
-
interiorAngleDeg: ccw === leftTurn ? 180 - turnDeg : 180 + turnDeg,
|
|
184
|
-
convex: leftTurn === ccw,
|
|
185
|
-
segTypes: [segType(contour.segments[(i - 1 + n) % n]), segType(contour.segments[i])],
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
return out;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Two numbers on every corner, and they are NOT interchangeable. `index` is the joint's
|
|
192
|
-
// vertex number within its own contour (what buildCornerOpRing, simplify and the lofts
|
|
193
|
-
// key on); `position` is the corner's place in THIS returned list — the one
|
|
194
|
-
// `{corners: {indices}}` selects by. They diverge as soon as a contour has a smooth
|
|
195
|
-
// joint (a collinear midpoint, a G1 arc-line join), and on region input `index`
|
|
196
|
-
// restarts per ring while `position` runs on through the flattened order.
|
|
197
|
-
const withPositions = (corners) => corners.map((c, position) => ({ ...c, position }));
|
|
198
|
-
|
|
199
|
-
export function profileCorners(input) {
|
|
200
|
-
const { kind, regions } = liftProfile(input);
|
|
201
|
-
if (kind === "points" || kind === "contour") return withPositions(contourCorners(regions[0].outer));
|
|
202
|
-
const out = [];
|
|
203
|
-
regions.forEach((rg, regionIndex) => {
|
|
204
|
-
for (const c of contourCorners(rg.outer)) out.push({ regionIndex, ring: "outer", ...c });
|
|
205
|
-
rg.holes.forEach((h, hi) => { for (const c of contourCorners(h)) out.push({ regionIndex, ring: { hole: hi }, ...c }); });
|
|
206
|
-
});
|
|
207
|
-
return withPositions(out);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
104
|
// ── Fillet / chamfer ─────────────────────────────────────────────────────────
|
|
211
105
|
// Resolve opts.corners against a flat corner list (contourCorners()'s order, or
|
|
212
106
|
// profileCorners()'s flattened order for region/regions input — {indices} always
|
|
@@ -280,14 +174,6 @@ const normalize2 = ([x, y]) => { const L = Math.hypot(x, y) || 1; return [x / L,
|
|
|
280
174
|
const rot90 = ([x, y]) => [-y, x];
|
|
281
175
|
const addScaled = (p, v, s) => [p[0] + v[0] * s, p[1] + v[1] * s];
|
|
282
176
|
|
|
283
|
-
export function cubicAt(p0, c1, c2, p1, t) {
|
|
284
|
-
const u = 1 - t;
|
|
285
|
-
return [0, 1].map((k) => u * u * u * p0[k] + 3 * u * u * t * c1[k] + 3 * u * t * t * c2[k] + t * t * t * p1[k]);
|
|
286
|
-
}
|
|
287
|
-
function cubicDeriv(p0, c1, c2, p1, t) {
|
|
288
|
-
const u = 1 - t;
|
|
289
|
-
return [0, 1].map((k) => 3 * u * u * (c1[k] - p0[k]) + 6 * u * t * (c2[k] - c1[k]) + 3 * t * t * (p1[k] - c2[k]));
|
|
290
|
-
}
|
|
291
177
|
// Exact de Casteljau split of cubic (p0,c1,c2,p1) at t → two exact cubic pieces.
|
|
292
178
|
export function splitCubic(p0, c1, c2, p1, t) {
|
|
293
179
|
const lerp = (a, b) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// package-global project — another consumer in the same worker may import paper too.
|
|
4
4
|
import paper from "paper/dist/paper-core.js";
|
|
5
5
|
import { tessellateContour, reverseContour, closeContourGap } from "./profile.js";
|
|
6
|
+
import { arcCenterAndSweep } from "./arc-math.js";
|
|
6
7
|
|
|
7
8
|
const ORIENT_SEGS = 8; // points/segment for the local orientation sampler below
|
|
8
9
|
|
|
@@ -12,27 +13,9 @@ function paperScope() {
|
|
|
12
13
|
return _scope;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
// below and contour-ops.js's jointTangents (arc tangents are ⊥ radius, oriented by dA's sign).
|
|
19
|
-
export function arcCenterAndSweep(p0, via, to) {
|
|
20
|
-
const [ax, ay] = p0, [bx, by] = via, [cx, cy] = to;
|
|
21
|
-
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
22
|
-
if (Math.abs(d) < 1e-12) return null;
|
|
23
|
-
const sa = ax*ax + ay*ay, sb = bx*bx + by*by, sc = cx*cx + cy*cy;
|
|
24
|
-
const ux = (sa * (by - cy) + sb * (cy - ay) + sc * (ay - by)) / d;
|
|
25
|
-
const uy = (sa * (cx - bx) + sb * (ax - cx) + sc * (bx - ax)) / d;
|
|
26
|
-
const r = Math.hypot(ax - ux, ay - uy);
|
|
27
|
-
const a0 = Math.atan2(ay - uy, ax - ux);
|
|
28
|
-
const av = Math.atan2(by - uy, bx - ux);
|
|
29
|
-
const a1 = Math.atan2(cy - uy, cx - ux);
|
|
30
|
-
const twoPi = 2 * Math.PI;
|
|
31
|
-
const ccw = (x) => { let v = x % twoPi; if (v < 0) v += twoPi; return v; };
|
|
32
|
-
const dCCW = ccw(a1 - a0), vCCW = ccw(av - a0);
|
|
33
|
-
const dA = vCCW <= dCCW ? dCCW : dCCW - twoPi;
|
|
34
|
-
return { center: [ux, uy], r, dA };
|
|
35
|
-
}
|
|
16
|
+
// arcCenterAndSweep now lives in arc-math.js (paper-free), re-exported here so every
|
|
17
|
+
// existing importer of it from this module keeps working unchanged.
|
|
18
|
+
export { arcCenterAndSweep } from "./arc-math.js";
|
|
36
19
|
|
|
37
20
|
// Circular arc through (p0, via, to) → cubic Bézier segments, ≤90° each, endpoints
|
|
38
21
|
// exact. Each piece uses the standard k = (4/3)·tan(θ/4) control-point offset.
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
// out at rings[] → ring → arcs[] → arc → center[] — 4 containers below the summary
|
|
41
41
|
// object itself (5 including it), against the cloud's `DESCRIBE_MAX_DEPTH = 8`, with
|
|
42
42
|
// headroom even wrapped in the paired-probe `{pair: {mine, ref}}` form.
|
|
43
|
-
import { arcCenterAndSweep } from "../geometry/
|
|
44
|
-
import { cubicAt, profileCorners } from "../geometry/contour-
|
|
43
|
+
import { arcCenterAndSweep } from "../geometry/arc-math.js";
|
|
44
|
+
import { cubicAt, profileCorners } from "../geometry/contour-corners.js";
|
|
45
45
|
|
|
46
46
|
export const MAX_ARCS = 64;
|
|
47
47
|
export const MAX_CORNERS = 64;
|