partforge 0.137.0 → 0.138.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/README.md +2 -1
- package/docs/AUTHORING-PARTS.md +12 -3
- package/docs/ERROR-PATTERNS.md +2 -2
- package/docs/KERNEL-CONTRACT.md +16 -7
- package/package.json +1 -1
- package/src/framework/geometry/blend-surfaces.js +16 -1
- package/src/framework/geometry/creased-normals.js +23 -1
- package/src/framework/geometry/kernel.js +2 -2
- package/src/framework/geometry/manifold-backend.js +11 -7
- package/src/framework/geometry/mesh-build.js +32 -0
- package/src/framework/geometry/mesh-fillet.js +971 -30
- package/src/framework/geometry/occt-backend.js +42 -9
- package/src/framework/geometry/region-interaction.js +98 -0
- package/src/framework/lint/rules-schema.js +27 -0
- package/src/parts/screw.js +3 -1
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ Two geometry backends run in Web Workers, and partforge routes each part to whic
|
|
|
43
43
|
needs:
|
|
44
44
|
|
|
45
45
|
- **[Manifold](https://github.com/elalish/manifold)** — fast preview meshes and STL / 3MF,
|
|
46
|
-
including mesh-native fillet and chamfer for straight
|
|
46
|
+
including mesh-native fillet and chamfer for straight, circular, planar-rim and
|
|
47
|
+
curved-face edges.
|
|
47
48
|
- **[Replicad](https://replicad.xyz)** (OpenCASCADE-in-WebAssembly) — exact B-rep for STEP,
|
|
48
49
|
shell, and automatic fallback for blend geometry Manifold cannot handle.
|
|
49
50
|
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -3583,7 +3583,7 @@ previously didn't; that's the fix working as intended, not a regression.
|
|
|
3583
3583
|
`duplicate-preset-name`, `duplicate-node-id`, `select-options-missing`,
|
|
3584
3584
|
`select-default-not-in-options`, `log-scale-needs-positive-min`,
|
|
3585
3585
|
`when-key-not-in-defaults`, `when-unknown-operator`, `unknown-control-type`,
|
|
3586
|
-
`custom-control-widget-not-function`, `custom-default-not-json`,
|
|
3586
|
+
`legacy-shape-widget`, `custom-control-widget-not-function`, `custom-default-not-json`,
|
|
3587
3587
|
`custom-keys-not-in-defaults` (errors);
|
|
3588
3588
|
`slider-range-excludes-default`, `unknown-control-field`, `duplicate-control-key`,
|
|
3589
3589
|
`default-not-exposed`, `readout-unknown-derived-key`, `slider-refinement-invalid`,
|
|
@@ -3627,6 +3627,13 @@ each comparison operator (`{ gt: 0 }`, `{ in: [...] }`, …) must be one
|
|
|
3627
3627
|
condition is always false and the node never shows — which is why both are
|
|
3628
3628
|
errors rather than warnings.
|
|
3629
3629
|
|
|
3630
|
+
`legacy-shape-widget` fires when an entry in the legacy shape (`advanced`, `toggles`,
|
|
3631
|
+
`features`) sets `type`, or sets `control` to anything but `slider`, `number`, `text` or
|
|
3632
|
+
`textarea`. The legacy shape never reads `type`, so a `{ type: "select", options }` there
|
|
3633
|
+
renders as a slider and writes numbers into a key the build compares against strings;
|
|
3634
|
+
put such a control in a `controls` section, where `type` picks the widget. A boolean in the
|
|
3635
|
+
legacy shape belongs in `toggles`.
|
|
3636
|
+
|
|
3630
3637
|
`unknown-control-type` fires when an authored control's `type` (e.g. a typo
|
|
3631
3638
|
like `"sldier"`) isn't one of the recognised widget types — the renderer skips
|
|
3632
3639
|
a node with an unrecognised type entirely, so the control silently vanishes
|
|
@@ -4156,9 +4163,11 @@ contract v3 that **includes fillet and chamfer**: the mesh backend blends straig
|
|
|
4156
4163
|
edges, circular-arc edges (bore rims, cylinder rims, the arcs where fillets meet a
|
|
4157
4164
|
face), and **planar contour edges at constant dihedral** — the top/bottom rims of any
|
|
4158
4165
|
extruded profile, however curvy its outline: `text2d` lettering, `Shape2D.offset`
|
|
4159
|
-
outlines, spline profiles all round natively now.
|
|
4166
|
+
outlines, spline profiles all round natively now. Edges **between two curved faces** — a boss meeting a tube, a cross hole's rim, the
|
|
4167
|
+
ellipse where a plane cuts a cylinder — blend natively too (a per-vertex
|
|
4168
|
+
cross-section, since 0.138). Only `shell` still routes a
|
|
4160
4169
|
sub-part to OCCT up front; a fillet/chamfer on an edge class the mesh backend can't
|
|
4161
|
-
blend (
|
|
4170
|
+
blend (mixed convexity along one edge, a bend tighter than the fillet radius, knife edges) reroutes that sub-part to OCCT automatically
|
|
4162
4171
|
at runtime — no declaration needed either way:
|
|
4163
4172
|
|
|
4164
4173
|
| Op | Meaning |
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -763,8 +763,8 @@ between the Manifold preview and the OCCT STEP export.
|
|
|
763
763
|
|
|
764
764
|
## mesh-fillet-unsupported-edge
|
|
765
765
|
|
|
766
|
-
- **Symptom:** `fillet: ` or `chamfer: ` followed by an edge-class reason — e.g. `edge curve is not circular`, `flank angle varies along the arc`, `selector matched no sharp edges`, `~180° knife edge` — thrown as a `KernelCapabilityError`, or a preview sub-part silently rebuilding on the slow OCCT worker.
|
|
767
|
-
- **Cause:** The mesh backend's native fillet/chamfer (`mesh-fillet.js`) covers straight and
|
|
766
|
+
- **Symptom:** `fillet: ` or `chamfer: ` followed by an edge-class reason — e.g. `edge curve is not circular`, `flank angle varies along the arc`, `selector matched no sharp edges`, `~180° knife edge`, `general chain: bend too tight for fillet 2 (local radius 1.48 mm)`, `general chain: too complex for the mesh fillet (work 303831 > budget 195000)`, `general chain: too many sections for the mesh fillet (… stations > …)` — thrown as a `KernelCapabilityError`, or a preview sub-part silently rebuilding on the slow OCCT worker.
|
|
767
|
+
- **Cause:** The mesh backend's native fillet/chamfer (`mesh-fillet.js`) covers straight, circular, planar-rim and curved-face sharp-edge chains; the selected edges fall outside that (an edge that flips between convex and concave, a bend tighter than the radius, a knife edge, or nothing sharp matched), so the op signals `NEEDS_OCCT` and the framework reroutes that sub-part (the CLI re-execs once with `PARTFORGE_BACKEND=occt`). The two `general chain: too …` reasons are complexity refusals, not edge classes: a selection containing a curved-face edge whose estimated blend work (every selected edge, weighted by kind, × √triangles) exceeds the budget — thread, knurl and texture geometry with thousands of sharp edges behind a few curved ones — or a single curved-face edge needing more cross-sections than the cap. Both are deterministic counts, never timings; because the work grows with √triangles, a sub-part near the budget can blend at preview and reroute at print (same reroute, OCCT result correct).
|
|
768
768
|
- **Fix:** Usually nothing — the reroute is the designed degrade and the OCCT result is correct, just slower. To stay on Manifold, restructure so the blend lands on a supported edge class (design the rounding into the profile, or fillet before the boolean that curves the edge). See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Fillet, chamfer & shell".
|
|
769
769
|
|
|
770
770
|
## mesh-fillet-oversized-radius
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -48,12 +48,19 @@ list).
|
|
|
48
48
|
implements `fillet` and `chamfer` natively** (`mesh-fillet.js` — tangent-tool CSG).
|
|
49
49
|
Its coverage and tolerance band are part of the contract:
|
|
50
50
|
|
|
51
|
-
- **Edge classes:** straight sharp edges with planar flanks
|
|
51
|
+
- **Edge classes:** straight sharp edges with planar flanks; circular-arc sharp
|
|
52
52
|
edges whose flanks are surfaces of revolution about the arc axis (bore rims,
|
|
53
|
-
cylinder rims, the arcs where blends meet a face) — full circles included
|
|
54
|
-
edges
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
cylinder rims, the arcs where blends meet a face) — full circles included;
|
|
54
|
+
planar-rim edges (an edge lying in a face plane at a constant wall angle — the
|
|
55
|
+
rims of any extruded outline); and, since 0.138, curved-face edges between two
|
|
56
|
+
curved faces (a boss meeting a tube, a cross hole's rim, the ellipse where a plane
|
|
57
|
+
cuts a cylinder), blended with a per-vertex cross-section. Convex edges subtract a
|
|
58
|
+
cutter; concave edges union a filler. A selection the mesh class cannot blend
|
|
59
|
+
throws `KernelCapabilityError` so the host can reroute that build to a B-rep
|
|
60
|
+
kernel: an edge that flips between convex and concave along its length, a bend
|
|
61
|
+
tighter than the radius, a knife edge (anti-parallel flanks), a curved-face
|
|
62
|
+
selection over the mesh fillet's complexity budget, or a function selector.
|
|
63
|
+
There is no partial blending — one such edge reroutes the whole sub-part.
|
|
57
64
|
- **Tolerance band, not identity:** the blend surface is the exact rolling-ball
|
|
58
65
|
(fillet) or setback-chord (chamfer) surface to within tessellation, plus
|
|
59
66
|
micron-scale robustness allowances (tool overshoot past tangency and seam-grazing
|
|
@@ -370,7 +377,7 @@ Normative signatures: `kernel.js`'s `@typedef Solid`.
|
|
|
370
377
|
| `toMesh({quality?})` | Render mesh: `{positions, normals, indices?, triangles, edges?, featureIds?, features?}`. `indices` optional (a backend may emit soup or indexed); `normals` and `edges` are authoritative shading intent from both backends — see [Shading intent](#shading-intent-tomesh-normals-and-edges) below; `featureIds`/`features` are optional metadata. |
|
|
371
378
|
| `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). |
|
|
372
379
|
| `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. |
|
|
373
|
-
| `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
|
|
380
|
+
| `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, circular-arc, planar-rim and curved-face 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. |
|
|
374
381
|
| `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. |
|
|
375
382
|
|
|
376
383
|
`quality` (`"preview"` | `"print"`) is **advisory**: it trades tessellation density for
|
|
@@ -942,7 +949,9 @@ now implement it (no stub, unlike the `OCCT_ONLY_OPS` ops).
|
|
|
942
949
|
|
|
943
950
|
**v2 → v3** (partforge 0.62): `Solid.fillet` and `Solid.chamfer` are implemented
|
|
944
951
|
natively on the mesh (core reference) kernel for straight and circular-arc edge
|
|
945
|
-
chains
|
|
952
|
+
chains (coverage has since grown inside v3 with no contract bump: planar-rim edges,
|
|
953
|
+
then curved-face edges in 0.138 — see [Conformance classes](#conformance-classes)),
|
|
954
|
+
and are **no longer probe-routed to OCCT** — `ROUTED_CAD_OPS` (`shell`) is
|
|
946
955
|
the remaining probe-routing set, and unsupported edge classes reroute at runtime via
|
|
947
956
|
`KernelCapabilityError`. Semantics change for existing parts: a part using fillet or
|
|
948
957
|
chamfer now previews (and STL/3MF-exports) from the mesh kernel's tolerance-band
|
package/package.json
CHANGED
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
// the swept path, f its face normal, dirs[i] the
|
|
20
20
|
// in-plane wall-side direction at vertex i, and the
|
|
21
21
|
// ball centre sits at kf·f + kd·dir off the edge
|
|
22
|
+
// { kind: "spine", pts, closed, r }
|
|
23
|
+
// general chain (mesh-fillet generalTool): the
|
|
24
|
+
// ball-centre polyline itself; r is the radius
|
|
22
25
|
//
|
|
23
26
|
// A path's polyline usually STANDS FOR a smooth curve (a circle's rim, an outline's
|
|
24
27
|
// rounded corner): its per-segment cylinders would shade faceted around the curve,
|
|
@@ -97,6 +100,11 @@ export function mapSurface(desc, M) {
|
|
|
97
100
|
dirs: desc.dirs.map((d) => norm(applyDir(M, d)) ?? d), f: norm(applyDir(M, desc.f)) ?? desc.f,
|
|
98
101
|
kf: desc.kf * s, kd: desc.kd * s };
|
|
99
102
|
}
|
|
103
|
+
case "spine": {
|
|
104
|
+
const s = Math.cbrt(Math.abs(
|
|
105
|
+
M[0] * (M[4] * M[8] - M[5] * M[7]) - M[3] * (M[1] * M[8] - M[2] * M[7]) + M[6] * (M[1] * M[5] - M[2] * M[4])));
|
|
106
|
+
return { kind: "spine", closed: desc.closed, pts: desc.pts.map((p) => applyPt(M, p)), r: desc.r * s };
|
|
107
|
+
}
|
|
100
108
|
default: throw new Error(`blend surface: unknown kind ${desc.kind}`);
|
|
101
109
|
}
|
|
102
110
|
}
|
|
@@ -168,7 +176,7 @@ function segmentGrid(desc) {
|
|
|
168
176
|
const { pts, closed } = desc, n = pts.length, nSeg = closed ? n : n - 1;
|
|
169
177
|
let total = 0;
|
|
170
178
|
for (let i = 0; i < nSeg; i++) { const p = pts[i], q = pts[(i + 1) % n]; total += Math.hypot(q[0] - p[0], q[1] - p[1], q[2] - p[2]); }
|
|
171
|
-
const h = Math.max(total / Math.max(1, nSeg), Math.abs(desc.kf) + Math.abs(desc.kd), 1e-6);
|
|
179
|
+
const h = Math.max(total / Math.max(1, nSeg), Math.abs(desc.kf ?? 0) + Math.abs(desc.kd ?? 0) + (desc.r ?? 0), 1e-6);
|
|
172
180
|
const cells = new Map();
|
|
173
181
|
// numeric hashed cell keys: a collision only adds candidates, each distance-checked
|
|
174
182
|
const key = (a, b, c) => (Math.imul(a, 73856093) ^ Math.imul(b, 19349663) ^ Math.imul(c, 83492791)) | 0;
|
|
@@ -232,6 +240,13 @@ function nearestSegment(desc, x) {
|
|
|
232
240
|
// Unit spine→point direction at x in the descriptor's frame, or null where undefined.
|
|
233
241
|
export function surfaceNormal(desc, x) {
|
|
234
242
|
if (desc.kind === "path") return pathNormal(desc, x);
|
|
243
|
+
if (desc.kind === "spine") {
|
|
244
|
+
const { bi, bt } = nearestSegment(desc, x);
|
|
245
|
+
if (bi < 0) return null;
|
|
246
|
+
const p = desc.pts[bi], q = desc.pts[(bi + 1) % desc.pts.length];
|
|
247
|
+
const c = [p[0] + (q[0] - p[0]) * bt, p[1] + (q[1] - p[1]) * bt, p[2] + (q[2] - p[2]) * bt];
|
|
248
|
+
return norm([x[0] - c[0], x[1] - c[1], x[2] - c[2]]);
|
|
249
|
+
}
|
|
235
250
|
const s = spinePoint(desc, x);
|
|
236
251
|
return s ? norm([x[0] - s[0], x[1] - s[1], x[2] - s[2]]) : null;
|
|
237
252
|
}
|
|
@@ -30,6 +30,8 @@ const ANALYTIC_COS = cosDeg(25);
|
|
|
30
30
|
// nanometre seam splits (see the weld below).
|
|
31
31
|
const SHADE_WELD = 5e-5;
|
|
32
32
|
const SHADE_CELL = 1e-3; // its hash-grid cell
|
|
33
|
+
// Vertex-normal sliver cutoff (mm of triangle height): see the smooth loop below.
|
|
34
|
+
const SHADE_SLIVER = 1e-3;
|
|
33
35
|
|
|
34
36
|
export function creasedNormals(g, { policies = null, featureLabels = null, surfaces = null } = {}) {
|
|
35
37
|
const np = g.numProp, vp = g.vertProperties, tris = g.triVerts;
|
|
@@ -205,7 +207,25 @@ export function creasedNormals(g, { policies = null, featureLabels = null, surfa
|
|
|
205
207
|
normals[o] = ln[0]; normals[o + 1] = ln[1]; normals[o + 2] = ln[2];
|
|
206
208
|
continue;
|
|
207
209
|
}
|
|
210
|
+
// Slivers vote last. The sum below is unweighted — every incident triangle that
|
|
211
|
+
// passes the surface and crease filters counts once, whatever its size — so a
|
|
212
|
+
// boolean's degenerate sliver (a wall strip split a hair from its own diagonal by
|
|
213
|
+
// a revolve cut; a loft seam's triangle with a collapsed edge) whose noisy facet
|
|
214
|
+
// normal lands inside the crease angle gets the same vote as the real faces
|
|
215
|
+
// around it. At a tall wall strip's end vertex one or two of them (18–31° off)
|
|
216
|
+
// tilted the whole strip's shading 5–10°: 12.5% of a plain cylinder's wall
|
|
217
|
+
// streaked under a top-rim fillet, 25% under a chamfer. Triangles under
|
|
218
|
+
// SHADE_SLIVER tall are left out of the sum and used only when nothing else
|
|
219
|
+
// contributes (a corner surrounded by slivers keeps today's normal), so shading
|
|
220
|
+
// changes only at vertices touching one. The cutoff sits in a measured gap: the
|
|
221
|
+
// streak slivers are ≤ ~2e-6 mm tall (and the loft seam's 5e-6), while the thin
|
|
222
|
+
// REAL facets of the reference parts (a coarse loft's, a screw's, a nameplate's)
|
|
223
|
+
// are 1e-3–4e-2 mm and keep their vote — MIN_FACE (0.04), the line pass's gate,
|
|
224
|
+
// took those out too and moved a lofted bottle's normals up to 5.6°.
|
|
225
|
+
// mesh-fillet's contact step leans to meet both neighbours at ≥ 45°, so it never
|
|
226
|
+
// passes the crease filter at all, whatever its height (see CONTACT_MU there).
|
|
208
227
|
let nx = 0, ny = 0, nz = 0, ax = 0, ay = 0, az = 0, analytic = false;
|
|
228
|
+
let sx = 0, sy = 0, sz = 0, solid = false;
|
|
209
229
|
for (const t2 of incident.get(weld[v])) {
|
|
210
230
|
// different cut surface → hard, EXCEPT when a blend surface (boundaryLines)
|
|
211
231
|
// is involved on either side. Blend↔blend: one band is many tool surfaces
|
|
@@ -220,12 +240,14 @@ export function creasedNormals(g, { policies = null, featureLabels = null, surfa
|
|
|
220
240
|
if (triOID[t2] !== oid &&
|
|
221
241
|
!(polFor(triOID[t2]).boundaryLines || polFor(oid).boundaryLines)) continue;
|
|
222
242
|
if (fn[t2 * 3] * fx + fn[t2 * 3 + 1] * fy + fn[t2 * 3 + 2] * fz < sharpCos) continue; // sharp same-surface edge → hard
|
|
223
|
-
|
|
243
|
+
if (thin[t2] < SHADE_SLIVER) { sx += fn[t2 * 3]; sy += fn[t2 * 3 + 1]; sz += fn[t2 * 3 + 2]; }
|
|
244
|
+
else { nx += fn[t2 * 3]; ny += fn[t2 * 3 + 1]; nz += fn[t2 * 3 + 2]; solid = true; }
|
|
224
245
|
if (runEval) {
|
|
225
246
|
const an = analyticAt(t2, weld[v]);
|
|
226
247
|
if (an) { ax += an[0]; ay += an[1]; az += an[2]; analytic = true; }
|
|
227
248
|
}
|
|
228
249
|
}
|
|
250
|
+
if (!solid) { nx = sx; ny = sy; nz = sz; }
|
|
229
251
|
if (analytic && Math.hypot(ax, ay, az) > 1e-9) { nx = ax; ny = ay; nz = az; }
|
|
230
252
|
const L = Math.hypot(nx, ny, nz) || 1;
|
|
231
253
|
const o = (t * 3 + k) * 3, vv = v * np;
|
|
@@ -106,8 +106,8 @@ export const ROUTED_CAD_OPS = ["shell"];
|
|
|
106
106
|
* `normals`/`edges` are authoritative shading intent from both backends — see docs/KERNEL-CONTRACT.md "Shading intent"; quality is advisory — the Manifold kernel bakes it at creation
|
|
107
107
|
* @property {(opts?: {quality?: "preview"|"print"}) => Promise<ArrayBuffer>} toSTL
|
|
108
108
|
* @property {() => {positions:Float32Array, indices:Uint32Array}} toIndexedMesh indexed mesh, for 3MF
|
|
109
|
-
* @property {(r:number|{r:number,edges?:object}) => Solid} fillet round edges; mesh-native
|
|
110
|
-
* @property {(d:number|{d:number,edges?:object}) => Solid} chamfer bevel edges; mesh-native
|
|
109
|
+
* @property {(r:number|{r:number,edges?:object}) => Solid} fillet round edges; mesh-native incl. curved-face edges, OCCT fallback otherwise; fillet(3) or fillet({r,edges}); legacy (r,selector) accepted for now (see file header)
|
|
110
|
+
* @property {(d:number|{d:number,edges?:object}) => Solid} chamfer bevel edges; mesh-native incl. curved-face edges, OCCT fallback otherwise; chamfer(1) or chamfer({d,edges}); legacy (d,selector) accepted for now (see file header)
|
|
111
111
|
* @property {(o:{t:number,open:object}) => Solid} shell hollow inward (OCCT only); legacy (thickness,openFaces) accepted for now (see file header)
|
|
112
112
|
* @property {() => number} [genus] through-hole count (Manifold only)
|
|
113
113
|
* @property {() => boolean} [isEmpty] no geometry at all (Manifold only)
|
|
@@ -6,7 +6,7 @@ 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";
|
|
9
|
-
import { manifoldFromMesh } from "./mesh-build.js";
|
|
9
|
+
import { manifoldFromMesh, ringStackManifold } from "./mesh-build.js";
|
|
10
10
|
import { createSolidCache } from "./solid-cache.js";
|
|
11
11
|
import { hoistCommonSuffix } from "./transform-hoist.js";
|
|
12
12
|
import { addSugar } from "./solid-sugar.js";
|
|
@@ -863,12 +863,10 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
863
863
|
if (e.error) throw e.error; // lazy: unusable-format entries fail at use, not at registration
|
|
864
864
|
return wrap(e.m, e.hash);
|
|
865
865
|
},
|
|
866
|
-
// Side-channel (underscore = off-contract
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
// EXCEPT an error entry is always upgradable (the post-crossover retry depends
|
|
871
|
-
// on this — see `_importDigest`).
|
|
866
|
+
// Side-channel for mesh-fillet's general-chain tools (underscore = off-contract):
|
|
867
|
+
// a solid from a stack of equal-size 3-D rings (mesh-build.js ringStackManifold).
|
|
868
|
+
_ringStackSolid: (rings, opts = {}) =>
|
|
869
|
+
cached(h("ringStack", rings, { closed: !!opts.closed }), () => T(ringStackManifold(wasm, rings, { closed: !!opts.closed }))),
|
|
872
870
|
// Side-channel for mesh-fillet (underscore = off-contract): `surf` describes the
|
|
873
871
|
// tool's blend wall in its current frame. Stored per surface id in the id's
|
|
874
872
|
// ORIGINAL frame (inverse runTransform), so any later pose or boolean — which
|
|
@@ -886,6 +884,12 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
886
884
|
}
|
|
887
885
|
return tool;
|
|
888
886
|
},
|
|
887
|
+
// Side-channel (underscore = off-contract, probe-invisible). Registration is
|
|
888
|
+
// TOTAL — it never throws for an unusable format; an `{error}` entry is stored
|
|
889
|
+
// verbatim and thrown by `import(name)` above at call time (spec: "Registration
|
|
890
|
+
// is total; errors are lazy"). Re-registering the same name+digest is a no-op
|
|
891
|
+
// EXCEPT an error entry is always upgradable (the post-crossover retry depends
|
|
892
|
+
// on this — see `_importDigest`).
|
|
889
893
|
_registerImport: ({ name, digest, positions, indices, error }) => {
|
|
890
894
|
const prev = imports.get(name);
|
|
891
895
|
if (!prev?.error && prev?.digest === digest) return; // error entries are always upgradable
|
|
@@ -41,6 +41,38 @@ export function reverseWinding(Tr) {
|
|
|
41
41
|
for (let t = 0; t < Tr.length; t += 3) { const tmp = Tr[t + 1]; Tr[t + 1] = Tr[t + 2]; Tr[t + 2] = tmp; }
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// A solid from a stack of equal-size 3-D rings (mesh-fillet's general-chain tools:
|
|
45
|
+
// one cross-section per edge vertex, each in its own plane). Consecutive rings are
|
|
46
|
+
// stitched by sideQuads; `closed` stitches the last ring back to the first (a loop,
|
|
47
|
+
// no caps), otherwise each end ring is capped by a fan from ITS vertex 0 — valid for
|
|
48
|
+
// rings star-shaped about vertex 0, which every fillet/chamfer section is (vertex 0
|
|
49
|
+
// is the section's corner). Ring orientation is arbitrary: the winding is normalized
|
|
50
|
+
// by the mesh's signed volume, because ofMesh throws or imports an inverted solid
|
|
51
|
+
// when it is wrong (see this file's header).
|
|
52
|
+
export function ringStackManifold(wasm, rings, { closed = false } = {}) {
|
|
53
|
+
const n = rings[0]?.length ?? 0;
|
|
54
|
+
if (n < 3 || !rings.every((r) => r.length === n)) throw new Error("ringStack: rings must have equal size (≥ 3 points)");
|
|
55
|
+
const V = [], Tr = [];
|
|
56
|
+
for (const ring of rings) for (const p of ring) V.push(p[0], p[1], p[2]);
|
|
57
|
+
sideQuads(Tr, rings.length, n, closed);
|
|
58
|
+
if (!closed) {
|
|
59
|
+
const last = (rings.length - 1) * n;
|
|
60
|
+
for (let j = 1; j + 1 < n; j++) {
|
|
61
|
+
Tr.push(0, j + 1, j); // first cap (flip, as fanCap's bottom)
|
|
62
|
+
Tr.push(last, last + j, last + j + 1); // last cap
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let vol6 = 0;
|
|
66
|
+
for (let t = 0; t < Tr.length; t += 3) {
|
|
67
|
+
const a = Tr[t] * 3, b = Tr[t + 1] * 3, c = Tr[t + 2] * 3;
|
|
68
|
+
vol6 += V[a] * (V[b + 1] * V[c + 2] - V[b + 2] * V[c + 1])
|
|
69
|
+
- V[a + 1] * (V[b] * V[c + 2] - V[b + 2] * V[c])
|
|
70
|
+
+ V[a + 2] * (V[b] * V[c + 1] - V[b + 1] * V[c]);
|
|
71
|
+
}
|
|
72
|
+
if (vol6 < 0) reverseWinding(Tr);
|
|
73
|
+
return manifoldFromMesh(wasm, V, Tr);
|
|
74
|
+
}
|
|
75
|
+
|
|
44
76
|
// Import a flat vertex array + triangle indices as a watertight Manifold. merge() welds
|
|
45
77
|
// coincident vertices so ofMesh sees a closed manifold; ofMesh consumes the mesh handle,
|
|
46
78
|
// so free it here and let the caller track the returned Manifold.
|