partforge 0.137.0 → 0.137.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
|
@@ -21,6 +21,7 @@ import { toEdgeFinder } from "./edge-selector.js";
|
|
|
21
21
|
import { toFaceFinder } from "./face-selector.js";
|
|
22
22
|
import { addSugar } from "./solid-sugar.js";
|
|
23
23
|
import { makeShape2dFactory } from "./shape2d.js";
|
|
24
|
+
import { groupInteractingRegions } from "./region-interaction.js";
|
|
24
25
|
import { finishKernel } from "./kernel-front.js";
|
|
25
26
|
import { makeProfileWarner } from "./profile-warnings.js";
|
|
26
27
|
import { createOcctRepair } from "./occt-repair.js";
|
|
@@ -519,15 +520,47 @@ export function createOcctKernel(replicad) {
|
|
|
519
520
|
for (const hole of holes) region = region.cut(contourDrawing(hole));
|
|
520
521
|
return region;
|
|
521
522
|
};
|
|
522
|
-
// Region list ->
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
523
|
+
// Region list -> Drawing, for extrude/revolve (via drawingFor below).
|
|
524
|
+
//
|
|
525
|
+
// A Shape2D's regions are NOT guaranteed disjoint: booleans/offset emit disjoint
|
|
526
|
+
// regions (an island inside a hole is its own top-level region), but k.shape2d([...])
|
|
527
|
+
// lifts a hand-authored region array verbatim, so two of them may overlap. The old
|
|
528
|
+
// left-fold `acc.fuse(region)` unioned them correctly but re-organised every
|
|
529
|
+
// accumulated blueprint on each fuse (replicad organiseBlueprints → containment
|
|
530
|
+
// search), so N regions cost O(N²) — a ~1,500-triangle lattice took ~45 s and
|
|
531
|
+
// gigabytes of embind wrappers. Now regions are grouped by whether they actually
|
|
532
|
+
// INTERACT (see regionsInteract); a region that touches no other is turned straight
|
|
533
|
+
// into Blueprints with no boolean at all, and only genuinely interacting clusters
|
|
534
|
+
// are fused, as a balanced tree.
|
|
535
|
+
//
|
|
536
|
+
// A region becomes an outer Blueprint plus its hole Blueprints in one CompoundBlueprint
|
|
537
|
+
// (outer first — what organiseBlueprints would produce); holes are never cut one at a
|
|
538
|
+
// time, since each cut2D re-organises too. Hole winding is irrelevant: replicad's
|
|
539
|
+
// CompoundSketch.face() runs ShapeFix_Face.FixOrientation.
|
|
540
|
+
const { Drawing, Blueprints, CompoundBlueprint } = replicad;
|
|
541
|
+
const bpOf = (contour) => contourDrawing(contour).innerShape;
|
|
542
|
+
const regionBlueprint = (rg) => (rg.holes.length
|
|
543
|
+
? new CompoundBlueprint([bpOf(rg.outer), ...rg.holes.map(bpOf)])
|
|
544
|
+
: bpOf(rg.outer));
|
|
545
|
+
const fuseBalanced = (drawings) => {
|
|
546
|
+
while (drawings.length > 1) {
|
|
547
|
+
const next = [];
|
|
548
|
+
for (let i = 0; i < drawings.length; i += 2)
|
|
549
|
+
next.push(i + 1 < drawings.length ? drawings[i].fuse(drawings[i + 1]) : drawings[i]);
|
|
550
|
+
drawings = next;
|
|
551
|
+
}
|
|
552
|
+
return drawings[0];
|
|
553
|
+
};
|
|
554
|
+
const drawingFromRegions = (regions) => {
|
|
555
|
+
if (regions.length === 0) return null;
|
|
556
|
+
const parts = [];
|
|
557
|
+
for (const group of groupInteractingRegions(regions, SHAPE2D_SEGS)) {
|
|
558
|
+
if (group.length === 1) { parts.push(regionBlueprint(group[0])); continue; }
|
|
559
|
+
const fused = fuseBalanced(group.map((rg) => new Drawing(regionBlueprint(rg)))).innerShape;
|
|
560
|
+
parts.push(...(fused instanceof Blueprints ? fused.blueprints : [fused]));
|
|
561
|
+
}
|
|
562
|
+
return new Drawing(parts.length === 1 ? parts[0] : new Blueprints(parts));
|
|
563
|
+
};
|
|
531
564
|
|
|
532
565
|
// 2-D boolean value: the SHARED Shape2D (shape2d.js), identical to the Manifold
|
|
533
566
|
// backend's. Storage is the curve-native contour IR and every op — including
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Pure (WASM-free) grouping of Shape2D regions into clusters that must be unioned
|
|
2
|
+
// together before a kernel op, versus regions that provably touch nothing else and can
|
|
3
|
+
// be handed to the kernel as-is. Used by the OCCT backend's drawingFromRegions: a
|
|
4
|
+
// lattice of N disjoint regions must not pay N-1 boolean fuses (each one re-runs
|
|
5
|
+
// replicad's containment search over everything accumulated: O(N²)).
|
|
6
|
+
//
|
|
7
|
+
// Two regions INTERACT when their boundaries touch or cross, or one lies inside the
|
|
8
|
+
// other's material (inside its outer and not inside one of its holes — so an island
|
|
9
|
+
// sitting in another region's hole is DISJOINT, which is exactly how booleans emit it).
|
|
10
|
+
// The test runs on the contours tessellated at the shape's LOD; a pair separated by less
|
|
11
|
+
// than the chord error of a curve is a degenerate near-touch the kernel's own tolerance
|
|
12
|
+
// can't resolve either.
|
|
13
|
+
import { tessellateContour } from "./profile.js";
|
|
14
|
+
import { pointInRing } from "./shape2d-regions.js";
|
|
15
|
+
|
|
16
|
+
const EPS = 1e-7;
|
|
17
|
+
|
|
18
|
+
const orient = (a, b, c) => (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
19
|
+
const onSeg = (a, b, p) =>
|
|
20
|
+
Math.min(a[0], b[0]) - EPS <= p[0] && p[0] <= Math.max(a[0], b[0]) + EPS &&
|
|
21
|
+
Math.min(a[1], b[1]) - EPS <= p[1] && p[1] <= Math.max(a[1], b[1]) + EPS;
|
|
22
|
+
// Segments ab and cd cross or touch (collinear overlap and shared endpoints count).
|
|
23
|
+
const segsMeet = (a, b, c, d) => {
|
|
24
|
+
const o1 = orient(a, b, c), o2 = orient(a, b, d), o3 = orient(c, d, a), o4 = orient(c, d, b);
|
|
25
|
+
if (((o1 > 0 && o2 < 0) || (o1 < 0 && o2 > 0)) && ((o3 > 0 && o4 < 0) || (o3 < 0 && o4 > 0))) return true;
|
|
26
|
+
return (o1 === 0 && onSeg(a, b, c)) || (o2 === 0 && onSeg(a, b, d)) ||
|
|
27
|
+
(o3 === 0 && onSeg(c, d, a)) || (o4 === 0 && onSeg(c, d, b));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ringBox = (ring) => {
|
|
31
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
32
|
+
for (const [x, y] of ring) { if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; }
|
|
33
|
+
return [x0, y0, x1, y1];
|
|
34
|
+
};
|
|
35
|
+
const boxesMeet = (a, b) => a[0] <= b[2] + EPS && b[0] <= a[2] + EPS && a[1] <= b[3] + EPS && b[1] <= a[3] + EPS;
|
|
36
|
+
|
|
37
|
+
const ringsMeet = (ra, rb) => {
|
|
38
|
+
if (!boxesMeet(ra.box, rb.box)) return false;
|
|
39
|
+
const A = ra.pts, B = rb.pts;
|
|
40
|
+
for (let i = 0; i < A.length; i++) {
|
|
41
|
+
const a = A[i], b = A[(i + 1) % A.length];
|
|
42
|
+
const ex0 = Math.min(a[0], b[0]) - EPS, ex1 = Math.max(a[0], b[0]) + EPS;
|
|
43
|
+
const ey0 = Math.min(a[1], b[1]) - EPS, ey1 = Math.max(a[1], b[1]) + EPS;
|
|
44
|
+
if (ex1 < rb.box[0] || ex0 > rb.box[2] || ey1 < rb.box[1] || ey0 > rb.box[3]) continue;
|
|
45
|
+
for (let j = 0; j < B.length; j++) {
|
|
46
|
+
const c = B[j], d = B[(j + 1) % B.length];
|
|
47
|
+
if (Math.max(c[0], d[0]) < ex0 || Math.min(c[0], d[0]) > ex1 ||
|
|
48
|
+
Math.max(c[1], d[1]) < ey0 || Math.min(c[1], d[1]) > ey1) continue;
|
|
49
|
+
if (segsMeet(a, b, c, d)) return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// p is in the region's MATERIAL: inside the outer ring, outside every hole.
|
|
56
|
+
const inMaterial = (p, r) =>
|
|
57
|
+
boxesMeet(r.outer.box, [p[0], p[1], p[0], p[1]]) && pointInRing(p, r.outer.pts) &&
|
|
58
|
+
!r.holes.some((h) => pointInRing(p, h.pts));
|
|
59
|
+
|
|
60
|
+
const regionsInteract = (a, b) => {
|
|
61
|
+
if (!boxesMeet(a.outer.box, b.outer.box)) return false;
|
|
62
|
+
for (const ra of [a.outer, ...a.holes])
|
|
63
|
+
for (const rb of [b.outer, ...b.holes])
|
|
64
|
+
if (ringsMeet(ra, rb)) return true;
|
|
65
|
+
// No boundary contact: each ring of one is wholly inside or outside the other, so one
|
|
66
|
+
// vertex of each outer decides.
|
|
67
|
+
return inMaterial(b.outer.pts[0], a) || inMaterial(a.outer.pts[0], b);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// regions: [{outer, holes}] (curve-native contours). Returns arrays of the ORIGINAL
|
|
71
|
+
// region objects: singletons are independent of everything else; larger groups
|
|
72
|
+
// interact transitively and need a union. Groups come out in first-member order.
|
|
73
|
+
export function groupInteractingRegions(regions, segs) {
|
|
74
|
+
const ring = (c) => { const pts = tessellateContour(c, segs); return { pts, box: ringBox(pts) }; };
|
|
75
|
+
const info = regions.map((rg) => ({ outer: ring(rg.outer), holes: rg.holes.map(ring) }));
|
|
76
|
+
const parent = regions.map((_, i) => i);
|
|
77
|
+
const find = (i) => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; };
|
|
78
|
+
|
|
79
|
+
// Sweep along x: only regions whose x-extents overlap can interact.
|
|
80
|
+
const order = regions.map((_, i) => i).sort((i, j) => info[i].outer.box[0] - info[j].outer.box[0]);
|
|
81
|
+
for (let oi = 0; oi < order.length; oi++) {
|
|
82
|
+
const i = order[oi];
|
|
83
|
+
for (let oj = oi + 1; oj < order.length; oj++) {
|
|
84
|
+
const j = order[oj];
|
|
85
|
+
if (info[j].outer.box[0] > info[i].outer.box[2] + EPS) break;
|
|
86
|
+
if (find(i) === find(j)) continue;
|
|
87
|
+
if (regionsInteract(info[i], info[j])) parent[find(j)] = find(i);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const groups = new Map();
|
|
91
|
+
regions.forEach((rg, i) => {
|
|
92
|
+
const root = find(i);
|
|
93
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
94
|
+
groups.get(root).push(rg);
|
|
95
|
+
});
|
|
96
|
+
// Map preserves insertion order: first member by index → stable output.
|
|
97
|
+
return [...groups.values()];
|
|
98
|
+
}
|