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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.121.0",
3
+ "version": "0.121.1",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 { isPathContour, tessellateContour, pointsToContour, reverseContour, closeContourGap } from "./profile.js";
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 definitions live in profile.js (the contour-IR home) so
7
- // paper-bridge.js can use them without importing this module (which already imports
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 { pointsToContour, reverseContour };
10
-
11
- const WINDING_SEGS = 64; // tessellation LOD for orientation/containment sampling
12
-
13
- const isPointList = (x) => Array.isArray(x) && x.length > 0 && Array.isArray(x[0]);
14
- // pointsToContour always closes explicitly; a raw {start,segments} contour (hand-authored
15
- // via pathProfile, or handed back from a Shape2D's own stored regions) might not —
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
- // Circumcircle center + signed sweep for the arc through (p0, via, to) — the sweep is the
16
- // one passing through `via` (sign-free, winding-free), same recovery as profile.js's
17
- // sampleArc. Returns null for a collinear (degenerate) triple. Shared by arcToCubicSegments
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/paper-bridge.js";
44
- import { cubicAt, profileCorners } from "../geometry/contour-ops.js";
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;