partforge 0.66.0 → 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,15 +2241,19 @@ 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
|
|
2245
|
-
|
|
2246
|
-
is
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
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.
|
|
2253
2257
|
|
|
2254
2258
|
Rules of thumb:
|
|
2255
2259
|
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import { meshToStl } from "./mesh-stl.js";
|
|
|
15
15
|
import { creasedNormals } from "./creased-normals.js";
|
|
16
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] };
|
|
@@ -168,6 +168,66 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
168
168
|
}
|
|
169
169
|
};
|
|
170
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
|
+
|
|
171
231
|
const wrap = (m, hash) => addSugar({
|
|
172
232
|
_m: m,
|
|
173
233
|
_hash: hash,
|
|
@@ -190,7 +250,8 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
190
250
|
// per-quality kernel, so it can never collide across tiers (the OCCT twin
|
|
191
251
|
// key omits it for the same reason); it just spells out that the ball
|
|
192
252
|
// tessellation, and so the result, is tier-dependent.
|
|
193
|
-
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)));
|
|
194
255
|
},
|
|
195
256
|
// batch difference: first minus the union of the rest, evaluated as one boolean
|
|
196
257
|
// tree — no materialized intermediate union (the unionRaw memory note applies)
|
|
@@ -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
|
+
}
|