partforge 0.58.1 → 0.59.0
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/AUTHORING-PARTS.md +5 -3
- package/docs/ERROR-PATTERNS.md +76 -0
- package/docs/KERNEL-CONTRACT.md +114 -13
- package/package.json +1 -1
- package/src/framework/geometry/contour-offset.js +542 -0
- package/src/framework/geometry/contour-ops.js +2 -2
- package/src/framework/geometry/kernel.js +19 -16
- package/src/framework/geometry/manifold-backend.js +9 -48
- package/src/framework/geometry/occt-backend.js +12 -94
- package/src/framework/geometry/paper-bridge.js +29 -0
- package/src/framework/geometry/shape2d-regions.js +13 -104
- package/src/framework/geometry/shape2d.js +16 -12
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -294,8 +294,10 @@ transposition mistake (swap two same-typed numbers, get a valid *wrong* solid).
|
|
|
294
294
|
Single-argument chaining ops (`translate`, `rotate*`, `cut`, `mirror`, `scale`, …)
|
|
295
295
|
already take one argument and are unaffected. Legacy positional calls (e.g.
|
|
296
296
|
`k.cylinder(rBottom, rTop, h)`) still work — they're accepted silently until a
|
|
297
|
-
future contract
|
|
298
|
-
|
|
297
|
+
future breaking contract version removes them (contract v2, partforge 0.59, did
|
|
298
|
+
not — it only changed `offset` semantics) — but are not shown here; see
|
|
299
|
+
`docs/KERNEL-CONTRACT.md` "Calling convention" for the full canonical/legacy table
|
|
300
|
+
and the detection rule.
|
|
299
301
|
|
|
300
302
|
**Kernel — make solids:**
|
|
301
303
|
|
|
@@ -1119,7 +1121,7 @@ const wall = k.shape2d(outer).offset(-2, { corners: "sharp" }); // inset, mite
|
|
|
1119
1121
|
|
|
1120
1122
|
(This achieves the same geometry as building the profiles separately and using `k.extrude({ profile: { outer, holes }, h })`, but the Shape2D path is more idiomatic for complex 2-D operations.)
|
|
1121
1123
|
|
|
1122
|
-
`Shape2D.offset(delta, {corners})` grows (`delta>0`) or insets (`delta<0`) a shape
|
|
1124
|
+
`Shape2D.offset(delta, { corners: "round" | "chamfer" | "sharp" })` grows (`delta>0`) or insets (`delta<0`) a shape. It runs backend-independently on the shared contour engine — lines and arcs offset exactly (arcs stay arcs), so results are backend-identical by construction, like every other `Shape2D` op; it throws if the offset collapses the shape. A region with holes offsets material-wise: the outer grows/shrinks by `delta`, holes by `-delta`, so a positive `delta` always adds material. (For `derive()`/main-thread clearance math on plain point lists, use the pure `offsetPolygon` helper instead.)
|
|
1123
1125
|
|
|
1124
1126
|
## Editing profiles
|
|
1125
1127
|
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -306,6 +306,82 @@ Variant literals under this entry: `offsetPolygon: delta must be a finite number
|
|
|
306
306
|
inset. Realistic clearances (fractions of a mm) and wall insets up to the
|
|
307
307
|
narrowest feature never trip this.
|
|
308
308
|
|
|
309
|
+
## shape2d-offset-partial-reflection-residual
|
|
310
|
+
|
|
311
|
+
- **Symptom:** A pocket that should close doesn't — an outward `Shape2D.offset` on a
|
|
312
|
+
region with a hole leaves a leftover hole ring behind (`.holes.length` stays > 0,
|
|
313
|
+
`.area()` under-reports) even though the hole's narrowest span is smaller than
|
|
314
|
+
`delta` and the pocket should have closed completely.
|
|
315
|
+
- **Cause:** A raw offset ring can be locally valid — correctly wound (CW for a
|
|
316
|
+
hole), no self-intersections — while still lying inside the region it should have
|
|
317
|
+
been swept away by. That's a *global* defect only a whole-shape containment check
|
|
318
|
+
can see, and `offset`'s validator (`contour-offset.js`'s `validateRawOffset`)
|
|
319
|
+
only checks local validity. Known limitation — see
|
|
320
|
+
[KERNEL-CONTRACT.md "Offset: known limitations"](KERNEL-CONTRACT.md#offset-known-limitations).
|
|
321
|
+
- **Fix:** No general fix yet. Verify `.holes.length` after an offset meant to
|
|
322
|
+
close a pocket rather than assuming it did; work around by offsetting in stages
|
|
323
|
+
or padding the pocket before offsetting.
|
|
324
|
+
|
|
325
|
+
Searchable phrasings of the same misbehavior: hole doesn't disappear after offset;
|
|
326
|
+
pocket not closed by offset; residual hole ring; `.holes.length` still 1 after
|
|
327
|
+
growing a shape past the hole's own width.
|
|
328
|
+
|
|
329
|
+
## shape2d-offset-reflex-cluster-too-much-material
|
|
330
|
+
|
|
331
|
+
- **Symptom:** An inward `Shape2D.offset` (`corners: "chamfer"` or `"sharp"`) on a
|
|
332
|
+
shape with several reflex/concave corners close together leaves several times too
|
|
333
|
+
much material — `.area()` comes back far larger than the eroded shape should be,
|
|
334
|
+
often split across more regions than expected. No error is thrown.
|
|
335
|
+
- **Cause:** When the offset's raw rings self-intersect, `offset` hands them to
|
|
336
|
+
paper.js (`paper-bridge.js`'s `resolveSelfRegions`) to untangle. Clustered reflex
|
|
337
|
+
corners produce several overlapping self-intersection loops at once, and paper.js
|
|
338
|
+
resolves that tangle into the wrong set of sub-regions — keeping loops that should
|
|
339
|
+
have cancelled. Verified: a 9-gon with clustered reflex corners at `delta` −2.79
|
|
340
|
+
chamfer resolves to ~7.71 where the true eroded area is ~2.76. Known limitation —
|
|
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.
|
|
346
|
+
|
|
347
|
+
## shape2d-offset-waist-not-severed-round-join
|
|
348
|
+
|
|
349
|
+
- **Symptom:** An inward `Shape2D.offset` past the width of a narrow waist does not
|
|
350
|
+
split the shape in two — the result stays connected (or comes back with a spurious
|
|
351
|
+
extra blob where the waist was) and `.area()` over-reports — but the *same* offset
|
|
352
|
+
with `corners: "sharp"` splits correctly. Verified: a 30×10 dumbbell with a 2-wide
|
|
353
|
+
waist at `delta` −2 gives 72.000 in two regions under `sharp` and 97.258 in three
|
|
354
|
+
regions under `round` (74.000 vs 96.000 under `chamfer`).
|
|
355
|
+
- **Cause:** The recovery that severs a waist pinched shut by an offset
|
|
356
|
+
(`splitAtDuplicateEdges` in `contour-offset.js`) works by finding the pair of
|
|
357
|
+
duplicate, exactly-collinear edges the two sides of the pinch land on — so it only
|
|
358
|
+
handles rings made entirely of straight lines. A round or chamfer join inserts an
|
|
359
|
+
arc or a bevel chord at the waist, so there is no duplicate straight edge left to
|
|
360
|
+
cut and the pinched ring survives as one over-solid blob.
|
|
361
|
+
- **Fix:** Use `corners: "sharp"` for an inward offset that is meant to split a
|
|
362
|
+
shape; or offset the pieces separately and union them. Check the result's region
|
|
363
|
+
count (`.toRegions().length`) rather than assuming the split happened.
|
|
364
|
+
|
|
365
|
+
## shape2d-offset-kissing-ring-passes-validation
|
|
366
|
+
|
|
367
|
+
- **Symptom:** *(Fixed in partforge 0.59 — kept because IDs are permanent.)* Two
|
|
368
|
+
rings produced by the same `Shape2D.offset` call — two eroding holes that grew into
|
|
369
|
+
each other, or a hole that eroded out through its own outer — used to come back
|
|
370
|
+
still separate and overlapping, and extruded to *solid* material inside the pocket
|
|
371
|
+
or a tab of material hanging off the outline.
|
|
372
|
+
- **Cause:** The offset validator's ring-crossing test only looked for transversal
|
|
373
|
+
crossings, so two rings that interfere along a shared collinear edge (which is what
|
|
374
|
+
a sharp join produces) registered as fine; and its hole-containment test sampled a
|
|
375
|
+
single point of the hole ring, which stays inside even when most of the ring has
|
|
376
|
+
escaped. The cleanup stage then self-united everything under one even-odd compound,
|
|
377
|
+
where a doubly-covered region cancels back to solid instead of merging.
|
|
378
|
+
- **Fix:** Upgrade to partforge ≥ 0.59, where `ringsCross` also tests collinear
|
|
379
|
+
overlap, hole containment tests the whole ring, and cleanup unites the outers and
|
|
380
|
+
*subtracts* the united hole rings. Nearby holes now merge into one hole and a
|
|
381
|
+
near-edge hole is clipped by its eroded outer. If a ring count still looks wrong
|
|
382
|
+
after an inward offset, see
|
|
383
|
+
[shape2d-offset-waist-not-severed-round-join](#shape2d-offset-waist-not-severed-round-join).
|
|
384
|
+
|
|
309
385
|
## fillet-chamfer-radius-does-not-fit
|
|
310
386
|
|
|
311
387
|
- **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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# The partforge kernel contract
|
|
2
2
|
|
|
3
|
-
**Contract version:
|
|
3
|
+
**Contract version: 2** (introduced in partforge 0.59) — mirrored by `CONTRACT_VERSION`
|
|
4
4
|
in `src/framework/geometry/kernel.js` and asserted by `test/kernel-contract.test.js`;
|
|
5
5
|
see [Versioning](#versioning) for what may change under which version bump.
|
|
6
6
|
|
|
@@ -116,13 +116,15 @@ is options (one plain object).
|
|
|
116
116
|
|
|
117
117
|
Options form is canonical — the form this document, `AUTHORING-PARTS.md`, and every
|
|
118
118
|
in-repo part teach and use. Legacy positional forms remain accepted (silently — no
|
|
119
|
-
runtime warning) until contract
|
|
120
|
-
accept both, and this repo's `finishKernel()`/`addSugar()`
|
|
121
|
-
for free.
|
|
119
|
+
runtime warning) until a future breaking contract version removes them; a conforming
|
|
120
|
+
implementation must accept both, and this repo's `finishKernel()`/`addSugar()`
|
|
121
|
+
provide the normalization for free. (Contract v2, partforge 0.59, did **not** remove
|
|
122
|
+
them — that bump was for `offset` semantics, see [Versioning](#versioning); legacy
|
|
123
|
+
positional removal is still pending a version of its own.)
|
|
122
124
|
|
|
123
125
|
### Kernel factory ops (options-canonical; legacy positional accepted)
|
|
124
126
|
|
|
125
|
-
| Op | Canonical options form | Legacy positional (
|
|
127
|
+
| Op | Canonical options form | Legacy positional (pending removal) |
|
|
126
128
|
|---|---|---|
|
|
127
129
|
| `cylinder` | `{r\|d, h, center?}` straight · `{r1, r2, h, center?}` or `{d1, d2, h, center?}` cone | `(rBottom, rTop, h, {center?})` |
|
|
128
130
|
| `sphere` | `{r\|d}` — `sphere(5)` stays valid, undeprecated | `(r)` |
|
|
@@ -352,6 +354,8 @@ against that IR, and both backends instantiate it. Booleans run through **paper.
|
|
|
352
354
|
`fillet`/`chamfer`/`simplify`, and `area`/`boundingBox`/`corners`/`contains` are
|
|
353
355
|
**backend-identical**, not merely parity-tolerant. `area()` and `boundingBox()` are
|
|
354
356
|
curve-exact (they integrate the real curves; they do not measure a tessellation).
|
|
357
|
+
`offset` runs on this same shared engine (`geometry/contour-offset.js`) — see below —
|
|
358
|
+
so it is backend-identical too, like everything else in this list.
|
|
355
359
|
|
|
356
360
|
**Lazy materialization.** Backend geometry is built only where it is unavoidable.
|
|
357
361
|
Three readbacks tessellate to point rings at the backend's own LOD (Manifold 116
|
|
@@ -365,16 +369,28 @@ contours — arcs and cubics become true B-rep edges). A `Shape2D` may be passed
|
|
|
365
369
|
directly as the `profile` to `extrude`/`revolve`, holes included. `toContours()` is
|
|
366
370
|
the one readback that tessellates nothing.
|
|
367
371
|
|
|
368
|
-
**Offset
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
**Offset runs on the contour IR too.** `Shape2D.offset(delta, { corners })` runs
|
|
373
|
+
backend-independently on the contour IR — no backend `CrossSection` or `Drawing` is
|
|
374
|
+
ever involved. Lines and arcs offset exactly (arcs stay arcs); cubics are
|
|
375
|
+
approximated to ≤ 1e-3 mm deviation. `corners: "round"` inserts exact arc joins,
|
|
376
|
+
`"chamfer"` a true 45°-bisecting bevel chord at every corner angle, `"sharp"` miters
|
|
377
|
+
with limit 2 (falling back to the bevel chord past the limit). Self-intersecting raw
|
|
378
|
+
results are resolved through the shared planar boolean engine (paper.js), which may
|
|
379
|
+
return arcs as cubic approximations — identical to boolean-op output. `segs` is
|
|
380
|
+
accepted and ignored (there is no backend LOD to tune). Both backends produce
|
|
381
|
+
identical offset geometry by construction, like every other Shape2D op.
|
|
382
|
+
|
|
383
|
+
A region with holes offsets **material-wise**: the outer boundary moves by `delta`,
|
|
384
|
+
each hole by `-delta`, so a positive `delta` always adds material (the outer grows,
|
|
385
|
+
holes shrink) and a negative one always removes it (the outer shrinks, holes grow) —
|
|
386
|
+
never the reverse for either. This one shared implementation is what guarantees it;
|
|
387
|
+
a route that offsets a single fused `outer.cut(hole)` drawing with one call gets it
|
|
388
|
+
backwards for the holes (see the migration note below).
|
|
373
389
|
|
|
374
390
|
| Op | Contract |
|
|
375
391
|
|---|---|
|
|
376
392
|
| `union(other)` / `cut(other)` / `cutAll(others[])` / `intersect(other)` | 2-D boolean ops; `other` may be a `Shape2D` or a raw profile (lifted via `shape2d` first). Curve-exact and backend-identical (paper.js). |
|
|
377
|
-
| `offset(delta, {corners?, segs?})` | Grows (`delta>0`) or insets (`delta<0`) by `delta` mm; `corners` = `round` (default) / `chamfer` / `sharp`.
|
|
393
|
+
| `offset(delta, {corners?, segs?})` | Grows (`delta>0`) or insets (`delta<0`) by `delta` mm; `corners` = `round` (default) / `chamfer` / `sharp`. Runs backend-independently on the contour IR — lines/arcs offset exactly, cubics approximate to ≤ 1e-3 mm; `chamfer` is a true 45°-bisecting bevel at every corner angle, `sharp` miters with limit 2. Backend-identical by construction, like every other Shape2D op. Holes offset material-wise (`-delta` where the outer gets `delta`). `segs` is accepted and ignored. Empty in → empty out (short-circuits before the engine). Throws if the offset collapses the shape. |
|
|
378
394
|
| `area()` | Net area (Σ\|outers\| − Σ\|holes\|), mm². Curve-exact. |
|
|
379
395
|
| `boundingBox()` | `{min, max}` — axis-aligned 2-D bounds, curve-exact (no `center`/`size`, unlike `Solid.boundingBox`). |
|
|
380
396
|
| `toRegions()` | Materialize into `{outer, holes}[]` point-ring region arrays (`assembleRegions`), tessellating curves at the backend's LOD; a boolean result may be several disjoint regions. |
|
|
@@ -404,9 +420,12 @@ pinned, Manifold silently built an empty solid where OCCT threw — behavior no
|
|
|
404
420
|
could rely on portably, so defining it follows the reference backend and is not a
|
|
405
421
|
contract break.)
|
|
406
422
|
|
|
407
|
-
On `offset`: `round`, `sharp`, and `chamfer` all agree across both backends **
|
|
423
|
+
On `offset`: `round`, `sharp`, and `chamfer` all agree across both backends **at every corner angle, convex or reflex** — a 10×10 square offset +1 gives 142.0 on both, a pentagon 298.920 on both, and an equilateral triangle's chamfer agrees to float precision on both, with no acute-corner carve-out. This follows from `offset` being one native implementation rather than a call into either backend's own 2-D engine — there is no Clipper2-vs-OCCT split left to diverge.
|
|
408
424
|
|
|
409
|
-
|
|
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
|
+
|
|
427
|
+
**Known limitations.** The native offset engine has verified defects on specific
|
|
428
|
+
input shapes; see [Offset: known limitations](#offset-known-limitations) below.
|
|
410
429
|
|
|
411
430
|
**Fillet after a boolean reaches STEP as real arcs.** Because booleans preserve curves
|
|
412
431
|
and `fillet` inserts true arc segments, `shape2d(a).union(b).fillet(2).extrude({h})`
|
|
@@ -414,6 +433,46 @@ exports a filleted profile as `CIRCLE` B-rep entities on OCCT — the corner op
|
|
|
414
433
|
have to run before the boolean, and no facet fan is baked in along the way. (Manifold
|
|
415
434
|
facets at mesh LOD, as always, since its meshes have no curve representation.)
|
|
416
435
|
|
|
436
|
+
### Offset: known limitations
|
|
437
|
+
|
|
438
|
+
The native offset engine (`geometry/contour-offset.js`) is correct on the honest-agreement
|
|
439
|
+
corpus (`test/contour-offset.test.js`, `test/offset-oracle-manifold.test.js`,
|
|
440
|
+
`test/offset-oracle-occt.test.js`), but two cases are verified defects, pinned as
|
|
441
|
+
characterization tests in the "known divergences (parked)" block of
|
|
442
|
+
`test/offset-oracle-manifold.test.js` rather than silently tolerated:
|
|
443
|
+
|
|
444
|
+
- **A pocket that should fully close doesn't.** An outward offset large enough that a
|
|
445
|
+
hole's max inscribed circle is smaller than `delta` should erase the hole entirely; the
|
|
446
|
+
engine instead leaves a residual ring. Verified: a 30×20 plate with a 5-wide-arm L-shaped
|
|
447
|
+
pocket, offset +3, should reach 0 holes / area 928.274 — actual leaves a residual hole at
|
|
448
|
+
~921.21. Root cause: a raw offset ring can be locally valid (correctly wound, no
|
|
449
|
+
self-intersections) while still lying inside the region it should have been swept away
|
|
450
|
+
by — only a *global* containment check catches this, and none currently runs.
|
|
451
|
+
- **Clustered reflex corners degrade accuracy.** A chamfer offset over several reflex
|
|
452
|
+
corners sitting close together can resolve to several times too much surviving area.
|
|
453
|
+
Verified: a 9-gon with clustered reflex corners, chamfer offset delta −2.79; true area
|
|
454
|
+
2.76 (a thin sliver), native resolves ~7.71. Root cause: `resolveSelfRegions`
|
|
455
|
+
(`paper-bridge.js`) doesn't fully untangle the self-intersections this corner geometry
|
|
456
|
+
produces.
|
|
457
|
+
|
|
458
|
+
Neither is silent in the sense of going unnoticed by tests — each has a pinned
|
|
459
|
+
characterization test that fails loudly if the defect gets worse, and is meant to be
|
|
460
|
+
deleted and promoted to the main corpus the day it's fixed. They matter to a part author
|
|
461
|
+
today: don't rely on `offset` to fully close a pocket, or to hold tight tolerance through a
|
|
462
|
+
reflex-corner cluster — verify the result (`holes`/`area`) rather than assuming it.
|
|
463
|
+
|
|
464
|
+
Two further cases were on this list and are now **fixed**, asserted as correctness in the
|
|
465
|
+
same block: two eroding holes that grow into each other merge into one hole (40×20 plate,
|
|
466
|
+
two 6×8 holes 3 mm apart, delta −2 sharp → area 348), and a hole that erodes through its
|
|
467
|
+
outer boundary is clipped by it (40×20 plate, 10×10 hole 2 mm from the edge, delta −2 sharp
|
|
468
|
+
→ area 408, hole absorbed into the outline). Both used to produce topologically invalid
|
|
469
|
+
output — overlapping rings, or a hole ring outside its own outer — which even-odd fill then
|
|
470
|
+
turned back into *solid* material on extrude (360 and 436 respectively). The fix was two
|
|
471
|
+
missing checks in the fast-path validator (ring-crossing now also detects collinear overlap;
|
|
472
|
+
hole containment now tests the whole hole ring, not one point of it) plus a cleanup stage
|
|
473
|
+
that unites the outer rings and *subtracts* the united hole rings instead of self-uniting
|
|
474
|
+
everything under one even-odd compound.
|
|
475
|
+
|
|
417
476
|
## The 2-D helper library
|
|
418
477
|
|
|
419
478
|
`partforge/geometry` ships pure-JS helpers of several kinds. The **contour builders**
|
|
@@ -562,6 +621,48 @@ in `kernel.js` define the current surface; only breaking changes bump the versio
|
|
|
562
621
|
`cut` per CadQuery/replicad rather than OpenSCAD's `difference`), so LLM priors
|
|
563
622
|
transfer. Renames are breaking changes with no offsetting benefit — don't.
|
|
564
623
|
|
|
624
|
+
**v1 → v2** (partforge 0.59): `Shape2D.offset` moved off the two per-backend 2-D
|
|
625
|
+
engines (Clipper2 via `CrossSection` on Manifold, replicad's `Drawing.offset` on
|
|
626
|
+
OCCT) onto the single native contour-offset engine described above. Semantics
|
|
627
|
+
changed, not just implementation: `offset` is now backend-identical by construction
|
|
628
|
+
at every corner angle (the old acute-corner `chamfer` divergence and the LOD-faceted
|
|
629
|
+
Manifold result are both gone), and `segs` is now accepted-but-ignored rather than
|
|
630
|
+
tuning Manifold's tessellation. Holes offset material-wise (`-delta` where the outer
|
|
631
|
+
gets `delta`) on both backends — the deleted OCCT production route got this backwards
|
|
632
|
+
by fusing `outer.cut(hole)` into one `Drawing` and offsetting it with a single call,
|
|
633
|
+
so holes grew under a positive `delta` instead of shrinking; no test caught it because
|
|
634
|
+
there was no holed-offset test before this contract version. Parts that relied on the
|
|
635
|
+
old holed-offset direction (if any existed) need the sign of their workaround removed.
|
|
636
|
+
|
|
637
|
+
**`sharp` and `chamfer` change shape on acute corners — check these when migrating.** This
|
|
638
|
+
is a real geometric change, not a precision polish, and it is the one thing v1 parts should
|
|
639
|
+
be re-measured for. Once a convex corner gets tighter than 90° the two old backends did not
|
|
640
|
+
agree with each other, and neither agreed with this repo's own `offsetPolygon`; v1's claim
|
|
641
|
+
that "`round` and `sharp` are exact across backends at every angle" was simply false.
|
|
642
|
+
Measured on an 11-point star (alternating radii 10 and 4) at `delta` +2:
|
|
643
|
+
|
|
644
|
+
| corners | native (v2) | Clipper2 (v1 Manifold) | OCCT (v1 B-rep) | `offsetPolygon` |
|
|
645
|
+
| --- | --- | --- | --- | --- |
|
|
646
|
+
| `round` | 295.933 | 295.933 | 295.933 | 295.933 |
|
|
647
|
+
| `sharp` | 282.158 | 300.671 | 326.534 | 282.158 |
|
|
648
|
+
| `chamfer` | 278.389 | 288.138 | 278.389 | 278.389 |
|
|
649
|
+
|
|
650
|
+
The spread is **miter-limit policy**, not accuracy: OCCT miters unbounded, so an acute spike
|
|
651
|
+
shoots arbitrarily far past the corner; Clipper2 squares the corner off past its own limit
|
|
652
|
+
rather than bevelling it. Native applies miter limit 2 and falls back to a plain bevel — the
|
|
653
|
+
same rule `offsetPolygon` (`geometry/polygon.js`) has always used, so `offset` and
|
|
654
|
+
`offsetPolygon` now agree to the digit where previously *neither* backend matched the pure-JS
|
|
655
|
+
helper sitting next to it. `chamfer` additionally lands exactly on OCCT's `bevel` join; only
|
|
656
|
+
Clipper2 differed there, because it had no bevel join and approximated one with two chords.
|
|
657
|
+
|
|
658
|
+
Practical rule: divergence from v1 is confined to `sharp` and `chamfer` on **outward**
|
|
659
|
+
offsets of shapes with sub-90° convex corners (star points, V-notches, triangles, spiky text
|
|
660
|
+
serifs), and native is always the *smaller*, never the over-solid, result — a clearance
|
|
661
|
+
offset that fit in v1 still fits. `round` is unchanged at every angle, inward offsets are
|
|
662
|
+
unchanged, and shapes whose corners are all ≥90° (rectangles, hexagons, rounded-rects, slots)
|
|
663
|
+
are unchanged. A random-polygon sweep put the >1%-divergent share at 1.6% overall, every one
|
|
664
|
+
of them `sharp` or `chamfer` at positive delta.
|
|
665
|
+
|
|
565
666
|
## Why not an existing CAD language
|
|
566
667
|
|
|
567
668
|
Considered and rejected as the part format (2026-07; revisit if the landscape shifts):
|
package/package.json
CHANGED