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
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -308,19 +308,19 @@ Variant literals under this entry: `offsetPolygon: delta must be a finite number
|
|
|
308
308
|
|
|
309
309
|
## shape2d-offset-partial-reflection-residual
|
|
310
310
|
|
|
311
|
-
- **Symptom:**
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
311
|
+
- **Symptom:** *(Fixed for positive round offsets in `0.60.0`; the ID remains
|
|
312
|
+
permanent.)* An outward `Shape2D.offset` on a region with a hole could leave a
|
|
313
|
+
residual hole ring after the source counter should have closed.
|
|
314
|
+
- **Cause:** A fully eroded source counter could survive the raw offset as a
|
|
315
|
+
locally valid negative loop. Positive-winding cleanup then correctly preserved
|
|
316
|
+
that loop because the defect was introduced before winding classification.
|
|
317
|
+
- **Fix:** The round resolver now computes a conservative largest-inscribed-disk
|
|
318
|
+
bound from the source hole and removes a residual counter only when the requested
|
|
319
|
+
dilation has certainly passed its source inradius. Upgrade to
|
|
320
|
+
`partforge >= 0.60.0`. Sharp and chamfer offsets use different structuring
|
|
321
|
+
elements and are not covered by this round-specific gate; continue to verify
|
|
322
|
+
those styles at the production offset and use explicit boolean stages when their
|
|
323
|
+
closing topology is critical.
|
|
324
324
|
|
|
325
325
|
Searchable phrasings of the same misbehavior: hole doesn't disappear after offset;
|
|
326
326
|
pocket not closed by offset; residual hole ring; `.holes.length` still 1 after
|
|
@@ -328,39 +328,40 @@ growing a shape past the hole's own width.
|
|
|
328
328
|
|
|
329
329
|
## shape2d-offset-reflex-cluster-too-much-material
|
|
330
330
|
|
|
331
|
-
- **Symptom:**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
chamfer
|
|
341
|
-
see [KERNEL-CONTRACT.md "Offset: known limitations"](KERNEL-CONTRACT.md#offset-known-limitations).
|
|
342
|
-
- **Fix:** No general fix yet. Check `.area()` against the expected eroded area
|
|
343
|
-
rather than trusting a large inward offset over a spiky outline; work around by
|
|
344
|
-
using `corners: "round"` (which does not produce the same chord tangle), by
|
|
345
|
-
simplifying the outline before offsetting, or by offsetting in smaller stages.
|
|
331
|
+
- **Symptom:** *(Fixed in `0.60.0`; the ID remains permanent.)* An inward
|
|
332
|
+
`Shape2D.offset` with clustered reflex corners could retain material outside the
|
|
333
|
+
true eroded shape.
|
|
334
|
+
- **Cause:** The old overlap-side trim could extend offset lines to an intersection
|
|
335
|
+
outside the finite extent of one or both segments. The deleted Paper.js
|
|
336
|
+
`resolveSelfRegions` path is not part of the current resolver.
|
|
337
|
+
- **Fix:** Overlap-side trimming now requires the intersection to lie within both
|
|
338
|
+
segment extents, and the resulting arrangement is resolved by positive winding.
|
|
339
|
+
Upgrade to `partforge >= 0.60.0`. The regression oracle pins the clustered-reflex
|
|
340
|
+
9-gon chamfer area at approximately `3.553831 mm²`.
|
|
346
341
|
|
|
347
342
|
## shape2d-offset-waist-not-severed-round-join
|
|
348
343
|
|
|
349
|
-
- **Symptom:**
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
regions under
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
arc or
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
344
|
+
- **Symptom:** *(Fixed — kept because IDs are permanent. This pattern no longer
|
|
345
|
+
exists.)* An inward `Shape2D.offset` past the width of a narrow waist used to leave
|
|
346
|
+
the shape connected (or add a spurious blob where the waist was) under
|
|
347
|
+
`corners: "round"` or `"chamfer"`, while `"sharp"` split it correctly. The entry's
|
|
348
|
+
own witness now behaves: a 30×10 dumbbell with a 2-wide waist at `delta` −2 severs
|
|
349
|
+
into **two** regions under all three joins — 72.346873 round, 74.000000 chamfer,
|
|
350
|
+
72.000000 sharp — against the three regions and 97.258 the round join used to give.
|
|
351
|
+
- **Cause:** The waist recovery it described (`splitAtDuplicateEdges` in
|
|
352
|
+
`contour-offset.js`) matched a pair of duplicate, exactly-collinear edges, so it only
|
|
353
|
+
ever handled rings made entirely of straight lines; a round or chamfer join left an
|
|
354
|
+
arc or bevel chord at the pinch with no duplicate edge to cut. That function no longer
|
|
355
|
+
exists anywhere in `src/` — the whole boolean/heuristic cleanup path was replaced by
|
|
356
|
+
the winding resolver (`geometry/contour-winding.js`), which computes the
|
|
357
|
+
positive-winding region of the raw outline directly and severs a pinched waist as an
|
|
358
|
+
ordinary consequence of that, with no per-shape recovery and no join casing.
|
|
359
|
+
- **Fix:** Nothing to work around; the advice this entry used to give ("use
|
|
360
|
+
`corners: "sharp"` to split") is obsolete and was making callers change corner style
|
|
361
|
+
for no reason. If a region count still looks wrong after an inward offset, see
|
|
362
|
+
[shape2d-offset-winding-chain-incomplete](#shape2d-offset-winding-chain-incomplete)
|
|
363
|
+
and the parked cases in [KERNEL-CONTRACT.md "Offset: known
|
|
364
|
+
limitations"](KERNEL-CONTRACT.md#offset-known-limitations).
|
|
364
365
|
|
|
365
366
|
## shape2d-offset-kissing-ring-passes-validation
|
|
366
367
|
|
|
@@ -382,6 +383,29 @@ growing a shape past the hole's own width.
|
|
|
382
383
|
after an inward offset, see
|
|
383
384
|
[shape2d-offset-waist-not-severed-round-join](#shape2d-offset-waist-not-severed-round-join).
|
|
384
385
|
|
|
386
|
+
## shape2d-offset-winding-chain-incomplete
|
|
387
|
+
|
|
388
|
+
- **Symptom:** `contour-winding: could not chain offset boundary (incomplete intersection
|
|
389
|
+
set)` thrown from `Shape2D.offset` (or `offsetPolygon`) after a raw offset self-overlaps
|
|
390
|
+
at a narrow pinch.
|
|
391
|
+
- **Cause:** *(Known corpus fixed in partforge 0.60; ID retained permanently.)* The resolver
|
|
392
|
+
used to classify every boundary piece from one fixed midpoint probe. At a narrow cell that
|
|
393
|
+
probe could cross a nearby non-incident edge, read the wrong winding on both sides, and
|
|
394
|
+
drop a real continuation. Fully eroded round text counters were a separate upstream cause:
|
|
395
|
+
their raw offset could retain a negative pocket even under a correct Positive fill.
|
|
396
|
+
- **Fix:** Upgrade to partforge ≥ 0.60. The classifier now chooses among deterministic
|
|
397
|
+
interior samples by local boundary clearance and caps its probe distance accordingly.
|
|
398
|
+
Positive round offsets also decide counter collapse from the source hole's inradius before
|
|
399
|
+
generating a raw outline. On the committed 36,090-offset corpus
|
|
400
|
+
(`node scripts/offset-rates.mjs`), chain failures before the retry ladder are
|
|
401
|
+
1 round / 2 chamfer / 4 sharp and **zero remain after it**; the full glyph matrix,
|
|
402
|
+
including `"Scott"` through +3, has no throw or topology divergence.
|
|
403
|
+
|
|
404
|
+
The literal error remains intentionally loud if a new pathological arrangement defeats
|
|
405
|
+
every retry rung. If it appears on ≥0.60, report the profile, delta, and corner style so it
|
|
406
|
+
can become a deterministic fixture. Reducing `|delta|` or simplifying nearly coincident
|
|
407
|
+
features is a temporary workaround; changing corner style is not a reliable general fix.
|
|
408
|
+
|
|
385
409
|
## fillet-chamfer-radius-does-not-fit
|
|
386
410
|
|
|
387
411
|
- **Symptom:** `filletProfile: corner <i> at (<x>, <y>): r=<r> does not fit; max ≈ <m>` (or `chamferProfile: … dist=<d> does not fit; max ≈ <m>`) thrown from `Shape2D.fillet`/`.chamfer` or the free `filletProfile`/`chamferProfile` functions.
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -424,8 +424,12 @@ On `offset`: `round`, `sharp`, and `chamfer` all agree across both backends **at
|
|
|
424
424
|
|
|
425
425
|
The three tessellating readbacks — `toRegions()`, `simple()`, `regions()` — remain LOD-dependent: they hand back point rings sampled at the backend's own segment count, so the two backends' output differs in vertex count and by chord error, converging as LOD rises. Those three ops are the whole LOD-dependent surface; everything else, including `offset`, is backend-identical.
|
|
426
426
|
|
|
427
|
-
**Known limitations.** The native offset engine has verified defects on specific
|
|
428
|
-
|
|
427
|
+
**Known limitations.** The native offset engine has verified defects on specific input
|
|
428
|
+
shapes — on inward offsets that sever a shape, and on outward offsets of text — under **all
|
|
429
|
+
three corner styles at nearly the same rate**; see [Offset: known
|
|
430
|
+
limitations](#offset-known-limitations) below for the parked cases, their measured values,
|
|
431
|
+
the committed corpus and script that produce every rate quoted there, and the independent
|
|
432
|
+
construction the truths come from.
|
|
429
433
|
|
|
430
434
|
**Fillet after a boolean reaches STEP as real arcs.** Because booleans preserve curves
|
|
431
435
|
and `fillet` inserts true arc segments, `shape2d(a).union(b).fillet(2).extrude({h})`
|
|
@@ -435,44 +439,53 @@ facets at mesh LOD, as always, since its meshes have no curve representation.)
|
|
|
435
439
|
|
|
436
440
|
### Offset: known limitations
|
|
437
441
|
|
|
438
|
-
The native offset engine
|
|
439
|
-
|
|
440
|
-
`
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
442
|
+
The native offset engine preserves line, arc, and cubic contour IR through its normal cleanup
|
|
443
|
+
path. Tangled raw offsets are split at crossings, classified under the Positive winding rule,
|
|
444
|
+
and chained back into regions by `geometry/contour-winding.js`. Positive round dilation also
|
|
445
|
+
uses the source hole's inradius to prove when a counter has fully closed, and positive
|
|
446
|
+
dilation drops output components that contain no source material. These are source-domain
|
|
447
|
+
topology proofs, not output-area heuristics.
|
|
448
|
+
|
|
449
|
+
The reported text case is covered as correctness in
|
|
450
|
+
`test/offset-oracle-manifold.test.js`: the 6-glyph × 7-delta round matrix, including
|
|
451
|
+
`"Scott"` at +0.8/+1.5/+2/+3, matches Clipper2 region and hole counts exactly and stays
|
|
452
|
+
within the corpus area tolerance. `"Scott"` retains native arcs and cubics at every tested
|
|
453
|
+
delta.
|
|
454
|
+
|
|
455
|
+
**Measured failure surface.** The committed instrument is
|
|
456
|
+
`node scripts/offset-rates.mjs`, over 600 deterministic seeded shapes plus six glyph cases,
|
|
457
|
+
20 deltas, and three corner styles (36,090 attempts). In partforge 0.60 it reports:
|
|
458
|
+
|
|
459
|
+
- before the retry ladder: round 1/12,030 (0.008%), chamfer 2/12,030 (0.017%), sharp
|
|
460
|
+
4/12,030 (0.033%);
|
|
461
|
+
- after the retry ladder: zero chain-incomplete failures for all three styles;
|
|
462
|
+
- seven oracle-checked rescues, with median area error 0.0972%, worst 1.663%
|
|
463
|
+
(2.2373 mm²), zero region-count losses, and zero complete arc losses.
|
|
464
|
+
|
|
465
|
+
The ladder remains a numerical escape hatch: it perturbs delta by 1e-9, coarsens crossing
|
|
466
|
+
clustering, then tries polyline outlines. A future case that reaches a coarse clustering or
|
|
467
|
+
polyline rung can still lose fine topology or native arcs, so the order remains
|
|
468
|
+
fidelity-first and every newly found rescue must be checked against the independent
|
|
469
|
+
Minkowski oracle.
|
|
470
|
+
|
|
471
|
+
The currently parked limitations are narrower:
|
|
472
|
+
|
|
473
|
+
- **Round erosion with several holes reaching the eroded outer can keep too much material.**
|
|
474
|
+
The characterized 30×20 plate with three rectangular holes at −2 returns about 324.75
|
|
475
|
+
instead of the 258.18 oracle truth under round corners; chamfer and sharp are exact.
|
|
476
|
+
- **Fully eroded holes under sharp and chamfer can leave a remnant.** The source-inradius
|
|
477
|
+
gate is intentionally limited to round joins, whose structuring element is a Euclidean
|
|
478
|
+
disk. A 1×1 hole at +2 closes correctly under round, while the sharp/chamfer variants
|
|
479
|
+
remain parked rather than applying the wrong geometric criterion.
|
|
480
|
+
- **Erosion can emit sub-0.001 mm² rings.** Five exact seeded cases are pinned in
|
|
481
|
+
`test/offset-fuzz.test.js`. They are not automatically deleted: unlike positive
|
|
482
|
+
dilation, erosion has no source-membership invariant that distinguishes a false island
|
|
483
|
+
from a genuine surviving crumb.
|
|
484
|
+
|
|
485
|
+
The fuzz oracle sweep covers 150 seeded shapes × 6 deltas × 3 styles and currently reports
|
|
486
|
+
no region-count, hole-count, or area disagreements outside those explicit
|
|
487
|
+
characterizations. Do not widen tolerances or add an area-based sliver filter when a new
|
|
488
|
+
case appears; add its deterministic fixture and establish the source-domain truth first.
|
|
476
489
|
## The 2-D helper library
|
|
477
490
|
|
|
478
491
|
`partforge/geometry` ships pure-JS helpers of several kinds. The **contour builders**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.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",
|
|
@@ -81,7 +81,8 @@
|
|
|
81
81
|
"test": "vitest run",
|
|
82
82
|
"test:watch": "vitest",
|
|
83
83
|
"typecheck": "tsc -p tsconfig.json",
|
|
84
|
-
"check": "node scripts/check-app.mjs"
|
|
84
|
+
"check": "node scripts/check-app.mjs",
|
|
85
|
+
"offset-rates": "node scripts/offset-rates.mjs"
|
|
85
86
|
},
|
|
86
87
|
"dependencies": {
|
|
87
88
|
"dompurify": "^3.4.11",
|