partforge 0.59.0 → 0.60.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/docs/ERROR-PATTERNS.md +67 -43
- package/docs/KERNEL-CONTRACT.md +53 -40
- package/package.json +3 -2
- package/src/framework/geometry/contour-offset.js +507 -164
- package/src/framework/geometry/contour-ops.js +8 -2
- package/src/framework/geometry/contour-winding.js +610 -0
- package/src/framework/geometry/creased-normals.js +7 -3
- package/src/framework/geometry/paper-bridge.js +97 -23
|
@@ -221,33 +221,107 @@ export function booleanRegions(aRegions, bRegions, op) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
export { paperScope, toContour, toOpenContour, groupPaperPaths };
|
|
225
|
+
|
|
226
|
+
// paper reports an intersection as a time on the PAPER CURVE it hit, and that is NOT this
|
|
227
|
+
// engine's IR parameter for the segment the curve came from. segMap fixes WHICH segment; this
|
|
228
|
+
// fixes WHERE ON IT. Three separate mismatches, only one of them benign:
|
|
229
|
+
//
|
|
230
|
+
// * an ARC ({to,via}) is expanded by arcToCubicSegments into up to four ≤90° cubics that all
|
|
231
|
+
// share ONE segMap entry, so `loc.time` is the time within whichever piece was hit —
|
|
232
|
+
// measured 0.404 for a point 70.3% along a 180° arc. That is the damaging one: it is not a
|
|
233
|
+
// small error but a different number entirely, so _splitRings trims the arc at a wildly
|
|
234
|
+
// wrong sweep, and two crossings on one arc can even sort backwards.
|
|
235
|
+
// * a LINE is a zero-handle cubic in paper, whose time satisfies 3t²−2t³ = the linear
|
|
236
|
+
// fraction: measured 0.560 where the IR parameter is 0.590. Benign so far only by luck —
|
|
237
|
+
// _splitRings overwrites a line piece's endpoints with the pooled vertices, and the map is
|
|
238
|
+
// monotonic so ordering survives.
|
|
239
|
+
// * a CUBIC ({to,c1,c2}) is the only kind that round-trips: one paper curve, the same
|
|
240
|
+
// parameterization trimSegment's splitCubic uses.
|
|
241
|
+
//
|
|
242
|
+
// The parameter is recovered from the intersection POINT rather than by undoing each of those.
|
|
243
|
+
// That inverts exactly what trimSegment does — linear in position along a line, linear in
|
|
244
|
+
// ANGLE about the arc's centre (trimSegment: aS = a0 + dA·tStart, off the same
|
|
245
|
+
// arcCenterAndSweep) — and it is exact to floating point, where reconstructing the arc case
|
|
246
|
+
// from the piece index as (j + tp)/k is not: (j + tp)/k is right about the PIECE (the sweep is
|
|
247
|
+
// split into equal angular pieces, `t0 = a0 + dA·(i/pieces)` above) but still reads a Bézier
|
|
248
|
+
// time as an angular fraction WITHIN that piece, leaving up to 4.5e-3 of parameter error — and
|
|
249
|
+
// none of it corrected on a ≤90° arc, where k is 1 and the formula degenerates to `tp`. A ≤90°
|
|
250
|
+
// round join is the commonest arc this engine emits.
|
|
251
|
+
//
|
|
252
|
+
// Recovering from the point is also the more robust reading for an arc: the cubic
|
|
253
|
+
// approximation's error is essentially RADIAL, so the point's ANGLE is right even where the
|
|
254
|
+
// point itself sits a fraction off the true circle.
|
|
255
|
+
function irTime(contour, segIdx, point, paperTime) {
|
|
256
|
+
const n = contour.segments.length;
|
|
257
|
+
// segIdx === n is the closing curve closePath() synthesizes for a contour that never returns
|
|
258
|
+
// to its own start (see toPaperPath's segMap note) — a straight edge back to `start`.
|
|
259
|
+
const seg = segIdx < n ? contour.segments[segIdx] : { to: contour.start };
|
|
260
|
+
const from = segIdx === 0 ? contour.start : contour.segments[(segIdx - 1) % n].to;
|
|
261
|
+
if (seg.c1) return paperTime;
|
|
262
|
+
if (seg.via) {
|
|
263
|
+
const c = arcCenterAndSweep(from, seg.via, seg.to);
|
|
264
|
+
if (c) { // null = collinear triple: a line, below
|
|
265
|
+
const a0 = Math.atan2(from[1] - c.center[1], from[0] - c.center[0]);
|
|
266
|
+
const aP = Math.atan2(point[1] - c.center[1], point[0] - c.center[0]);
|
|
267
|
+
const span = Math.abs(c.dA);
|
|
268
|
+
const twoPi = 2 * Math.PI;
|
|
269
|
+
let d = (c.dA >= 0 ? aP - a0 : a0 - aP) % twoPi; // angle travelled from the arc's start
|
|
270
|
+
if (d < 0) d += twoPi;
|
|
271
|
+
const t = d / span;
|
|
272
|
+
// A point a rounding step BEFORE the start normalizes to nearly a whole turn rather than
|
|
273
|
+
// to ~0, and one past the end simply exceeds 1. Snap to the nearer end either way instead
|
|
274
|
+
// of handing _splitRings a parameter outside [0,1].
|
|
275
|
+
return t <= 1 ? t : (t - 1 <= twoPi / span - t ? 1 : 0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const dx = seg.to[0] - from[0], dy = seg.to[1] - from[1];
|
|
279
|
+
const L2 = dx * dx + dy * dy;
|
|
280
|
+
if (L2 < 1e-18) return paperTime; // degenerate segment: nothing to project on
|
|
281
|
+
const t = ((point[0] - from[0]) * dx + (point[1] - from[1]) * dy) / L2;
|
|
282
|
+
return t < 0 ? 0 : (t > 1 ? 1 : t);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Every crossing among a set of contour-IR rings — self-intersections of each ring plus
|
|
286
|
+
// pairwise intersections — expressed back in IR terms as { ring, seg, t, point }.
|
|
287
|
+
//
|
|
288
|
+
// This deliberately borrows the half of paper.js that works. Paper implements fat-line
|
|
289
|
+
// Bézier clipping (Sederberg–Nishita) with convex-hull rejection: recursive subdivision
|
|
290
|
+
// that returns exact (curve, t) on the original curves. Paper's weakness in this engine
|
|
291
|
+
// was never finding intersections — it is the tracing and branch selection afterwards,
|
|
292
|
+
// which contour-winding.js replaces. segMap (filled by toPaperPath) maps paper's curve
|
|
293
|
+
// index back to our IR segment index.
|
|
294
|
+
//
|
|
295
|
+
// NB paper's addCurveIntersections bails at 40 recursion levels / 4096 calls and returns
|
|
296
|
+
// a PARTIAL set on pathological input. Callers must detect that downstream (an unconsumed
|
|
297
|
+
// piece during chaining) rather than trusting completeness here.
|
|
298
|
+
export function ringCrossings(rings) {
|
|
299
|
+
if (rings.length === 0) return [];
|
|
232
300
|
const scope = paperScope();
|
|
233
301
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
|
|
302
|
+
const maps = rings.map(() => []);
|
|
303
|
+
const paths = rings.map((c, i) => toPaperPath(scope, c, maps[i]));
|
|
304
|
+
const out = [];
|
|
305
|
+
const push = (ringIdx, loc) => {
|
|
306
|
+
const seg = maps[ringIdx][loc.curve.index];
|
|
307
|
+
if (!Number.isInteger(seg)) return; // defensive: unmapped curve
|
|
308
|
+
const point = [loc.point.x, loc.point.y];
|
|
309
|
+
out.push({ ring: ringIdx, seg, t: irTime(rings[ringIdx], seg, point, loc.time), point });
|
|
310
|
+
};
|
|
311
|
+
for (let i = 0; i < paths.length; i++) {
|
|
312
|
+
for (const loc of paths[i].getIntersections()) { // self
|
|
313
|
+
push(i, loc);
|
|
314
|
+
if (loc.intersection) push(i, loc.intersection);
|
|
315
|
+
}
|
|
316
|
+
for (let j = i + 1; j < paths.length; j++) {
|
|
317
|
+
for (const loc of paths[i].getIntersections(paths[j])) { // pairwise
|
|
318
|
+
push(i, loc);
|
|
319
|
+
push(j, loc.intersection);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
242
322
|
}
|
|
243
|
-
|
|
244
|
-
const paths = (acc.className === "CompoundPath" ? acc.children : [acc])
|
|
245
|
-
.filter((p) => p.segments && p.segments.length >= 2 && Math.abs(p.area) > 1e-9);
|
|
246
|
-
if (!paths.length) return [];
|
|
247
|
-
return groupPaperPathsOriented(paths);
|
|
323
|
+
return out;
|
|
248
324
|
} finally {
|
|
249
325
|
scope.project.clear();
|
|
250
326
|
}
|
|
251
327
|
}
|
|
252
|
-
|
|
253
|
-
export { paperScope, toContour, toOpenContour, groupPaperPaths };
|