partforge 0.116.0 → 0.117.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.
@@ -3650,16 +3650,24 @@ symptom first** — it maps error text → cause → fix. The invariants, one li
3650
3650
  is a part that exports: the old flat 480 turned a 0.75 mm rivet into 115,200 triangles
3651
3651
  and a body with a few hundred of them into an out-of-memory trap at export
3652
3652
  ([export-kernel-out-of-memory](ERROR-PATTERNS.md#export-kernel-out-of-memory)).
3653
- **Spheres are the exception on both tiers:** a sphere spends the segment count
3654
- squared (6,728 triangles at 116, whatever its radius), so `k.sphere` is sized by chord
3655
- tolerance 0.02 mm at preview, never fewer than 24 segments (288 triangles) and never
3656
- more than 116, so only spheres under about 60 mm radius get coarser and none get
3657
- finer. That is what keeps a body studded with a few hundred rivet spheres inside a
3658
- phone's memory ([preview-build-too-heavy-for-phones](ERROR-PATTERNS.md#preview-build-too-heavy-for-phones)).
3659
- Spheres are still the costliest way to add small detail: a domed rivet is 288
3660
- triangles where a short cylinder is 232 and a box is 12, and every one of them is a
3661
- boolean operand. Prefer instancing one union of a row over a chain of per-feature
3662
- booleans, and drop counts the print cannot show.
3653
+ **Doubly-curved surfaces are the exception on both tiers:** a sphere, a lathe's
3654
+ profile arcs (`torus`, `roundedCylinder`, a `revolve` of your own rounded `Shape2D`)
3655
+ and a `roundedBox`'s corners all spend the segment count squared (6,728 triangles
3656
+ for any sphere at 116; 27,000 for a tiny O-ring), so they are sized by chord
3657
+ tolerance — 0.02 mm at preview, never fewer than 24 segments per circle (a rivet
3658
+ sphere is 288 triangles, an O-ring ~6,000) and never more than the flat count, so
3659
+ only features under about 60 mm radius get coarser and none get finer. That is what
3660
+ keeps a body studded with a few hundred rivets inside a phone's memory
3661
+ ([preview-build-too-heavy-for-phones](ERROR-PATTERNS.md#preview-build-too-heavy-for-phones)).
3662
+ Two consequences to know: a lathe's sweep still runs at the flat count, so a small
3663
+ rounded cylinder is cheaper as `roundedCylinder` than as a revolve you densify by
3664
+ hand; and a tolerance-sized surface measures slightly smaller than the exact one
3665
+ (about 0.8% on a 3 mm rounded feature), so give a `verify` volume gate on small
3666
+ rounded parts that much slack. Spheres and rounded features are still the costliest
3667
+ way to add small detail — a domed rivet is 288 triangles where a short cylinder is
3668
+ 232 and a box is 12, and every one is a boolean operand — so prefer instancing one
3669
+ union of a row over a chain of per-feature booleans, and drop counts the print
3670
+ cannot show.
3663
3671
  - **Display placement is view-independent**; only `place(..., { purpose: "export" })` may
3664
3672
  depend on `view` ([view-dependent-display-place](ERROR-PATTERNS.md#view-dependent-display-place)).
3665
3673
  - **Keep geometry backend-agnostic** (kernel calls only); only STEP requires OCCT
@@ -833,7 +833,7 @@ between the Manifold preview and the OCCT STEP export.
833
833
 
834
834
  - **Symptom:** A part that previews on a desktop crashes, reloads, or shows a blank viewer on phones — iOS Safari's "This webpage was reloaded because a problem occurred", a viewer that never finishes its first build, or (in partforge-cloud) a `sandbox_timeout` from a phone user agent — with no error text at all, because the browser killed the page rather than the build throwing.
835
835
  - **Cause:** Peak WASM memory during the preview build exceeded what the phone allows a page (roughly 1–1.5 GB on iOS; a desktop tolerates several GB). The usual shape is hundreds of repeated small features unioned into one body: before partforge 0.116 every sphere was 6,728 triangles regardless of radius (the flat 116-segment preview count, squared), so a body with ~250 rivet spheres carried 1.7 M triangles of rivets into its booleans — measured 10 s and a 2.7 GB peak on a desktop for a 400k-triangle sub-part, against 4 s and 500 MB with the rivets as 12-triangle boxes. A boolean chain whose intermediates dwarf the result (a full-body skin intersected per groove) is the other shape.
836
- - **Fix:** Update partforge (0.116 sizes spheres by chord tolerance: that body builds in ~550 MB and 120k triangles unchanged). Then keep the preview build's peak down the same ways the export needs: build repeated detail as one union of instances rather than a chain of per-feature booleans, cut grooves from a thin local skin rather than the whole envelope, and drop feature counts the print cannot show ([Preview vs print quality](AUTHORING-PARTS.md#conventions--gotchas)). `partforge measure` reports triangle counts per sub-part; a single preview sub-part past ~200k triangles, or a whole view past ~400k, is the range where phones start to fail.
836
+ - **Fix:** Update partforge (0.116 sizes spheres by chord tolerance: that body builds in ~550 MB and 120k triangles unchanged; 0.117 extends the rule to lathe profile arcs — `torus`, `roundedCylinder`, revolved rounded profiles — and `roundedBox` corners, the other two shapes that spend the count squared). Then keep the preview build's peak down the same ways the export needs: build repeated detail as one union of instances rather than a chain of per-feature booleans, cut grooves from a thin local skin rather than the whole envelope, and drop feature counts the print cannot show ([Preview vs print quality](AUTHORING-PARTS.md#conventions--gotchas)). `partforge measure` reports triangle counts per sub-part; a single preview sub-part past ~200k triangles, or a whole view past ~400k, is the range where phones start to fail.
837
837
 
838
838
  # Hardware library
839
839
 
@@ -381,14 +381,20 @@ deflection; Manifold's per-circle segment rule in `geometry/circle-segs.js`, flo
381
381
  the preview's 116 segments so print is never coarser than preview and capped at 480), so
382
382
  a small feature costs the export exactly what its preview cost — the property that makes
383
383
  "if it previews, it exports" hold. Preview is a flat 116 on Manifold, a visual choice —
384
- with one exception: `sphere`, whose triangle count is quadratic in the segment count
385
- (8·(n/4)²), is sized by chord tolerance on BOTH tiers (`sphereSegs` in
384
+ except for the DOUBLY-CURVED family, whose triangle count is quadratic in the segment
385
+ count: `sphere` (8·(n/4)² triangles), a lathe's profile arcs (`revolve` of a
386
+ `Shape2D`, so `torus`, `roundedCylinder` and any hand-drawn rounded profile — every
387
+ arc sample becomes a full ring of the sweep) and a `roundedBox`'s corners (sphere
388
+ octants). Those are sized by chord tolerance on BOTH tiers (`doubleCurvatureSegs` in
386
389
  `geometry/circle-segs.js`: 0.02 mm at preview, floored at 24 segments and capped at
387
390
  the flat 116; 0.01 mm at print, floored at the preview count, capped at 480). At the
388
- flat count a 0.75 mm rivet sphere was 6,728 triangles, and a part carrying a few
389
- hundred of them was a 2.7 GB preview build that phones could not survive; at the
390
- floor it is 288. Circles in extrusions, revolves and outlines keep the flat count,
391
- which the mesh fillet's arc gate and the roundAll fast path are tuned to.
391
+ flat count a 0.75 mm rivet sphere was 6,728 triangles and an O-ring of that tube
392
+ radius 27,376, and a part carrying a few hundred rivets was a 2.7 GB preview build
393
+ that phones could not survive; at the floor they are 288 and ~6,000. A tolerance-sized
394
+ surface reads slightly smaller than the exact one (about 2π²/(3n²): 0.8% on a 3 mm
395
+ tube at 28 segments), which `measure` reports faithfully. A lathe's SWEEP, and
396
+ circles in extrusions, cylinders and outlines, keep the flat count, which the mesh
397
+ fillet's arc gate and the roundAll fast path are tuned to.
392
398
 
393
399
  ### Shading intent (toMesh normals and edges)
394
400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.116.0",
3
+ "version": "0.117.0",
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",
@@ -35,7 +35,8 @@
35
35
  // helix tube's station/ring counts (TUBE in manifold-backend.js) and mesh-fillet's
36
36
  // blend bands keep their own sizing; loft rings keep LOFT_SEGS.
37
37
  //
38
- // The one exception is `sphere`, which has its own rule at the bottom of this file.
38
+ // The exceptions are the doubly-curved surfaces sphere, a lathe's profile arcs, a
39
+ // rounded box's corners — which have their own rule at the bottom of this file.
39
40
  //
40
41
  // Pure, dependency-free: profile.js's samplers take a `(r) => n` function in place of
41
42
  // a count, and this is what the backend hands them.
@@ -66,44 +67,60 @@ export function circleSegs(r, quality) {
66
67
  return segsForSagitta(r, tol, SEGS.preview, cap);
67
68
  }
68
69
 
69
- // Spheres are the one primitive whose triangle count is QUADRATIC in the segment
70
- // count Manifold.sphere(r, n) subdivides an octahedron n/4 times per edge, 8·(n/4)²
71
- // triangles — so the per-circle budget above, spent on a sphere, is spent squared:
72
- // at the flat preview 116 every sphere is 6,728 triangles whatever its radius, and a
73
- // 0.75 mm rivet sphere carries a chord error of 0.0003 mm, a hundredth of a screen
74
- // pixel at any zoom. A part carrying ~250 such rivets (the steampunk-spider feedback,
75
- // "not loading on phones") unioned 1.7 M triangles of rivets into a 100k body: 10 s
76
- // and a 2.7 GB peak on a desktop, which iOS Safari's content process does not
77
- // survive — with the rivets at 12 triangles each the same body was 4 s and 500 MB.
70
+ // DOUBLE CURVATURE the family whose triangle count is QUADRATIC in the segment
71
+ // count, so the per-circle budget above, spent on one of them, is spent squared:
78
72
  //
79
- // So spheres are sized by chord tolerance on BOTH tiers, like print circles are:
73
+ // - `sphere`: Manifold.sphere(r, n) subdivides an octahedron n/4 times per edge,
74
+ // 8·(n/4)² triangles. At the flat preview 116 every sphere was 6,728 triangles
75
+ // whatever its radius; a 0.75 mm rivet carried a chord error of 0.0003 mm, a
76
+ // hundredth of a screen pixel at any zoom. A part with ~250 such rivets (the
77
+ // steampunk-spider feedback, "not loading on phones") unioned 1.7 M triangles of
78
+ // rivets into a 100k body: 10 s and a 2.7 GB peak on a desktop, which iOS Safari's
79
+ // content process does not survive — at 12 triangles each the same body was 4 s
80
+ // and 500 MB.
81
+ // - a lathe's PROFILE ARCS (`revolve` of a Shape2D — `torus`, `roundedCylinder`, a
82
+ // hand-drawn rounded profile): every arc sample becomes a full ring of the sweep,
83
+ // so an O-ring of 0.75 mm tube radius was 27,376 triangles. The sweep itself stays
84
+ // on the circle rule (its rim is what the fillet machinery reads).
85
+ // - a `roundedBox`'s corners: sphere octants, sampled per quarter arc AND per rim
86
+ // station, so a 3 mm rounded cube was 4,092 triangles (roundedBoxArcSamples in
87
+ // rounded-solids.js turns this count into that sample budget).
80
88
  //
81
- // - preview holds SPHERE_SAGITTA_TOL.preview (0.02 mm an absolute chord error is
82
- // what screen pixels measure, so one tolerance reads equally smooth at every radius
83
- // and every zoom; 0.02 mm is a fifth of a pixel at a typical 100 mm-part zoom),
84
- // floored at SPHERE_FLOOR segments (24 — ~15° facets, so a tiny ball never reads as
85
- // a polygon under close zoom, and 288 triangles instead of 6,728) and capped at the
86
- // old flat count (nothing gets FINER than before; a sphere of 60 mm radius and up
87
- // keeps exactly the density it always had).
89
+ // All three are sized by chord tolerance on BOTH tiers, like print circles are:
90
+ //
91
+ // - preview holds DOUBLE_CURVATURE_SAGITTA_TOL.preview (0.02 mm an absolute chord
92
+ // error is what screen pixels measure, so one tolerance reads equally smooth at
93
+ // every radius and every zoom; 0.02 mm is a fifth of a pixel at a typical
94
+ // 100 mm-part zoom), floored at DOUBLE_CURVATURE_FLOOR segments (24 ~15° facets,
95
+ // so a tiny ball never reads as a polygon under close zoom; a rivet sphere is 288
96
+ // triangles instead of 6,728) and capped at the old flat count (nothing gets FINER
97
+ // than before; a radius of 60 mm and up keeps exactly the density it always had).
88
98
  // - print holds the print tier's 0.01 mm, floored at the preview count for the same
89
99
  // radius (never coarser than the preview the user approved — the same property the
90
100
  // circle rule keeps) and capped at 480.
91
101
  //
92
- // Circles in extrusions, revolves, cylinders and 2-D outlines stay on the flat preview
93
- // count on purpose: their cost is linear in the count, and the mesh fillet's arc gate,
94
- // the roundAll prism fast path and the shading policies are all tuned to that density
95
- // a spike that made EVERY preview circle tolerance-based (0.05 mm, floor 24) turned
96
- // bore-rim fillets from revolve tools into planar sweeps and drew 26 feature lines
97
- // across a roundAll band that had none. A sphere has no sharp edges of its own and roundAll
98
- // sizes its own balls (roundAllSegs), so this rule touches nothing tuned to 116.
99
- export const SPHERE_SAGITTA_TOL = { preview: 0.02, print: SAGITTA_TOL.print }; // mm
100
- export const SPHERE_FLOOR = 24; // segments
102
+ // A tolerance-sized surface reads a little SMALLER than the exact one an inscribed
103
+ // polygon's area deficit is about 2π²/(3n²), so 0.8% on a 3 mm tube at 28 segments
104
+ // and `measure` reports what the mesh holds. Gates on small rounded features should
105
+ // carry that slack; print-scale features are unaffected at the cap.
106
+ //
107
+ // Circles in extrusions, cylinders, 2-D outlines and a lathe's SWEEP stay on the flat
108
+ // preview count on purpose: their cost is linear in the count, and the mesh fillet's
109
+ // arc gate, the roundAll prism fast path and the shading policies are all tuned to
110
+ // that density a spike that made EVERY preview circle tolerance-based (0.05 mm,
111
+ // floor 24) turned bore-rim fillets from revolve tools into planar sweeps and drew 26
112
+ // feature lines across a roundAll band that had none. None of the three surfaces
113
+ // above has a sharp edge of its own, and roundAll sizes its own balls (roundAllSegs),
114
+ // so this rule touches nothing tuned to 116.
115
+ export const DOUBLE_CURVATURE_SAGITTA_TOL = { preview: 0.02, print: SAGITTA_TOL.print }; // mm
116
+ export const DOUBLE_CURVATURE_FLOOR = 24; // segments
101
117
 
102
- // Segments for a sphere of radius `r` at `quality`: the fewest that keep the chord
103
- // sagitta under the tier's sphere tolerance, clamped as described above. An unknown
104
- // tier facets as preview; a degenerate radius takes the floor and never throws.
105
- export function sphereSegs(r, quality) {
106
- const tier = Object.hasOwn(SPHERE_SAGITTA_TOL, quality ?? "") ? quality : "preview";
107
- const floor = tier === "preview" ? SPHERE_FLOOR : sphereSegs(r, "preview");
108
- return segsForSagitta(r, SPHERE_SAGITTA_TOL[tier], floor, SEGS[tier]);
118
+ // Segments per full circle for a doubly-curved feature of radius `r` at `quality`: the
119
+ // fewest that keep the chord sagitta under the tier's tolerance, clamped as described
120
+ // above. An unknown tier facets as preview; a degenerate radius takes the floor and
121
+ // never throws.
122
+ export function doubleCurvatureSegs(r, quality) {
123
+ const tier = Object.hasOwn(DOUBLE_CURVATURE_SAGITTA_TOL, quality ?? "") ? quality : "preview";
124
+ const floor = tier === "preview" ? DOUBLE_CURVATURE_FLOOR : doubleCurvatureSegs(r, "preview");
125
+ return segsForSagitta(r, DOUBLE_CURVATURE_SAGITTA_TOL[tier], floor, SEGS[tier]);
109
126
  }
@@ -2,7 +2,7 @@ import { helixTube } from "./helix-tube.js";
2
2
  import { loftMesh } from "./loft.js";
3
3
  import { resolveLoftRings, loftRingsKey } from "./loft-rings.js";
4
4
  import { sweepMesh } from "./sweep.js";
5
- import { roundedBoxRings } from "./rounded-solids.js";
5
+ import { roundedBoxRings, roundedBoxArcSamples } from "./rounded-solids.js";
6
6
  import { tessellateContour, tessellateProfile } from "./profile.js";
7
7
  import { h } from "./solid-hash.js";
8
8
  import { ensureOutward, openEdgeCount } from "./mesh-repair.js";
@@ -19,7 +19,7 @@ import { creasedNormals } from "./creased-normals.js";
19
19
  import { loftShadingPolicy, SMOOTH, BLEND } from "./shading-policy.js";
20
20
  import { meshFillet, meshChamfer, UnsupportedEdgeError } from "./mesh-fillet.js";
21
21
  import { meshRoundAll, prismSection, roundAllSegs } from "./mesh-roundall.js";
22
- import { SEGS, circleSegs, sphereSegs } from "./circle-segs.js";
22
+ import { SEGS, circleSegs, doubleCurvatureSegs } from "./circle-segs.js";
23
23
  import { checkBooleanResult } from "./boolean-gate.js";
24
24
  import { KernelCapabilityError } from "./errors.js";
25
25
  import { heightfieldMesh, hashGridData } from "./heightfield.js";
@@ -51,6 +51,9 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
51
51
  // Per-radius count for every site that facets a circle it knows the radius of; the
52
52
  // samplers behind prism/extrude/Shape2D take it as a function (profile.js).
53
53
  const segsAt = (r) => circleSegs(r, quality);
54
+ // The doubly-curved family's own per-radius count (sphere, a lathe's profile arcs,
55
+ // a rounded box's corners) — circle-segs.js explains why they are sized apart.
56
+ const dcAt = (r) => doubleCurvatureSegs(r, quality);
54
57
 
55
58
  // Manifold/CrossSection are WASM objects with no garbage collection — every
56
59
  // primitive and boolean op allocates a new one. Track them all and free them
@@ -187,6 +190,14 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
187
190
  const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, segsAt), "EvenOdd"));
188
191
  return { value: cs, pin: cs, dispose: () => cs.delete?.() };
189
192
  });
193
+ // The same shape as a LATHE profile: its arcs sampled at the double-curvature count
194
+ // (every sample becomes a full ring of the sweep — the quadratic cost circle-segs.js
195
+ // describes), memoized apart from the extrude-facing tessellation above under its
196
+ // own key. Keyed on the tier, not a count: the count varies per arc radius.
197
+ const csForLathe = (shape) => cache.lookup(h("cs2d-lathe", shape._hash, quality), () => {
198
+ const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, dcAt), "EvenOdd"));
199
+ return { value: cs, pin: cs, dispose: () => cs.delete?.() };
200
+ });
190
201
 
191
202
  // Copy the mesh out into JS-owned arrays (so it survives cleanup) and free the
192
203
  // transient mesh handle.
@@ -653,16 +664,17 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
653
664
  // spec). Reuses loftMesh's stitch/cap/winding machinery. Atomic cache
654
665
  // node hashed from its own args, like boredCylinder.
655
666
  roundedBox: ({ size, center, round }) => cached(
656
- h("roundedBox", size, center, round.side, round.top, round.bottom, segsAt(Math.max(round.side, round.top, round.bottom))),
667
+ h("roundedBox", size, center, round.side, round.top, round.bottom, roundedBoxArcSamples(segsAt(Math.max(round.side, round.top, round.bottom)), dcAt(Math.max(round.side, round.top, round.bottom)))),
657
668
  () => {
658
669
  // One count serves every corner and rim arc, so it is sized for the largest.
659
- const solid = T(loftMesh(wasm, roundedBoxRings(size, round, segsAt(Math.max(round.side, round.top, round.bottom)))));
670
+ const rMax = Math.max(round.side, round.top, round.bottom);
671
+ const solid = T(loftMesh(wasm, roundedBoxRings(size, round, segsAt(rMax), roundedBoxArcSamples(segsAt(rMax), dcAt(rMax)))));
660
672
  return center ? T(solid.translate([0, 0, -size[2] / 2])) : solid;
661
673
  }),
662
- // Sized by its own per-radius rule (circle-segs.js: a sphere spends the per-circle
674
+ // Sized by the double-curvature rule (circle-segs.js: a sphere spends the per-circle
663
675
  // count squared), on both tiers — the hash carries the count, so a sphere built at
664
676
  // one tier never masquerades as the other's in the cache.
665
- sphere: (r) => wrap(T(Manifold.sphere(r, sphereSegs(r, quality))), h("sphere", r, sphereSegs(r, quality))),
677
+ sphere: (r) => wrap(T(Manifold.sphere(r, dcAt(r))), h("sphere", r, dcAt(r))),
666
678
  box: (min, max) => {
667
679
  const cube = T(Manifold.cube([max[0] - min[0], max[1] - min[1], max[2] - min[2]]));
668
680
  return wrap(T(cube.translate(min)), h("box", min, max));
@@ -782,8 +794,8 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
782
794
  if (pts && pts._shape2d) {
783
795
  // Keyed on the override, not the resolved density, so a cache hit never
784
796
  // materializes the CrossSection just to measure its bounds.
785
- return cached(h("revolve", pts._hash, degrees, segsOverride ?? null), () => {
786
- const cs = csFor(pts);
797
+ return cached(h("revolve", pts._hash, degrees, segsOverride ?? null, quality), () => {
798
+ const cs = csForLathe(pts); // profile arcs at the double-curvature count; the sweep below stays on the circle rule
787
799
  const b = cs.bounds();
788
800
  return T(cs.revolve(densityFor(Math.max(Math.abs(b.min[0]), Math.abs(b.max[0]))), degrees));
789
801
  });
@@ -34,13 +34,25 @@ export function roundedRectRing(hw, hd, rc, A) {
34
34
  return pts;
35
35
  }
36
36
 
37
+ // Quarter-arc sample count for a rounded box's corners (and z-stations per rim
38
+ // zone): the corners are sphere octants, so they take the double-curvature count
39
+ // for the corner radius, a quarter of it per 90° arc — the facet angle the sphere
40
+ // floor itself has (24 → 6 → 15°). Capped at what the flat circle count used to
41
+ // give (ceil(circle/8): 15 at preview, up to 49 at a metre-scale print), so no
42
+ // rounded box gets finer than before and a large one keeps exactly its old
43
+ // density. Floor 2: the builder's own minimum for a valid ring.
44
+ export function roundedBoxArcSamples(circleSegs, doubleCurvatureSegs) {
45
+ return Math.max(2, Math.min(Math.ceil(circleSegs / 8), Math.ceil(doubleCurvatureSegs / 4)));
46
+ }
47
+
37
48
  // Ring stack for the Manifold roundedBox: ascending-z loft ring specs
38
49
  // [{ polygon, z }]. A (arc samples per corner AND z-stations per rim zone) is
39
- // derived from the kernel's segs so the z-sampling matches the in-plane LOD.
40
- // Consecutive duplicate stations (top + bottom = h) are deduped so the loft
41
- // never sees a zero-height band.
42
- export function roundedBoxRings([w, d, h], { side, top, bottom }, segs) {
43
- const A = Math.max(2, Math.ceil(segs / 8));
50
+ // derived from the kernel's segs or passed outright as `arcSamples` (the
51
+ // backend hands in roundedBoxArcSamples) so the z-sampling matches the
52
+ // in-plane LOD. Consecutive duplicate stations (top + bottom = h) are deduped
53
+ // so the loft never sees a zero-height band.
54
+ export function roundedBoxRings([w, d, h], { side, top, bottom }, segs, arcSamples = Math.max(2, Math.ceil(segs / 8))) {
55
+ const A = arcSamples;
44
56
  const st = [];
45
57
  const push = (z, delta) => {
46
58
  const last = st[st.length - 1];