partforge 0.62.1 → 0.63.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.
@@ -2093,6 +2093,29 @@ print faceted — or the reverse — set the hint rather than changing facet cou
2093
2093
  > (Manifold-only topology); `render` works on both. Filleted parts now measure on
2094
2094
  > Manifold with full topology.
2095
2095
 
2096
+ ### `roundAll(r)` — round everything at once
2097
+
2098
+ `s.roundAll(2)` (or `s.roundAll({ r: 2 })`) rounds **every** edge of the solid
2099
+ — convex and concave — with radius ≈ 2 mm in one pass, on both backends, with
2100
+ no OCCT routing. Faces stay in place (within backend tolerance). It is the
2101
+ blunt, global counterpart to `fillet`/`chamfer`: there is no edge selection,
2102
+ and features smaller than the ball are **consumed** — walls thinner than `2r`
2103
+ melt away, holes narrower than `2r` seal shut. That makes it ideal for
2104
+ "soften this whole organic part" and wrong for parts where a specific edge
2105
+ must stay sharp (use `fillet` with a selector for that).
2106
+
2107
+ Rules of thumb:
2108
+
2109
+ - Keep `r` under half your thinnest wall unless you *want* melting.
2110
+ - Preview and STL/3MF export always work (mesh morphology). STEP export gets
2111
+ true B-rep arc surfaces while `r` is below the smallest feature size; at
2112
+ consuming radii OCCT cannot represent the melt and **skips the op whole**
2113
+ with a `roundall-skipped` warning — the STEP then has the un-rounded shape.
2114
+ - Relying on consumption? Add a `verify` volume assertion so a regression in
2115
+ the radius (or a backend skip) fails loudly.
2116
+ - A small ridge remaining where a thin rib was consumed is correct morphology
2117
+ (the rib's dilation fillet survives the opening), not a bug.
2118
+
2096
2119
  ### Cost on the OCCT path: fillet/chamfer scale with edge count — and order matters
2097
2120
 
2098
2121
  These costs apply when a sub-part **does** run on OCCT (a shell, an unsupported edge
@@ -101,6 +101,12 @@ Variant literals under this entry: `extrude: unknown bevel option`, `extrude: be
101
101
  - **Cause:** OCCT's native fillet cannot build the rim round-over at a degenerate boundary (e.g. a rim radius exactly equal to `round.side` on a stadium profile, `2·side == min(w, d)`) and would otherwise return invalid-but-nonempty geometry; the monotonicity/validity gate in `occt-repair.js`'s `safeOp` catches it and skips the feature rather than exporting invalid STEP.
102
102
  - **Fix:** shrink the affected rim radius slightly below `round.side` (or below the degenerate boundary), or accept the sharp rim at that exact radius.
103
103
 
104
+ ## roundall-skipped
105
+
106
+ - **Symptom:** console warning `partforge: roundall-skipped: offset step … produced no valid solid — r=… is likely at/above the smallest feature size; returning the un-rounded solid`, and the OCCT build (STEP export, CLI measure with the OCCT backend) shows sharp edges where `roundAll` was expected.
107
+ - **Cause:** the B-rep backend implements `roundAll` as a triple OCCT offset, which cannot change topology: a radius at or above the smallest local feature (thin wall < 2r, hole < 2r) has no valid B-rep offset result, so the op skips whole rather than emit garbage (KERNEL-CONTRACT.md, `roundAll` row). The mesh backend meanwhile performs true consumption — so preview and STEP legitimately differ in this regime.
108
+ - **Fix:** reduce `r` below half the smallest wall/hole radius if STEP fidelity matters; or accept the divergence (preview/STL are correct) and gate the part with a `verify` volume assertion so the behavior is intentional.
109
+
104
110
  ## boolean-not-watertight
105
111
 
106
112
  - **Symptom:** `NOT watertight ✗` from `partforge measure` (non-zero exit) after adding a boolean cut or union.
@@ -1,6 +1,6 @@
1
1
  # The partforge kernel contract
2
2
 
3
- **Contract version: 3** (introduced in partforge 0.62) — mirrored by `CONTRACT_VERSION`
3
+ **Contract version: 4** (introduced in partforge 0.63) — 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
 
@@ -68,6 +68,20 @@ Its coverage and tolerance band are part of the contract:
68
68
  labels upstream of the call do not survive through it (attribution uses the
69
69
  fallback path), and the result shades with the default SMOOTH policy.
70
70
 
71
+ `roundAll` is required on **every** class (it is in `SOLID_OPS`, not
72
+ `OCCT_ONLY_OPS`) and is **parity-tolerant with a regime split**: while `r` is
73
+ strictly below the solid's smallest feature size (min wall, min hole
74
+ diameter / 2), both classes produce the morphological result and volumes agree
75
+ within the mesh-tolerance band. That band is stated at the **export (print)
76
+ tier**, where the mesh class's tessellation is fine enough to compare against
77
+ B-rep arcs (box and L-bracket cases: ≤ 0.05%); the preview tier is coarser by
78
+ design and runs ~0.2% below the B-rep volume, which is tessellation, not a
79
+ parity failure. At consuming radii, only the core (mesh) class
80
+ performs true consumption; a B-rep class MUST either produce a valid solid or
81
+ skip the entire op (returning the input unchanged, warning `roundall-skipped`)
82
+ — emitting an invalid or semantically wrong solid is non-conforming. Authors
83
+ relying on consumption should gate it with a `verify` volume assertion.
84
+
71
85
  **B-rep class.** Core plus native `fillet`/`chamfer`/`shell` and `toSTEP`. The in-repo
72
86
  OCCT/replicad backend is the reference.
73
87
 
@@ -304,6 +318,7 @@ Normative signatures: `kernel.js`'s `@typedef Solid`.
304
318
  | `toSTL({quality?})` | `Promise<ArrayBuffer>`, binary STL, outward CCW winding. Stored facet normals may be zero — slicers recompute them (the mesh backend happens to write them). |
305
319
  | `toIndexedMesh({quality?})` | `{positions, indices}` indexed mesh (3MF path); defaults to `"print"` like `toSTL`. Coincident vertices need NOT be welded — the 3MF writer welds, because that format reads topology from the indices rather than re-stitching soup by position the way an STL consumer does. |
306
320
  | `fillet(r)` · `fillet({r, edges?})` / `chamfer(d)` · `chamfer({d, edges?})` / `shell({t, open})` | `fillet`/`chamfer`: implemented on BOTH in-repo classes since v3 — exactly on B-rep, tolerance-band on the mesh class for straight and circular-arc edge chains (see [Conformance classes](#conformance-classes)); an edge class the mesh kernel cannot blend throws `KernelCapabilityError` and reroutes. Zero magnitude — `fillet(0)` / `chamfer({d: 0})` — is the identity on every class (returns the solid unchanged, never throws; `shell` excluded, `t: 0` is degenerate). Scalar `fillet(3)`/`chamfer(1)` acts on all edges; the options form adds an `edges` selector. `shell` remains B-rep-only (core throws), hollows inward keeping outer dimensions; `open` (face selector) is required. |
321
+ | `roundAll(r)` · `roundAll({r})` | Morphological close-then-open with a ball of radius `r`: rounds EVERY edge (convex and concave) at radius ≈ `r`; faces stay in place (within the class's tolerance band — the B-rep offset chain can drift ~0.1 mm); features smaller than the ball are consumed (walls < 2r melt, holes < 2r seal). Implemented natively on BOTH classes — never routes, never throws `KernelCapabilityError`. Parity-tolerant only while `r` is below the smallest feature size; at consuming radii the mesh class performs true consumption and a B-rep class MAY skip the whole op unchanged with a `roundall-skipped` warning (skip is the only permitted degrade). `roundAll(0)` is the identity on every class. |
307
322
 
308
323
  `quality` (`"preview"` | `"print"`) is **advisory**: it trades tessellation density for
309
324
  speed and a backend may bake it at kernel creation (Manifold does). A part must never
@@ -680,6 +695,13 @@ in `kernel.js` define the current surface; only breaking changes bump the versio
680
695
  `cut` per CadQuery/replicad rather than OpenSCAD's `difference`), so LLM priors
681
696
  transfer. Renames are breaking changes with no offsetting benefit — don't.
682
697
 
698
+ **v3 → v4** (partforge 0.63): `Solid.roundAll` is added to `SOLID_OPS` (portable
699
+ morphological rounding — rounds every edge with a ball of radius `r`;
700
+ parity-tolerant, regime-split semantics, see
701
+ [Conformance classes](#conformance-classes)). Additive for parts — nothing
702
+ existing calls it — but breaking for backend implementers: a core kernel must
703
+ now implement it (no stub, unlike the `OCCT_ONLY_OPS` ops).
704
+
683
705
  **v2 → v3** (partforge 0.62): `Solid.fillet` and `Solid.chamfer` are implemented
684
706
  natively on the mesh (core reference) kernel for straight and circular-arc edge
685
707
  chains, and are **no longer probe-routed to OCCT** — `ROUTED_CAD_OPS` (`shell`) is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.62.1",
3
+ "version": "0.63.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",
@@ -16,7 +16,7 @@
16
16
  // The prose half's version: docs/KERNEL-CONTRACT.md's "Contract version" header
17
17
  // must match this number (asserted in kernel-contract.test.js). Bump only on a
18
18
  // breaking contract change — see the doc's Versioning section.
19
- export const CONTRACT_VERSION = 3;
19
+ export const CONTRACT_VERSION = 4;
20
20
 
21
21
  // Ops every backend kernel must implement.
22
22
  export const KERNEL_OPS = [
@@ -38,7 +38,7 @@ export const SOLID_OPS = [
38
38
  "cut", "cutAll", "intersect", "union", "clone", "label", "boundingBox", "volume",
39
39
  "translate", "rotate", "rotateX", "rotateY", "rotateZ", "rotateAbout", "along", "at",
40
40
  "mirror", "scale", "toMesh", "toSTL", "toIndexedMesh",
41
- "fillet", "chamfer", "shell",
41
+ "fillet", "chamfer", "shell", "roundAll",
42
42
  ];
43
43
 
44
44
  // Backend-optional Solid queries: Manifold mesh-topology numbers (measure.js
@@ -13,6 +13,7 @@ import { meshToStl } from "./mesh-stl.js";
13
13
  import { creasedNormals } from "./creased-normals.js";
14
14
  import { loftShadingPolicy, SMOOTH } from "./shading-policy.js";
15
15
  import { meshFillet, meshChamfer, UnsupportedEdgeError } from "./mesh-fillet.js";
16
+ import { meshRoundAll } from "./mesh-roundall.js";
16
17
  import { KernelCapabilityError } from "./errors.js";
17
18
 
18
19
  const PLANE_NORMAL = { XY: [0, 0, 1], XZ: [0, 1, 0], YZ: [1, 0, 0] };
@@ -143,6 +144,14 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
143
144
  return cached(h("chamfer", hash, d, selector ?? null, segs), () =>
144
145
  meshCadOp("chamfer", () => meshChamfer(kernel, wrap(m, hash), { d, edges: selector, segs })));
145
146
  },
147
+ roundAll: (r) => {
148
+ if (r === 0) return wrap(m, hash); // contract: zero magnitude is the identity
149
+ // `quality` in the key is redundant but harmless — the cache lives on a
150
+ // per-quality kernel, so it can never collide across tiers (the OCCT twin
151
+ // key omits it for the same reason); it just spells out that the ball
152
+ // tessellation, and so the result, is tier-dependent.
153
+ return cached(h("roundAll", hash, r, quality), () => T(meshRoundAll(wasm, m, r, quality)));
154
+ },
146
155
  cutAll: (tools) => cached(h("cutAll", hash, tools.map((t) => t._hash)),
147
156
  () => T(m.subtract(unionRaw(tools.map((t) => t._m))))),
148
157
  intersect: (t) => cached(h("intersect", hash, t._hash), () => T(m.intersect(t._m))),
@@ -0,0 +1,83 @@
1
+ // Morphological whole-solid rounding on the mesh backend: close-then-open with
2
+ // a ball — dilate(+r), erode(2r), dilate(+r) via Manifold's native Minkowski
3
+ // sum/difference. Rounds EVERY edge (convex and concave) at radius ≈ r and
4
+ // consumes features smaller than the ball (walls < 2r melt, holes < 2r seal) —
5
+ // that is the op's contract, not a defect (docs/roundall-design.md).
6
+ //
7
+ // simplify() between steps is mandatory, not an optimization: the naive
8
+ // Minkowski (hull-of-triangle-pairs) emits sliver-degenerate meshes whose
9
+ // complexity compounds through the chain — the design spike measured 106s and
10
+ // 761k broken-topology triangles without it vs 2.4s and 340 clean triangles
11
+ // with it, on the same torture case. asOriginal() first, because simplify()
12
+ // will not collapse triangles across run (originalID) boundaries and the
13
+ // Minkowski output is stitched from many.
14
+ //
15
+ // This op must NEVER throw KernelCapabilityError / NEEDS_OCCT: the mesh
16
+ // backend is roundAll's reference implementation — rerouting to OCCT would
17
+ // trade a correct result for a skip (occt-roundall.js can only skip where
18
+ // morphology exceeds what B-rep offsets support).
19
+ //
20
+ // INVARIANT — both balls share ONE segment count. Minkowski support functions
21
+ // add, so the chain displaces a face with normal n by 2·h_r(n) − h_2r(n), where
22
+ // h is the ball's support. Manifold spheres built with equal `segs` are similar
23
+ // (the 2r ball is the r ball scaled by two), so h_2r = 2·h_r and the term is
24
+ // exactly zero in EVERY direction — the input's planar faces return to their
25
+ // original planes. Give the two balls different segment counts and the term is
26
+ // only zero where a vertex happens to line up: axis-aligned boxes still look
27
+ // right while off-axis faces drift (measured 0.07mm at preview, r=2). The count
28
+ // is sized from the erosion ball (2r), the larger of the two, so the coarser of
29
+ // the two facetings still meets the tier's sagitta tolerance.
30
+
31
+ // Sphere tessellation from the facet sagitta r·(1 − cos(π/segs)): pick the
32
+ // fewest segments that keep it under the quality tier's tolerance.
33
+ const SAGITTA_TOL = { preview: 0.05, print: 0.01 }; // mm
34
+ export function roundAllSegs(r, quality) {
35
+ const tol = SAGITTA_TOL[quality] ?? SAGITTA_TOL.preview;
36
+ if (!(r > tol)) return 12;
37
+ return Math.min(64, Math.max(12, Math.ceil(Math.PI / Math.acos(1 - tol / r))));
38
+ }
39
+
40
+ export function meshRoundAll(wasm, m, r, quality) {
41
+ if (!Number.isFinite(r) || r <= 0) throw new Error("roundAll: r must be a finite number > 0 (r = 0 is handled as the identity by the caller)");
42
+ const segs = roundAllSegs(2 * r, quality); // ONE count for BOTH balls — see the invariant above
43
+ // Simplify tolerance: max(r/100, 0.01) mm, but never enough to collapse the
44
+ // rim ring of a rounded edge into the next ring up. That ring sits
45
+ // r·(1 − cos(2π/segs)) above the face plane, and it is what holds a planar
46
+ // face at its exact position; collapsing it shaves the face inward (print
47
+ // tier, r = 2: segs 45 puts the ring 0.0195 mm up, and a flat 0.02 tolerance
48
+ // pulled every face of a box in by 0.034 mm). Half that spacing keeps margin.
49
+ const tol = Math.min(Math.max(r / 100, 0.01), 0.5 * r * (1 - Math.cos((2 * Math.PI) / segs)));
50
+ const step = (input, sphere, op) => {
51
+ const raw = op === "sum" ? input.minkowskiSum(sphere) : input.minkowskiDifference(sphere);
52
+ let orig;
53
+ try {
54
+ orig = raw.asOriginal();
55
+ } finally {
56
+ raw.delete?.();
57
+ }
58
+ try {
59
+ return orig.simplify(tol);
60
+ } finally {
61
+ orig.delete?.();
62
+ }
63
+ };
64
+ const sphR = wasm.Manifold.sphere(r, segs);
65
+ const sph2R = wasm.Manifold.sphere(2 * r, segs);
66
+ try {
67
+ const a = step(m, sphR, "sum"); // dilate: rounds convex, seals holes < 2r
68
+ let b;
69
+ try {
70
+ b = step(a, sph2R, "diff"); // erode 2r: melts walls < 2r
71
+ } finally {
72
+ a.delete?.();
73
+ }
74
+ try {
75
+ return step(b, sphR, "sum"); // dilate back: final radius ≈ r everywhere
76
+ } finally {
77
+ b.delete?.();
78
+ }
79
+ } finally {
80
+ sphR.delete?.();
81
+ sph2R.delete?.();
82
+ }
83
+ }
@@ -21,6 +21,7 @@ import { addSugar } from "./solid-sugar.js";
21
21
  import { makeShape2dFactory } from "./shape2d.js";
22
22
  import { finishKernel } from "./kernel-front.js";
23
23
  import { createOcctRepair } from "./occt-repair.js";
24
+ import { occtRoundAll } from "./occt-roundall.js";
24
25
  import { classifyFaceGroups } from "./feature-attribution.js";
25
26
  import { resolveRings } from "./loft.js";
26
27
  import { resolveSweepStations } from "./sweep.js";
@@ -223,6 +224,14 @@ export function createOcctKernel(replicad) {
223
224
  return wrap(validChamfer(a._s, toEdgeFinder(selector), distance), cloneLabels(a._labels), key);
224
225
  });
225
226
  },
227
+ roundAll: (r) => {
228
+ const key = h("roundAll", hash, r);
229
+ return cached(key, () => {
230
+ const a = mat();
231
+ if (r === 0) return wrap(a._s.clone(), cloneLabels(a._labels), key);
232
+ return wrap(occtRoundAll(replicad, a._s, r), cloneLabels(a._labels), key);
233
+ });
234
+ },
226
235
  shell: (thickness, openFaces) => {
227
236
  if (openFaces == null) throw new Error("shell: openFaces is required (a fully closed hollow is not supported)");
228
237
  const key = h("shell", hash, thickness, selKey(openFaces));
@@ -0,0 +1,94 @@
1
+ // B-rep side of roundAll: dilate(+r), erode(-2r), dilate(+r) via raw OCCT
2
+ // BRepOffsetAPI_MakeOffsetShape (replicad has no solid-offset wrapper). Each
3
+ // step runs a variant cascade — the design spike showed no single parameter
4
+ // combo survives all shapes: plain solids dilate with Arc/Intersection=false,
5
+ // the chained erosion only succeeds with Arc/Intersection=true, and concave
6
+ // post-boolean solids only dilate with the Intersection join (morphologically
7
+ // fine: the FINAL Arc dilation supplies the convex rounding).
8
+ //
9
+ // A candidate is accepted only if IsDone, non-null, closed (mesh check),
10
+ // finite positive volume, AND volume-monotonic (dilation must not shrink,
11
+ // erosion must not grow). The monotonic gate is load-bearing: OCCT offsets
12
+ // return BRepCheck-valid garbage — the spike saw an erosion "succeed" at 0.4%
13
+ // of the input volume, and a sealed hole come back as an 11% crater. A gated
14
+ // failure skips the WHOLE op (warn + clone), mirroring safeOp's fillet policy
15
+ // and for the same reason: OCCT offset failures are not monotonic in r, so
16
+ // searching for a "largest working radius" would converge on garbage.
17
+ // Feature-consuming radii (r at/above the smallest feature) are the expected
18
+ // skip trigger — true consumption is mesh-class-only (docs/roundall-design.md).
19
+ //
20
+ // Every WASM object made here is freed, same convention as occt-repair.js, so
21
+ // OCCT's heap doesn't grow across regenerates: up to twelve offset builders and
22
+ // progress ranges, every rejected candidate, and each superseded intermediate.
23
+ // The one thing never freed is the caller's `shape` — it belongs to the caller,
24
+ // and the skip path hands back a clone of it.
25
+ import { isClosedSolid } from "./occt-repair.js";
26
+
27
+ const VARIANTS = [
28
+ { join: "arc", inter: false },
29
+ { join: "arc", inter: true },
30
+ { join: "int", inter: false },
31
+ { join: "int", inter: true },
32
+ ];
33
+
34
+ export function occtRoundAll(replicad, shape, r) {
35
+ if (!Number.isFinite(r) || r <= 0) throw new Error("roundAll: r must be a finite number > 0 (r = 0 is handled as the identity by the caller)");
36
+ const oc = replicad.getOC();
37
+ const tryOffset = (topo, offset, v) => {
38
+ const progress = new oc.Message_ProgressRange_1();
39
+ const mk = new oc.BRepOffsetAPI_MakeOffsetShape();
40
+ try {
41
+ mk.PerformByJoin(topo, offset, 1e-6,
42
+ oc.BRepOffset_Mode.BRepOffset_Skin, v.inter, false,
43
+ v.join === "arc" ? oc.GeomAbs_JoinType.GeomAbs_Arc : oc.GeomAbs_JoinType.GeomAbs_Intersection,
44
+ false, progress);
45
+ if (!mk.IsDone()) return null;
46
+ const s = mk.Shape();
47
+ // Wrap BEFORE the finally frees the builder — replicad's own idiom for this
48
+ // very algorithm (its `offset()` does `cast(offsetBuilder.Shape()); offsetBuilder.delete()`).
49
+ // The wrapper carries its own TopoDS_Shape handle, so the result outlives `mk`.
50
+ return s.IsNull() ? null : new replicad.Solid(s);
51
+ } catch {
52
+ return null;
53
+ } finally {
54
+ mk.delete?.();
55
+ progress.delete?.();
56
+ }
57
+ };
58
+ let vol;
59
+ try {
60
+ vol = replicad.measureVolume(shape);
61
+ } catch (e) {
62
+ // Can't gate what can't be measured — skip rather than run the cascade blind.
63
+ console.warn(`partforge: roundall-skipped: the input solid's volume could not be measured (${e?.message || e}); returning the un-rounded solid`);
64
+ return shape.clone(); // if the clone throws too, the caller's shape is unusable — let it propagate
65
+ }
66
+ let cur = shape;
67
+ for (const off of [r, -2 * r, r]) {
68
+ let next = null;
69
+ for (const v of VARIANTS) {
70
+ const cand = tryOffset(cur.wrapped, off, v);
71
+ if (!cand) continue;
72
+ let cvol;
73
+ try { cvol = replicad.measureVolume(cand); } catch { cand.delete?.(); continue; }
74
+ if (!Number.isFinite(cvol) || cvol <= 0) { cand.delete?.(); continue; }
75
+ if (off > 0 && cvol < vol * 0.999) { cand.delete?.(); continue; } // dilation shrank: garbage
76
+ if (off < 0 && cvol > vol * 1.001) { cand.delete?.(); continue; } // erosion grew: garbage
77
+ // isClosedSolid meshes the candidate, and meshing OCCT offset garbage can
78
+ // throw — that is just another rejected candidate, not an escape hatch out
79
+ // of "roundAll never throws for geometry".
80
+ try { if (!isClosedSolid(cand)) { cand.delete?.(); continue; } }
81
+ catch { cand.delete?.(); continue; }
82
+ next = cand;
83
+ vol = cvol;
84
+ break;
85
+ }
86
+ if (cur !== shape) cur.delete?.(); // superseded intermediate; never the caller's shape
87
+ if (!next) {
88
+ console.warn(`partforge: roundall-skipped: offset step ${off} produced no valid solid — r=${r} is likely at/above the smallest feature size; returning the un-rounded solid`);
89
+ return shape.clone();
90
+ }
91
+ cur = next;
92
+ }
93
+ return cur;
94
+ }
@@ -289,6 +289,8 @@ export const SOLID_OP_SPECS = {
289
289
  return [req("chamfer", o, "d"), ...(o.edges !== undefined ? [o.edges] : [])]; } },
290
290
  shell: { toArgs: (o) => { checkKeys("shell", o, ["t", "open"]);
291
291
  return [req("shell", o, "t"), req("shell", o, "open")]; } },
292
+ roundAll: { toArgs: (o) => { checkKeys("roundAll", o, ["r"]);
293
+ return [req("roundAll", o, "r")]; } },
292
294
  };
293
295
 
294
296
  // A zero-magnitude fillet/chamfer is the identity on every conformance class
package/types/kernel.d.ts CHANGED
@@ -269,6 +269,12 @@ export interface Solid {
269
269
  * Closed (no open face) hollows are not supported.
270
270
  */
271
271
  shell(o: { t: number; open: FaceSelector }): Solid;
272
+ /**
273
+ * Morphological close-then-open with a ball of radius `r`: rounds EVERY edge
274
+ * (convex and concave). Implemented natively on both backends — never routes,
275
+ * never throws `KernelCapabilityError`. `roundAll(0)` is the identity.
276
+ */
277
+ roundAll(r: number | { r: number }): Solid;
272
278
  /** Through-hole count (Manifold only). */
273
279
  genus?(): number;
274
280
  /** No geometry at all (Manifold only). */