partforge 0.65.1 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -2241,6 +2241,20 @@ melt away, holes narrower than `2r` seal shut. That makes it ideal for
|
|
|
2241
2241
|
"soften this whole organic part" and wrong for parts where a specific edge
|
|
2242
2242
|
must stay sharp (use `fillet` with a selector for that).
|
|
2243
2243
|
|
|
2244
|
+
**Cost note.** On a Z-aligned extrusion (constant cross-section — a plate, a
|
|
2245
|
+
text backing, any straight-sided prism) roundAll takes a fast path: the ball
|
|
2246
|
+
morphology is computed as a 2-D close-open of the cross-section plus rim
|
|
2247
|
+
fillets, so even a complex text-outline backing rounds in well under a second.
|
|
2248
|
+
Everything else pays the full morphology (three Minkowski passes), whose
|
|
2249
|
+
runtime grows steeply with triangle count — a rotated or lofted solid of a few
|
|
2250
|
+
thousand triangles can take tens of seconds. When only a specific edge needs
|
|
2251
|
+
rounding, `fillet` with a selector (e.g. `{ inPlane: "XY", at: h }` for a rim)
|
|
2252
|
+
says what you mean and is always the cheap, predictable choice; reach for
|
|
2253
|
+
roundAll when the design genuinely calls for every edge softened at once. At
|
|
2254
|
+
the fast path's corners the plan silhouette follows the rim fillet's corner
|
|
2255
|
+
rounding (radius ≈ 1.05–1.25·r rather than exactly r) — the same corner
|
|
2256
|
+
treatment fillet itself applies.
|
|
2257
|
+
|
|
2244
2258
|
Rules of thumb:
|
|
2245
2259
|
|
|
2246
2260
|
- Keep `r` under half your thinnest wall unless you *want* melting.
|
package/package.json
CHANGED
|
@@ -72,7 +72,12 @@ export function creasedNormals(g, { policies = null, featureLabels = null } = {}
|
|
|
72
72
|
const v = tris[t * 3 + k];
|
|
73
73
|
let nx = 0, ny = 0, nz = 0;
|
|
74
74
|
for (const t2 of incident.get(remap[v])) {
|
|
75
|
-
|
|
75
|
+
// different cut surface → hard, EXCEPT two blend surfaces (boundaryLines on
|
|
76
|
+
// both): one band is many tool surfaces continuing each other tangentially,
|
|
77
|
+
// and hard normals at their handovers would put lighting seams along a band
|
|
78
|
+
// that used to shade as one re-originaled surface
|
|
79
|
+
if (triOID[t2] !== oid &&
|
|
80
|
+
!(polFor(triOID[t2]).boundaryLines && polFor(oid).boundaryLines)) continue;
|
|
76
81
|
if (fn[t2 * 3] * fx + fn[t2 * 3 + 1] * fy + fn[t2 * 3 + 2] * fz < sharpCos) continue; // sharp same-surface edge → hard
|
|
77
82
|
nx += fn[t2 * 3]; ny += fn[t2 * 3 + 1]; nz += fn[t2 * 3 + 2];
|
|
78
83
|
}
|
|
@@ -105,16 +110,32 @@ export function creasedNormals(g, { policies = null, featureLabels = null } = {}
|
|
|
105
110
|
// are invisible but whose long boundary edges would otherwise draw at full
|
|
106
111
|
// line weight. A triangle thinner than MIN_EDGE cannot be seen, so its
|
|
107
112
|
// edges are noise by definition — same threshold the segment filter uses.
|
|
108
|
-
|
|
113
|
+
const sameOID = triOID[prev] === triOID[t];
|
|
114
|
+
// Blend boundary: a cross-surface seam with a BLEND policy on EXACTLY one side
|
|
115
|
+
// is the start/end of a fillet band — draw it even when tangent (the band's
|
|
116
|
+
// extent must be readable). It also bypasses the thin-triangle gate below:
|
|
117
|
+
// simplify() cannot collapse the boolean's slivers ACROSS the blend/base run
|
|
118
|
+
// boundary, so half the seam's edges border a sliver, and gating them dashed
|
|
119
|
+
// the ring — those slivers ride within microns OF the seam curve, so their
|
|
120
|
+
// long edges redraw it rather than add noise (the MIN_EDGE segment-length
|
|
121
|
+
// filter still drops the short ones).
|
|
122
|
+
const boundary = !sameOID &&
|
|
123
|
+
!!polFor(triOID[prev]).boundaryLines !== !!polFor(triOID[t]).boundaryLines;
|
|
124
|
+
if (!boundary && (thin[prev] < MIN_EDGE || thin[t] < MIN_EDGE)) continue;
|
|
109
125
|
const dot = fn[prev * 3] * fn[t * 3] + fn[prev * 3 + 1] * fn[t * 3 + 1] + fn[prev * 3 + 2] * fn[t * 3 + 2];
|
|
110
126
|
// A multi-hole cap triangulation can contain an opposite-wound bridge:
|
|
111
127
|
// its two normals disagree by 180 degrees even though both triangles lie
|
|
112
128
|
// in the same plane. Gate on the unoriented supporting-plane angle first
|
|
113
129
|
// so that triangulation seam never becomes a feature line.
|
|
114
130
|
const bends = Math.abs(dot) < COPLANAR_COS;
|
|
115
|
-
|
|
131
|
+
// Two blend surfaces (a handover along one band) line-draw like ONE surface:
|
|
132
|
+
// the 35° same-surface bar, not the 5° cut-seam bar — a band is many tool
|
|
133
|
+
// surfaces whose overshoot crossings bend a few degrees by construction.
|
|
134
|
+
const bothBlend = !sameOID &&
|
|
135
|
+
!!polFor(triOID[prev]).boundaryLines && !!polFor(triOID[t]).boundaryLines;
|
|
136
|
+
const hard = boundary || (bends && (sameOID || bothBlend
|
|
116
137
|
? polFor(triOID[t]).sameSurfaceLines && dot < cosFor(triOID[t])
|
|
117
|
-
: true);
|
|
138
|
+
: true));
|
|
118
139
|
if (hard) {
|
|
119
140
|
const ai = i * np, bj = j * np;
|
|
120
141
|
const dx = vp[ai] - vp[bj], dy = vp[ai + 1] - vp[bj + 1], dz = vp[ai + 2] - vp[bj + 2];
|
|
@@ -13,9 +13,9 @@ import { offsetRegions } from "./contour-offset.js";
|
|
|
13
13
|
import { finishKernel } from "./kernel-front.js";
|
|
14
14
|
import { meshToStl } from "./mesh-stl.js";
|
|
15
15
|
import { creasedNormals } from "./creased-normals.js";
|
|
16
|
-
import { loftShadingPolicy, SMOOTH } from "./shading-policy.js";
|
|
16
|
+
import { loftShadingPolicy, SMOOTH, BLEND } from "./shading-policy.js";
|
|
17
17
|
import { meshFillet, meshChamfer, UnsupportedEdgeError } from "./mesh-fillet.js";
|
|
18
|
-
import { meshRoundAll } from "./mesh-roundall.js";
|
|
18
|
+
import { meshRoundAll, prismSection, roundAllSegs } from "./mesh-roundall.js";
|
|
19
19
|
import { KernelCapabilityError } from "./errors.js";
|
|
20
20
|
|
|
21
21
|
const PLANE_NORMAL = { XY: [0, 0, 1], XZ: [0, 1, 0], YZ: [1, 0, 0] };
|
|
@@ -123,12 +123,6 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
123
123
|
// degenerate normals poison the crease pass into drawing phantom edge lines).
|
|
124
124
|
// Unsupported edge classes (helical edges, varying dihedral, …) surface as
|
|
125
125
|
// KernelCapabilityError so the framework reroutes that sub-part to OCCT.
|
|
126
|
-
// simplify() will not collapse triangles across run (originalID) boundaries,
|
|
127
|
-
// and the boolean's sliver triangles sit exactly on them — so the result is
|
|
128
|
-
// re-originaled first. That folds every surface into one fresh original,
|
|
129
|
-
// which is also the documented B-rep semantic: fillet/chamfer produce new
|
|
130
|
-
// surfaces, so feature-label attribution downstream of the op uses the
|
|
131
|
-
// fallback path (AUTHORING-PARTS.md), and the blend shades SMOOTH.
|
|
132
126
|
const SIMPLIFY_EPS = 1e-4; // 0.1 µm — must exceed the boolean's sliver widths (~2e-5)
|
|
133
127
|
// Debris sweep: where blend tools graze each other or a flank near-tangentially, the
|
|
134
128
|
// boolean can strand a CLOSED femto-component (measured ~1e-8 mm³, 4 triangles) that
|
|
@@ -144,15 +138,96 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
144
138
|
for (const p of parts) { T(p); if (p.volume() >= DEBRIS_VOL) kept.push(p); }
|
|
145
139
|
return kept.length === parts.length ? m : T(Manifold.compose(kept));
|
|
146
140
|
};
|
|
147
|
-
|
|
141
|
+
// Blend surfaces KEEP their originalIDs, and every id the op introduced is
|
|
142
|
+
// registered with the BLEND policy — that is what lets creased-normals draw the
|
|
143
|
+
// band's start/end (a tangent blend↔base seam draws regardless of bend) while
|
|
144
|
+
// blend↔blend handovers along one band stay invisible. This replaced a blanket
|
|
145
|
+
// asOriginal(): folding the result into one fresh original made simplify able to
|
|
146
|
+
// collapse boundary slivers, but it also erased the one distinction the boundary
|
|
147
|
+
// lines need. The trade is stated honestly: simplify() cannot collapse across the
|
|
148
|
+
// surviving run boundaries, so seam-adjacent slivers persist as triangles — they
|
|
149
|
+
// are sub-MIN_EDGE thin, so the line pass gates them, and closed sliver DEBRIS is
|
|
150
|
+
// swept above; the visible cost is a modestly larger mesh, bounded by
|
|
151
|
+
// mesh-fillet-perf.test.js. Shading is unchanged (BLEND shades like SMOOTH), and
|
|
152
|
+
// label() on a blend result already handles a mixed-original mesh (the majority
|
|
153
|
+
// vote below).
|
|
154
|
+
const runOids = (mm) => { const g = mm.getMesh(); const s = new Set(g.runOriginalID); g.delete?.(); return s; };
|
|
155
|
+
const meshCadOp = (op, baseM, run) => {
|
|
148
156
|
try {
|
|
149
|
-
|
|
157
|
+
const raw = run()._m;
|
|
158
|
+
const baseOids = runOids(baseM);
|
|
159
|
+
for (const oid of runOids(raw)) if (!baseOids.has(oid)) oidPolicies.set(oid, BLEND);
|
|
160
|
+
// debris sweep AFTER simplify: the boolean's own femto-components are joined by
|
|
161
|
+
// ones simplify() itself pinches off while collapsing seam slivers (measured:
|
|
162
|
+
// 4-triangle atto-scale bubbles, one with negative volume, after a rim fillet
|
|
163
|
+
// over vertical-fillet bands) — sweeping first missed those
|
|
164
|
+
return dropDebris(T(raw.simplify(SIMPLIFY_EPS)));
|
|
150
165
|
} catch (e) {
|
|
151
166
|
if (e instanceof UnsupportedEdgeError) throw new KernelCapabilityError(`${op}: ${e.message}`);
|
|
152
167
|
throw e;
|
|
153
168
|
}
|
|
154
169
|
};
|
|
155
170
|
|
|
171
|
+
// roundAll prism fast path. Ball close-then-open via native Minkowski is
|
|
172
|
+
// seconds-per-thousand-triangles (a text-outline backing measured 30+ s), but on
|
|
173
|
+
// a Z-prism the SAME morphology decomposes: the 2-D close-open of the
|
|
174
|
+
// cross-section (Clipper2 offsets +r, -2r, +r) supplies the wall melting and
|
|
175
|
+
// hole sealing, and a selector-free fillet of the re-extruded section at r
|
|
176
|
+
// supplies every rounded surface — vertical edges, both rims, and the corner
|
|
177
|
+
// treatments. Returns a raw (tracked) manifold, or null to
|
|
178
|
+
// keep the reference morphology — the fast path may only ever SUBSTITUTE for
|
|
179
|
+
// it: any doubt (not a prism, plate too thin, everything melted, a fillet
|
|
180
|
+
// refusal) falls back rather than widening or narrowing what roundAll accepts,
|
|
181
|
+
// and roundAll must never surface NEEDS_OCCT (it is its own reference
|
|
182
|
+
// implementation), which is why the whole attempt is fenced by a bare catch.
|
|
183
|
+
const prismRoundAllFast = (m, mHash, r) => {
|
|
184
|
+
let sect = null;
|
|
185
|
+
try {
|
|
186
|
+
sect = prismSection(wasm, m);
|
|
187
|
+
if (!sect) return null;
|
|
188
|
+
const { cs, z0, h: height } = sect;
|
|
189
|
+
// the erosion consumes the whole plate below 2r, and just above it the two
|
|
190
|
+
// rim bands graze each other — both belong to the reference morphology
|
|
191
|
+
if (!(height > 2 * r * 1.05)) return null;
|
|
192
|
+
// MITER joins on all three offsets, deliberately: the true morphology mints
|
|
193
|
+
// outline corners of radius exactly r, and a rim fillet of radius r over an
|
|
194
|
+
// r-radius corner pinches its top tangent contour to a point — the planar
|
|
195
|
+
// sweep refuses, structurally. Miter keeps every corner SHARP instead, and
|
|
196
|
+
// the selector-free fillet below performs ALL the rounding: convex vertical
|
|
197
|
+
// edges (cutters at r — the same solid the round join would have produced),
|
|
198
|
+
// concave verticals (fillers), both rims, and the corner treatments. Melt
|
|
199
|
+
// and seal thresholds stay exact on straight stretches; corner-local
|
|
200
|
+
// thresholds and silhouettes differ from the ball morphology by the rim
|
|
201
|
+
// fillet's own corner tolerance (~0.25·r) — the documented corner trade.
|
|
202
|
+
let cur = null;
|
|
203
|
+
try {
|
|
204
|
+
for (const delta of [r, -2 * r, r]) {
|
|
205
|
+
const next = (cur ?? cs).offset(delta, "Miter", 2);
|
|
206
|
+
const cleaned = next.simplify(1e-6);
|
|
207
|
+
next.delete?.();
|
|
208
|
+
cur?.delete?.();
|
|
209
|
+
cur = cleaned;
|
|
210
|
+
}
|
|
211
|
+
if (!(cur.area() > 0)) return null; // everything melted — reference path owns the empty result
|
|
212
|
+
let base = T(Manifold.extrude(cur, height));
|
|
213
|
+
if (z0 !== 0) base = T(base.translate([0, 0, z0]));
|
|
214
|
+
const wrapped = wrap(base, h("roundAllPrismBase", mHash, r, quality));
|
|
215
|
+
// selector-free: every sharp edge of the mitered prism gets its radius here
|
|
216
|
+
const filleted = wrapped.fillet(r);
|
|
217
|
+
// decouple from the fillet cache's pin: cached() will pin the object this
|
|
218
|
+
// returns under the roundAll hash, and one WASM object must never sit
|
|
219
|
+
// under two cache entries (double-dispose on eviction)
|
|
220
|
+
return T(filleted._m.asOriginal());
|
|
221
|
+
} finally {
|
|
222
|
+
cur?.delete?.();
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
} finally {
|
|
227
|
+
sect?.cs?.delete?.();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
156
231
|
const wrap = (m, hash) => addSugar({
|
|
157
232
|
_m: m,
|
|
158
233
|
_hash: hash,
|
|
@@ -161,13 +236,13 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
161
236
|
if (typeof selector === "function") throw new KernelCapabilityError("fillet: function selectors need the OCCT backend");
|
|
162
237
|
if (r === 0) return wrap(m, hash); // contract: zero magnitude is the identity
|
|
163
238
|
return cached(h("fillet", hash, r, selector ?? null, segs), () =>
|
|
164
|
-
meshCadOp("fillet", () => meshFillet(kernel, wrap(m, hash), { r, edges: selector, segs })));
|
|
239
|
+
meshCadOp("fillet", m, () => meshFillet(kernel, wrap(m, hash), { r, edges: selector, segs })));
|
|
165
240
|
},
|
|
166
241
|
chamfer: (d, selector) => {
|
|
167
242
|
if (typeof selector === "function") throw new KernelCapabilityError("chamfer: function selectors need the OCCT backend");
|
|
168
243
|
if (d === 0) return wrap(m, hash);
|
|
169
244
|
return cached(h("chamfer", hash, d, selector ?? null, segs), () =>
|
|
170
|
-
meshCadOp("chamfer", () => meshChamfer(kernel, wrap(m, hash), { d, edges: selector, segs })));
|
|
245
|
+
meshCadOp("chamfer", m, () => meshChamfer(kernel, wrap(m, hash), { d, edges: selector, segs })));
|
|
171
246
|
},
|
|
172
247
|
roundAll: (r) => {
|
|
173
248
|
if (r === 0) return wrap(m, hash); // contract: zero magnitude is the identity
|
|
@@ -175,7 +250,8 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
175
250
|
// per-quality kernel, so it can never collide across tiers (the OCCT twin
|
|
176
251
|
// key omits it for the same reason); it just spells out that the ball
|
|
177
252
|
// tessellation, and so the result, is tier-dependent.
|
|
178
|
-
return cached(h("roundAll", hash, r, quality), () =>
|
|
253
|
+
return cached(h("roundAll", hash, r, quality), () =>
|
|
254
|
+
prismRoundAllFast(m, hash, r) ?? T(meshRoundAll(wasm, m, r, quality)));
|
|
179
255
|
},
|
|
180
256
|
// batch difference: first minus the union of the rest, evaluated as one boolean
|
|
181
257
|
// tree — no materialized intermediate union (the unionRaw memory note applies)
|
|
@@ -192,6 +268,49 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
192
268
|
label: (name) => {
|
|
193
269
|
const lh = h("label", hash, name);
|
|
194
270
|
return cache.lookup(lh, () => {
|
|
271
|
+
// Blend-aware re-stamp. If this mesh carries blend surfaces (the boundaryLines
|
|
272
|
+
// policy), one asOriginal() would fold band and base into a single surface and
|
|
273
|
+
// erase the band-boundary overlay on exactly the solids parts label — every
|
|
274
|
+
// real part labels its top-level solids. Re-stamp as TWO reserved ids instead,
|
|
275
|
+
// base and blend: the label covers both (same string → one feature entry), the
|
|
276
|
+
// distinction survives labeling and every later boolean, and reserved ids are
|
|
277
|
+
// fresh so cached-solid reuse under another label cannot collide — the same
|
|
278
|
+
// guarantee asOriginal() gives the plain path below.
|
|
279
|
+
const g0 = m.getMesh();
|
|
280
|
+
const isBlend = (oid) => !!oidPolicies.get(oid)?.boundaryLines;
|
|
281
|
+
const oids0 = new Set(g0.runOriginalID);
|
|
282
|
+
if ([...oids0].some(isBlend)) {
|
|
283
|
+
const baseId = Manifold.reserveIDs(2), blendId = baseId + 1;
|
|
284
|
+
// base-group policy: the same triangle-weighted majority vote as the plain
|
|
285
|
+
// path, but over the NON-blend runs only — blend runs would elect BLEND for
|
|
286
|
+
// the base group and flag BOTH sides of every boundary seam, which is
|
|
287
|
+
// exactly the no-line state this path exists to avoid.
|
|
288
|
+
const ri = g0.runIndex, roid = g0.runOriginalID;
|
|
289
|
+
const weightByKey = new Map();
|
|
290
|
+
let bestWeight = -1, basePol;
|
|
291
|
+
for (let r = 0; r < roid.length; r++) {
|
|
292
|
+
if (isBlend(roid[r])) continue;
|
|
293
|
+
const pol = oidPolicies.get(roid[r]) ?? SMOOTH;
|
|
294
|
+
const key = `${pol.creaseAngle}/${pol.sameSurfaceLines}/${!!pol.boundaryLines}`;
|
|
295
|
+
const weight = (weightByKey.get(key) || 0) + (ri[r + 1] / 3 - ri[r] / 3);
|
|
296
|
+
weightByKey.set(key, weight);
|
|
297
|
+
const better = weight > bestWeight || (weight === bestWeight && !pol.sameSurfaceLines && basePol?.sameSurfaceLines);
|
|
298
|
+
if (better) { bestWeight = weight; basePol = pol; }
|
|
299
|
+
}
|
|
300
|
+
g0.runOriginalID = Uint32Array.from(roid, (o2) => (isBlend(o2) ? blendId : baseId));
|
|
301
|
+
const o = T(new Manifold(g0));
|
|
302
|
+
g0.delete?.();
|
|
303
|
+
featureLabels.set(baseId, name);
|
|
304
|
+
featureLabels.set(blendId, name);
|
|
305
|
+
oidPolicies.set(blendId, BLEND);
|
|
306
|
+
if (basePol !== undefined) oidPolicies.set(baseId, basePol);
|
|
307
|
+
return { value: wrap(o, lh), pin: o, dispose: () => {
|
|
308
|
+
featureLabels.delete(baseId); featureLabels.delete(blendId);
|
|
309
|
+
oidPolicies.delete(baseId); oidPolicies.delete(blendId);
|
|
310
|
+
o.delete?.();
|
|
311
|
+
} };
|
|
312
|
+
}
|
|
313
|
+
g0.delete?.();
|
|
195
314
|
const prevId = typeof m.originalID === "function" ? m.originalID() : -1;
|
|
196
315
|
const o = T(m.asOriginal());
|
|
197
316
|
const id = o.originalID();
|
|
@@ -231,7 +350,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
231
350
|
let bestWeight = -1, bestPol;
|
|
232
351
|
for (let r = 0; r < roid.length; r++) {
|
|
233
352
|
const pol = oidPolicies.get(roid[r]) ?? SMOOTH;
|
|
234
|
-
const key = `${pol.creaseAngle}/${pol.sameSurfaceLines}`;
|
|
353
|
+
const key = `${pol.creaseAngle}/${pol.sameSurfaceLines}/${!!pol.boundaryLines}`;
|
|
235
354
|
const weight = (weightByKey.get(key) || 0) + (ri[r + 1] / 3 - ri[r] / 3);
|
|
236
355
|
weightByKey.set(key, weight);
|
|
237
356
|
const better = weight > bestWeight || (weight === bestWeight && !pol.sameSurfaceLines && bestPol?.sameSurfaceLines);
|
|
@@ -81,3 +81,46 @@ export function meshRoundAll(wasm, m, r, quality) {
|
|
|
81
81
|
sph2R.delete?.();
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Prism detection for the fast path (manifold-backend.js). The Minkowski chain
|
|
87
|
+
// above is seconds-per-thousand-triangles, but roundAll's expensive real-world
|
|
88
|
+
// inputs are almost always Z-prisms (text backings, plates, extruded outlines) —
|
|
89
|
+
// and on a prism the ball morphology decomposes exactly into the 2-D disk
|
|
90
|
+
// morphology of the cross-section plus rim fillets, all of which are fast. This
|
|
91
|
+
// function answers "is m a Z-prism, and what is its constant cross-section?"
|
|
92
|
+
//
|
|
93
|
+
// Detection is deliberately behavioral, not structural: three slices must have
|
|
94
|
+
// equal area AND vanishing symmetric difference (a sheared prism has equal-area
|
|
95
|
+
// TRANSLATED sections — the subtract catches it), and the solid's volume must
|
|
96
|
+
// equal section × height (a bulge parked between the slice planes would pass the
|
|
97
|
+
// slice checks alone). Any failure returns null and the caller keeps the
|
|
98
|
+
// reference morphology — the fast path may only ever substitute, never widen.
|
|
99
|
+
//
|
|
100
|
+
// On success the returned CrossSection is the CALLER's to delete.
|
|
101
|
+
export function prismSection(wasm, m, relTol = 1e-4) {
|
|
102
|
+
const bb = m.boundingBox();
|
|
103
|
+
const z0 = bb.min[2], h = bb.max[2] - z0;
|
|
104
|
+
if (!(h > 0)) return null;
|
|
105
|
+
const volume = m.volume();
|
|
106
|
+
if (!(volume > 0)) return null;
|
|
107
|
+
const slices = [0.25, 0.5, 0.75].map((t) => m.slice(z0 + t * h));
|
|
108
|
+
try {
|
|
109
|
+
const area = slices[1].area();
|
|
110
|
+
if (!(area > 0)) return null;
|
|
111
|
+
for (const s of slices) if (Math.abs(s.area() - area) > relTol * area) return null;
|
|
112
|
+
for (const s of [slices[0], slices[2]]) {
|
|
113
|
+
const d1 = slices[1].subtract(s), d2 = s.subtract(slices[1]);
|
|
114
|
+
const diff = d1.area() + d2.area();
|
|
115
|
+
d1.delete?.();
|
|
116
|
+
d2.delete?.();
|
|
117
|
+
if (diff > relTol * area) return null;
|
|
118
|
+
}
|
|
119
|
+
if (Math.abs(volume - area * h) > 10 * relTol * area * h) return null;
|
|
120
|
+
const cs = slices[1];
|
|
121
|
+
slices[1] = null; // ownership moves to the caller
|
|
122
|
+
return { cs, z0, h };
|
|
123
|
+
} finally {
|
|
124
|
+
for (const s of slices) s?.delete?.();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
export const SMOOTH = Object.freeze({ creaseAngle: 35, sameSurfaceLines: true });
|
|
10
10
|
export const FACETED = Object.freeze({ creaseAngle: 10, sameSurfaceLines: false });
|
|
11
|
+
// A blend surface (fillet/chamfer band). `boundaryLines` widens the CROSS-surface
|
|
12
|
+
// rule for it: a seam between a blend and a NON-blend surface draws regardless of
|
|
13
|
+
// bend — the band's start/end are tangent (~0°) and would otherwise be invisible,
|
|
14
|
+
// leaving the fillet's extent unreadable in the overlay. A seam between TWO blend
|
|
15
|
+
// surfaces keeps the ordinary bend rule (exactly-one-side semantics), so the
|
|
16
|
+
// handover seams along one band — tool splits, corner arcs continuing a sweep —
|
|
17
|
+
// stay invisible while real mitre crossings still draw.
|
|
18
|
+
export const BLEND = Object.freeze({ creaseAngle: 35, sameSurfaceLines: true, boundaryLines: true });
|
|
11
19
|
|
|
12
20
|
export const COPLANAR_ANGLE = 5; // deg — cut seams bending less than this are coplanar: no line
|
|
13
21
|
export const TANGENT_ANGLE = 5; // deg — B-rep edges whose faces agree within this are tangent: no line
|