partforge 0.144.0 → 0.144.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/package.json
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
// { kind: "spine", pts, closed, r }
|
|
23
23
|
// general chain (mesh-fillet generalTool): the
|
|
24
24
|
// ball-centre polyline itself; r is the radius
|
|
25
|
+
// { kind: "planes", planes: [{ n, d }], tol }
|
|
26
|
+
// roundAll's flat faces (mesh-roundall.js
|
|
27
|
+
// flatFacePlanes): a point within tol of one plane
|
|
28
|
+
// (n·x = d) takes its normal; anywhere else — the
|
|
29
|
+
// rounded band off its seams — there is none, and
|
|
30
|
+
// creased-normals keeps the facet average
|
|
25
31
|
//
|
|
26
32
|
// A path's polyline usually STANDS FOR a smooth curve (a circle's rim, an outline's
|
|
27
33
|
// rounded corner): its per-segment cylinders would shade faceted around the curve,
|
|
@@ -105,10 +111,32 @@ export function mapSurface(desc, M) {
|
|
|
105
111
|
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
112
|
return { kind: "spine", closed: desc.closed, pts: desc.pts.map((p) => applyPt(M, p)), r: desc.r * s };
|
|
107
113
|
}
|
|
114
|
+
case "planes": {
|
|
115
|
+
const s = Math.cbrt(Math.abs(
|
|
116
|
+
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])));
|
|
117
|
+
return { kind: "planes", tol: desc.tol * s, planes: desc.planes.map(({ n, d }) => {
|
|
118
|
+
const n2 = norm(applyDir(M, n)) ?? n;
|
|
119
|
+
const p = applyPt(M, [n[0] * d, n[1] * d, n[2] * d]);
|
|
120
|
+
return { n: n2, d: n2[0] * p[0] + n2[1] * p[1] + n2[2] * p[2] };
|
|
121
|
+
}) };
|
|
122
|
+
}
|
|
108
123
|
default: throw new Error(`blend surface: unknown kind ${desc.kind}`);
|
|
109
124
|
}
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
// The plane x lies on, or null — off every plane, or on two at once (where two
|
|
128
|
+
// flat faces would meet, which a rounded result never shows, so no answer is right).
|
|
129
|
+
function planeNormal(desc, x) {
|
|
130
|
+
let hit = null;
|
|
131
|
+
for (const pl of desc.planes) {
|
|
132
|
+
const n = pl.n;
|
|
133
|
+
if (Math.abs(n[0] * x[0] + n[1] * x[1] + n[2] * x[2] - pl.d) > desc.tol) continue;
|
|
134
|
+
if (hit) return null;
|
|
135
|
+
hit = n;
|
|
136
|
+
}
|
|
137
|
+
return hit;
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
// Nearest spine point to x, in the descriptor's own frame.
|
|
113
141
|
function spinePoint(desc, x) {
|
|
114
142
|
switch (desc.kind) {
|
|
@@ -240,6 +268,7 @@ function nearestSegment(desc, x) {
|
|
|
240
268
|
// Unit spine→point direction at x in the descriptor's frame, or null where undefined.
|
|
241
269
|
export function surfaceNormal(desc, x) {
|
|
242
270
|
if (desc.kind === "path") return pathNormal(desc, x);
|
|
271
|
+
if (desc.kind === "planes") return planeNormal(desc, x);
|
|
243
272
|
if (desc.kind === "spine") {
|
|
244
273
|
const { bi, bt } = nearestSegment(desc, x);
|
|
245
274
|
if (bi < 0) return null;
|
|
@@ -18,8 +18,8 @@ import { meshToStl } from "./mesh-stl.js";
|
|
|
18
18
|
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
|
-
import { affineAt, invertAffine, mapSurface } from "./blend-surfaces.js";
|
|
22
|
-
import { meshRoundAll, prismSection, roundAllSegs } from "./mesh-roundall.js";
|
|
21
|
+
import { affineAt, IDENTITY, invertAffine, mapSurface } from "./blend-surfaces.js";
|
|
22
|
+
import { flatFacePlanes, meshRoundAll, prismSection, roundAllSegs } from "./mesh-roundall.js";
|
|
23
23
|
import { SEGS, circleSegs, doubleCurvatureSegs } from "./circle-segs.js";
|
|
24
24
|
import { checkBooleanResult } from "./boolean-gate.js";
|
|
25
25
|
import { KernelCapabilityError } from "./errors.js";
|
|
@@ -294,12 +294,20 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
294
294
|
// round-trip, so the descriptor's original frame stays valid). Folding the band
|
|
295
295
|
// into a single blend id, as this used to, lost every descriptor with it: a labeled
|
|
296
296
|
// fillet shaded with facet normals again.
|
|
297
|
+
//
|
|
298
|
+
// "Blend" here is any surface that must keep an id of its own: a fillet band (the
|
|
299
|
+
// boundaryLines policy) or a surface carrying an analytic descriptor at all —
|
|
300
|
+
// roundAll's flat-face planes ride on a SMOOTH surface, and folding that into the
|
|
301
|
+
// base id would drop them and bring the flat-face shading gradient back on every
|
|
302
|
+
// labeled roundAll. Each keeps the policy it had (none, for the roundAll surface).
|
|
303
|
+
const keepsOwnId = (oid) => !!oidPolicies.get(oid)?.boundaryLines || blendSurfaces.has(oid);
|
|
297
304
|
const reserveBlendRestamp = (oids) => {
|
|
298
|
-
const blends = [...oids].filter(
|
|
305
|
+
const blends = [...oids].filter(keepsOwnId);
|
|
299
306
|
const baseId = Manifold.reserveIDs(1 + blends.length);
|
|
300
307
|
const blendIdFor = new Map(blends.map((oid, i) => [oid, baseId + 1 + i]));
|
|
301
308
|
for (const [oid, id2] of blendIdFor) {
|
|
302
|
-
oidPolicies.
|
|
309
|
+
const pol = oidPolicies.get(oid);
|
|
310
|
+
if (pol !== undefined) oidPolicies.set(id2, pol);
|
|
303
311
|
const surf = blendSurfaces.get(oid);
|
|
304
312
|
if (surf) blendSurfaces.set(id2, surf);
|
|
305
313
|
}
|
|
@@ -405,7 +413,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
405
413
|
const isBlend = (oid) => !!oidPolicies.get(oid)?.boundaryLines;
|
|
406
414
|
if (![...new Set(g0.runOriginalID)].some(isBlend)) return T(filleted._m.asOriginal());
|
|
407
415
|
const { baseId, blendIdFor } = reserveBlendRestamp(new Set(g0.runOriginalID));
|
|
408
|
-
g0.runOriginalID = Uint32Array.from(g0.runOriginalID, (o) =>
|
|
416
|
+
g0.runOriginalID = Uint32Array.from(g0.runOriginalID, (o) => blendIdFor.get(o) ?? baseId);
|
|
409
417
|
// The constructor re-welds, and that can pinch a latent sliver off a
|
|
410
418
|
// component as fresh femto-debris (the same rebirth dropDebris's own
|
|
411
419
|
// compose() loop guards against) — sweep the reconstruction too.
|
|
@@ -423,6 +431,39 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
423
431
|
}
|
|
424
432
|
};
|
|
425
433
|
|
|
434
|
+
// The reference roundAll's flat faces (mesh-roundall.js flatFacePlanes): the
|
|
435
|
+
// input's flat faces, which the chain returns exactly to their planes, become a
|
|
436
|
+
// "planes" descriptor on the result's surface so creased-normals shades them —
|
|
437
|
+
// and the band's seams beside them — with the plane's own normal. The tolerance
|
|
438
|
+
// sits well under the first ring of band vertices off each face (the same ring
|
|
439
|
+
// meshRoundAll's simplify tolerance protects), so only the face and its seam
|
|
440
|
+
// vertices are on a plane. Keyed on geometry like every descriptor (see
|
|
441
|
+
// blendSurfaces above), so it is never removed with the cache entry.
|
|
442
|
+
const markFlatFaces = (input, out, r) => {
|
|
443
|
+
const gi = input.getMesh();
|
|
444
|
+
let planes;
|
|
445
|
+
try {
|
|
446
|
+
planes = flatFacePlanes(gi, { policyFor: (oid) => oidPolicies.get(oid) });
|
|
447
|
+
} finally {
|
|
448
|
+
gi.delete?.();
|
|
449
|
+
}
|
|
450
|
+
if (!planes.length) return out;
|
|
451
|
+
const segs = roundAllSegs(2 * r, quality);
|
|
452
|
+
const tol = Math.min(1e-3, 0.25 * r * (1 - Math.cos((2 * Math.PI) / segs)));
|
|
453
|
+
const go = out.getMesh();
|
|
454
|
+
try {
|
|
455
|
+
const roid = go.runOriginalID, rt = go.runTransform;
|
|
456
|
+
for (let i = 0; i < roid.length; i++) {
|
|
457
|
+
// a fresh original carries no runTransform at all — identity, as creased-normals reads it
|
|
458
|
+
const inv = rt && rt.length >= (i + 1) * 12 ? invertAffine(affineAt(rt, i)) : IDENTITY;
|
|
459
|
+
if (inv) blendSurfaces.set(roid[i], mapSurface({ kind: "planes", planes, tol }, inv));
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
go.delete?.();
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
};
|
|
466
|
+
|
|
426
467
|
// Replay a recorded transform chain onto a solid. Each record maps back to the op
|
|
427
468
|
// that produced it, so the replayed solid rebuilds the same chain on its own canon.
|
|
428
469
|
const replay = (solid, chain) => chain.reduce(
|
|
@@ -503,7 +544,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
503
544
|
// key omits it for the same reason); it just spells out that the ball
|
|
504
545
|
// tessellation, and so the result, is tier-dependent.
|
|
505
546
|
return cached(h("roundAll", hash, r, quality), () =>
|
|
506
|
-
prismRoundAllFast(m, hash, r) ?? T(meshRoundAll(wasm, m, r, quality)));
|
|
547
|
+
prismRoundAllFast(m, hash, r) ?? markFlatFaces(m, T(meshRoundAll(wasm, m, r, quality)), r));
|
|
507
548
|
},
|
|
508
549
|
// batch difference: first minus the union of the rest, evaluated as one boolean
|
|
509
550
|
// tree — no materialized intermediate union (the unionRaw memory note applies)
|
|
@@ -538,7 +579,6 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
538
579
|
// fresh so cached-solid reuse under another label cannot collide — the same
|
|
539
580
|
// guarantee asOriginal() gives the plain path below.
|
|
540
581
|
const g0 = m.getMesh();
|
|
541
|
-
const isBlend = (oid) => !!oidPolicies.get(oid)?.boundaryLines;
|
|
542
582
|
const oids0 = new Set(g0.runOriginalID);
|
|
543
583
|
// Sector-aware re-stamp: a provenance-sectored loft (the `sector` policy
|
|
544
584
|
// marker) must keep every run's identity — folding them (either the plain
|
|
@@ -561,14 +601,16 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
561
601
|
featureLabels.set(newId, name);
|
|
562
602
|
const pol = oidPolicies.get(oldId);
|
|
563
603
|
if (pol !== undefined) oidPolicies.set(newId, pol);
|
|
604
|
+
const surf = blendSurfaces.get(oldId); // analytic normals ride the 1:1 stamp too
|
|
605
|
+
if (surf) blendSurfaces.set(newId, surf);
|
|
564
606
|
setIds.push(newId);
|
|
565
607
|
}
|
|
566
608
|
return { value: wrap(o, lh), pin: o, dispose: () => {
|
|
567
|
-
for (const id2 of setIds) { featureLabels.delete(id2); oidPolicies.delete(id2); }
|
|
609
|
+
for (const id2 of setIds) { featureLabels.delete(id2); oidPolicies.delete(id2); blendSurfaces.delete(id2); }
|
|
568
610
|
o.delete?.();
|
|
569
611
|
} };
|
|
570
612
|
}
|
|
571
|
-
if ([...oids0].some(
|
|
613
|
+
if ([...oids0].some(keepsOwnId)) {
|
|
572
614
|
const { baseId, blendIdFor, blendIds } = reserveBlendRestamp(oids0);
|
|
573
615
|
// base-group policy: the same triangle-weighted majority vote as the plain
|
|
574
616
|
// path, but over the NON-blend runs only — blend runs would elect BLEND for
|
|
@@ -578,7 +620,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
578
620
|
const weightByKey = new Map();
|
|
579
621
|
let bestWeight = -1, basePol;
|
|
580
622
|
for (let r = 0; r < roid.length; r++) {
|
|
581
|
-
if (
|
|
623
|
+
if (blendIdFor.has(roid[r])) continue;
|
|
582
624
|
const pol = oidPolicies.get(roid[r]) ?? SMOOTH;
|
|
583
625
|
const key = `${pol.creaseAngle}/${pol.sameSurfaceLines}/${!!pol.boundaryLines}`;
|
|
584
626
|
const weight = (weightByKey.get(key) || 0) + (ri[r + 1] / 3 - ri[r] / 3);
|
|
@@ -586,7 +628,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
586
628
|
const better = weight > bestWeight || (weight === bestWeight && !pol.sameSurfaceLines && basePol?.sameSurfaceLines);
|
|
587
629
|
if (better) { bestWeight = weight; basePol = pol; }
|
|
588
630
|
}
|
|
589
|
-
g0.runOriginalID = Uint32Array.from(roid, (o2) =>
|
|
631
|
+
g0.runOriginalID = Uint32Array.from(roid, (o2) => blendIdFor.get(o2) ?? baseId);
|
|
590
632
|
const o = T(new Manifold(g0));
|
|
591
633
|
g0.delete?.();
|
|
592
634
|
featureLabels.set(baseId, name);
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// the two facetings still meets the tier's sagitta tolerance.
|
|
30
30
|
|
|
31
31
|
import { segsForSagitta } from "./circle-segs.js";
|
|
32
|
+
import { SMOOTH, cosDeg } from "./shading-policy.js";
|
|
32
33
|
|
|
33
34
|
// Sphere tessellation from the facet sagitta r·(1 − cos(π/segs)): pick the
|
|
34
35
|
// fewest segments that keep it under the quality tier's tolerance — its own
|
|
@@ -83,6 +84,121 @@ export function meshRoundAll(wasm, m, r, quality) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Flat-face shading. The reference path's result is one fresh surface, shaded
|
|
89
|
+
// SMOOTH (35° crease), and the rounding ball's facets bend far less than that, so
|
|
90
|
+
// a flat face used to share its corner normals with the band facets around it.
|
|
91
|
+
// The vertex sum is unweighted, so a large face split into two triangles — a
|
|
92
|
+
// sloped palm plate's top — took a several-degree tilt at each corner and showed
|
|
93
|
+
// it as a shading gradient across the whole face (cloud feedback #167).
|
|
94
|
+
//
|
|
95
|
+
// The cure rests on the invariant above: the chain returns every planar face of
|
|
96
|
+
// the input to its own plane. So the input's flat faces, found here, say exactly
|
|
97
|
+
// where the result is flat; the backend registers them as an analytic "planes"
|
|
98
|
+
// descriptor (blend-surfaces.js), and creased-normals then shades every vertex on
|
|
99
|
+
// one of those planes — the band's seam vertices too, since the band is tangent
|
|
100
|
+
// there — with the plane's normal, the same way a fillet band's seam agrees with
|
|
101
|
+
// its face.
|
|
102
|
+
//
|
|
103
|
+
// "Flat" means flat the way the INPUT shades it: a coplanar group counts only if
|
|
104
|
+
// every edge bounding it is one creased-normals would shade hard (a bend past the
|
|
105
|
+
// surface's crease angle, or a cut seam). A rod's wall facets, or a slot's sides
|
|
106
|
+
// running tangentially into its round ends, meet their neighbours softly — the
|
|
107
|
+
// input already shades them as part of a curved surface, and pinning them to
|
|
108
|
+
// their own planes would facet the rounded result.
|
|
109
|
+
//
|
|
110
|
+
// `g` is a MeshGL (or the same fields on plain arrays); `policyFor(oid)` returns
|
|
111
|
+
// that surface's shading policy. Returns [{ n, d }] with n·x = d on the plane.
|
|
112
|
+
const PLANE_COS = cosDeg(0.05); // coplanar neighbours: a boolean split, not a bend
|
|
113
|
+
export function flatFacePlanes(g, { policyFor = () => SMOOTH } = {}) {
|
|
114
|
+
const np = g.numProp, vp = g.vertProperties, tris = g.triVerts;
|
|
115
|
+
const nTri = (tris.length / 3) | 0, nVert = (vp.length / np) | 0;
|
|
116
|
+
const remap = new Uint32Array(nVert);
|
|
117
|
+
for (let i = 0; i < nVert; i++) remap[i] = i;
|
|
118
|
+
const mf = g.mergeFromVert, mt = g.mergeToVert;
|
|
119
|
+
if (mf && mt) for (let i = 0; i < mf.length; i++) remap[mf[i]] = mt[i];
|
|
120
|
+
const triOID = new Uint32Array(nTri);
|
|
121
|
+
const ri = g.runIndex, roid = g.runOriginalID;
|
|
122
|
+
if (ri && roid) for (let r = 0; r < roid.length; r++)
|
|
123
|
+
for (let t = ri[r] / 3; t < ri[r + 1] / 3; t++) triOID[t] = roid[r];
|
|
124
|
+
|
|
125
|
+
// facet normals, areas and centroids; a degenerate triangle has no normal and
|
|
126
|
+
// takes no part — it neither joins a face nor disqualifies one
|
|
127
|
+
const fn = new Float64Array(nTri * 3), area = new Float64Array(nTri), cen = new Float64Array(nTri * 3);
|
|
128
|
+
let span = 0;
|
|
129
|
+
for (let t = 0; t < nTri; t++) {
|
|
130
|
+
const a = tris[t * 3] * np, b = tris[t * 3 + 1] * np, c = tris[t * 3 + 2] * np;
|
|
131
|
+
const ux = vp[b] - vp[a], uy = vp[b + 1] - vp[a + 1], uz = vp[b + 2] - vp[a + 2];
|
|
132
|
+
const vx = vp[c] - vp[a], vy = vp[c + 1] - vp[a + 1], vz = vp[c + 2] - vp[a + 2];
|
|
133
|
+
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
|
|
134
|
+
const L = Math.hypot(nx, ny, nz);
|
|
135
|
+
area[t] = L / 2;
|
|
136
|
+
if (L > 0) { fn[t * 3] = nx / L; fn[t * 3 + 1] = ny / L; fn[t * 3 + 2] = nz / L; }
|
|
137
|
+
for (let k = 0; k < 3; k++) cen[t * 3 + k] = (vp[a + k] + vp[b + k] + vp[c + k]) / 3;
|
|
138
|
+
span = Math.max(span, Math.abs(vp[a]), Math.abs(vp[a + 1]), Math.abs(vp[a + 2]));
|
|
139
|
+
}
|
|
140
|
+
const live = (t) => area[t] > 1e-9;
|
|
141
|
+
const dot = (t, u) => fn[t * 3] * fn[u * 3] + fn[t * 3 + 1] * fn[u * 3 + 1] + fn[t * 3 + 2] * fn[u * 3 + 2];
|
|
142
|
+
|
|
143
|
+
// edge → incident triangles, on welded vertex ids
|
|
144
|
+
const edges = new Map();
|
|
145
|
+
for (let t = 0; t < nTri; t++) for (let k = 0; k < 3; k++) {
|
|
146
|
+
const p = remap[tris[t * 3 + k]], q = remap[tris[t * 3 + ((k + 1) % 3)]];
|
|
147
|
+
const key = p < q ? p * nVert + q : q * nVert + p;
|
|
148
|
+
const list = edges.get(key);
|
|
149
|
+
if (list) list.push(t); else edges.set(key, [t]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// coplanar groups (union-find over shared edges)
|
|
153
|
+
const parent = new Int32Array(nTri);
|
|
154
|
+
for (let t = 0; t < nTri; t++) parent[t] = t;
|
|
155
|
+
const find = (t) => { while (parent[t] !== t) { parent[t] = parent[parent[t]]; t = parent[t]; } return t; };
|
|
156
|
+
for (const list of edges.values()) {
|
|
157
|
+
if (list.length !== 2) continue;
|
|
158
|
+
const [t, u] = list;
|
|
159
|
+
if (live(t) && live(u) && dot(t, u) > PLANE_COS) parent[find(t)] = find(u);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// a group is a flat face only if every edge leaving it would shade hard
|
|
163
|
+
const soft = new Uint8Array(nTri); // indexed by group root
|
|
164
|
+
const hard = (t, u) => {
|
|
165
|
+
const pt = policyFor(triOID[t]) ?? SMOOTH, pu = policyFor(triOID[u]) ?? SMOOTH;
|
|
166
|
+
if (triOID[t] !== triOID[u] && !(pt.boundaryLines || pu.boundaryLines)) return true;
|
|
167
|
+
return dot(t, u) < cosDeg(pt.creaseAngle);
|
|
168
|
+
};
|
|
169
|
+
for (const list of edges.values()) {
|
|
170
|
+
if (list.length !== 2) continue; // non-manifold: no smooth neighbour to share with
|
|
171
|
+
const [t, u] = list;
|
|
172
|
+
if (!live(t) || !live(u) || find(t) === find(u)) continue;
|
|
173
|
+
if (!hard(t, u)) soft[find(t)] = 1;
|
|
174
|
+
if (!hard(u, t)) soft[find(u)] = 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// one area-weighted plane per surviving group, merged where two groups share a
|
|
178
|
+
// plane (a degenerate triangle can split one face into two)
|
|
179
|
+
const acc = new Map();
|
|
180
|
+
for (let t = 0; t < nTri; t++) {
|
|
181
|
+
if (!live(t)) continue;
|
|
182
|
+
const root = find(t);
|
|
183
|
+
if (soft[root]) continue;
|
|
184
|
+
let e = acc.get(root);
|
|
185
|
+
if (!e) acc.set(root, (e = { n: [0, 0, 0], c: [0, 0, 0], a: 0 }));
|
|
186
|
+
for (let k = 0; k < 3; k++) { e.n[k] += fn[t * 3 + k] * area[t]; e.c[k] += cen[t * 3 + k] * area[t]; }
|
|
187
|
+
e.a += area[t];
|
|
188
|
+
}
|
|
189
|
+
const dTol = Math.max(1e-6, span * 1e-7);
|
|
190
|
+
const planes = [];
|
|
191
|
+
for (const e of acc.values()) {
|
|
192
|
+
const L = Math.hypot(...e.n);
|
|
193
|
+
if (!(L > 0)) continue;
|
|
194
|
+
const n = e.n.map((x) => x / L);
|
|
195
|
+
const d = (n[0] * e.c[0] + n[1] * e.c[1] + n[2] * e.c[2]) / e.a;
|
|
196
|
+
const dup = planes.find((p) => p.n[0] * n[0] + p.n[1] * n[1] + p.n[2] * n[2] > PLANE_COS && Math.abs(p.d - d) < dTol);
|
|
197
|
+
if (!dup) planes.push({ n, d });
|
|
198
|
+
}
|
|
199
|
+
return planes;
|
|
200
|
+
}
|
|
201
|
+
|
|
86
202
|
// ---------------------------------------------------------------------------
|
|
87
203
|
// Prism detection for the fast path (manifold-backend.js). The Minkowski chain
|
|
88
204
|
// above is seconds-per-thousand-triangles, but roundAll's expensive real-world
|