partforge 0.145.2 → 0.146.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.
|
@@ -1,87 +1,175 @@
|
|
|
1
1
|
// Minimal 3MF reader, the read twin of threemf.js's writer. 3MF is an OPC
|
|
2
|
-
// package (a zip) holding
|
|
3
|
-
// extract vertices/triangles
|
|
4
|
-
// merge every build item into one soup-free indexed mesh in
|
|
2
|
+
// package (a zip) holding one or more XML model parts; unzip (fflate), find the
|
|
3
|
+
// ROOT model part, extract vertices/triangles, resolve <components> and per-item
|
|
4
|
+
// transforms, and merge every build item into one soup-free indexed mesh in
|
|
5
|
+
// millimetres.
|
|
6
|
+
//
|
|
7
|
+
// Slicer project files (Bambu Studio, Orca, PrusaSlicer) keep no mesh in the
|
|
8
|
+
// root part at all: its objects are <components> pointing, through the
|
|
9
|
+
// Production extension's `p:path`, at meshes in other parts
|
|
10
|
+
// (3D/Objects/object_1.model). Reading only one part — or only top-level
|
|
11
|
+
// <object><mesh> — finds nothing in exactly the files people export most.
|
|
5
12
|
//
|
|
6
13
|
// Regex-based extraction, NOT a DOM parse — workers have no DOMParser and the
|
|
7
14
|
// worker graph must stay DOM-free (test/worker-layering.test.js enforces
|
|
8
15
|
// this transitively). Scope: geometry only — materials, colors and beam
|
|
9
|
-
// lattices are ignored
|
|
10
|
-
// (no <components> nesting).
|
|
16
|
+
// lattices are ignored.
|
|
11
17
|
import { unzipSync } from "fflate";
|
|
12
18
|
|
|
13
19
|
const UNIT_MM = { micron: 0.001, millimeter: 1, centimeter: 10, inch: 25.4, foot: 304.8, meter: 1000 };
|
|
20
|
+
const MAX_COMPONENT_DEPTH = 32;
|
|
21
|
+
const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0];
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
// Part names are compared without a leading slash and case-insensitively: OPC
|
|
24
|
+
// names are case-insensitive, and references carry a leading "/" that zip
|
|
25
|
+
// entry names do not.
|
|
26
|
+
const partKey = (name) => name.replace(/^\/+/, "").toLowerCase();
|
|
27
|
+
|
|
28
|
+
// Transforms are 12 numbers, row-major 4x3 with the translation in the last
|
|
29
|
+
// row, per the 3MF core spec (applied to a row vector: v' = v*M, i.e.
|
|
30
|
+
// x' = x*m00 + y*m10 + z*m20 + m30). The translation is in the units of the
|
|
31
|
+
// part that holds the transform, so it is scaled to mm on read; the linear
|
|
32
|
+
// part is unitless.
|
|
33
|
+
function readTransform(tag, scale) {
|
|
34
|
+
const raw = tag.match(/\btransform="([^"]+)"/)?.[1];
|
|
35
|
+
if (!raw) return IDENTITY;
|
|
36
|
+
const t = raw.trim().split(/\s+/).map(Number);
|
|
37
|
+
if (t.length !== 12 || t.some((n) => !Number.isFinite(n))) return IDENTITY;
|
|
38
|
+
return [...t.slice(0, 9), t[9] * scale, t[10] * scale, t[11] * scale];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Child first, then parent: v * C * P.
|
|
42
|
+
function compose(c, p) {
|
|
43
|
+
const r = new Array(12);
|
|
44
|
+
for (let row = 0; row < 4; row++) {
|
|
45
|
+
for (let col = 0; col < 3; col++) {
|
|
46
|
+
r[row * 3 + col] =
|
|
47
|
+
c[row * 3] * p[col] + c[row * 3 + 1] * p[3 + col] + c[row * 3 + 2] * p[6 + col] + (row === 3 ? p[9 + col] : 0);
|
|
48
|
+
}
|
|
22
49
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
return r;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The OPC/Production-extension `path` attribute, whatever its namespace prefix.
|
|
54
|
+
const readPath = (tag) => tag.match(/\s(?:[\w.-]+:)?path="([^"]+)"/)?.[1] ?? null;
|
|
55
|
+
|
|
56
|
+
function rootModelPath(files, keys) {
|
|
57
|
+
const rels = files.get("_rels/.rels");
|
|
58
|
+
if (rels) {
|
|
59
|
+
const xml = new TextDecoder().decode(rels);
|
|
60
|
+
for (const m of xml.matchAll(/<Relationship\b[^>]*>/g)) {
|
|
61
|
+
const type = m[0].match(/\bType="([^"]+)"/)?.[1] ?? "";
|
|
62
|
+
const target = m[0].match(/\bTarget="([^"]+)"/)?.[1];
|
|
63
|
+
if (target && /\/3dmodel$/i.test(type) && files.has(partKey(target))) return partKey(target);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (files.has("3d/3dmodel.model")) return "3d/3dmodel.model";
|
|
67
|
+
return keys.find((k) => k.endsWith(".model")) ?? null;
|
|
68
|
+
}
|
|
26
69
|
|
|
70
|
+
function parseModelPart(xml, key) {
|
|
27
71
|
const unit = xml.match(/<model\b[^>]*\bunit="([^"]+)"/)?.[1] ?? "millimeter";
|
|
28
72
|
const scale = UNIT_MM[unit];
|
|
29
73
|
if (!scale) throw new Error(`3mf import: unknown unit "${unit}"`);
|
|
30
74
|
|
|
31
|
-
// objects: id -> {
|
|
75
|
+
// objects: id -> { type, mesh: {P (mm), I} | null, components: [{key, id, t}] }
|
|
32
76
|
const objects = new Map();
|
|
33
|
-
const objRe = /<object\b[^>]
|
|
77
|
+
const objRe = /<object\b([^>]*)>([\s\S]*?)<\/object>/g;
|
|
34
78
|
for (let m; (m = objRe.exec(xml)); ) {
|
|
35
|
-
const [,
|
|
79
|
+
const [, attrs, body] = m;
|
|
80
|
+
const id = attrs.match(/\bid="(\d+)"/)?.[1];
|
|
81
|
+
if (!id) continue;
|
|
82
|
+
const type = attrs.match(/\btype="([^"]+)"/)?.[1] ?? "model";
|
|
36
83
|
const P = [], I = [];
|
|
37
84
|
const vRe = /<vertex\b[^>]*\bx="([^"]+)"[^>]*\by="([^"]+)"[^>]*\bz="([^"]+)"/g;
|
|
38
85
|
for (let v; (v = vRe.exec(body)); ) P.push(+v[1] * scale, +v[2] * scale, +v[3] * scale);
|
|
39
86
|
const tRe = /<triangle\b[^>]*\bv1="(\d+)"[^>]*\bv2="(\d+)"[^>]*\bv3="(\d+)"/g;
|
|
40
87
|
for (let t; (t = tRe.exec(body)); ) I.push(+t[1], +t[2], +t[3]);
|
|
41
|
-
|
|
88
|
+
const components = [];
|
|
89
|
+
for (const c of body.matchAll(/<component\b[^>]*>/g)) {
|
|
90
|
+
const cid = c[0].match(/\bobjectid="(\d+)"/)?.[1];
|
|
91
|
+
if (!cid) continue;
|
|
92
|
+
const path = readPath(c[0]);
|
|
93
|
+
components.push({ key: path ? partKey(path) : key, id: cid, t: readTransform(c[0], scale) });
|
|
94
|
+
}
|
|
95
|
+
objects.set(id, { type, mesh: I.length ? { P, I } : null, components });
|
|
42
96
|
}
|
|
43
|
-
if (objects.size === 0) throw new Error("3mf import: model contains no mesh geometry");
|
|
44
97
|
|
|
45
|
-
// Build items
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// Attributes are pulled independently from each <item> tag (rather than in
|
|
52
|
-
// one fixed-order regex) because `objectid` and `transform` can appear in
|
|
53
|
-
// either order and a single ordered pattern with an optional middle group
|
|
54
|
-
// can match the tag while silently leaving `transform` uncaptured.
|
|
98
|
+
// Build items. Attributes are pulled independently from each <item> tag
|
|
99
|
+
// (rather than in one fixed-order regex) because `objectid` and `transform`
|
|
100
|
+
// can appear in either order and a single ordered pattern with an optional
|
|
101
|
+
// middle group can match the tag while silently leaving `transform`
|
|
102
|
+
// uncaptured. Items may be self-closing or carry children (metadata).
|
|
55
103
|
const items = [];
|
|
56
|
-
const
|
|
57
|
-
for (
|
|
58
|
-
const
|
|
59
|
-
const id = tag.match(/\bobjectid="(\d+)"/)?.[1];
|
|
104
|
+
const build = xml.match(/<build\b[^>]*>([\s\S]*?)<\/build>/)?.[1] ?? "";
|
|
105
|
+
for (const m of build.matchAll(/<item\b[^>]*>/g)) {
|
|
106
|
+
const id = m[0].match(/\bobjectid="(\d+)"/)?.[1];
|
|
60
107
|
if (!id) continue;
|
|
61
|
-
const
|
|
62
|
-
items.push({
|
|
108
|
+
const path = readPath(m[0]);
|
|
109
|
+
items.push({ key: path ? partKey(path) : key, id, t: readTransform(m[0], scale) });
|
|
110
|
+
}
|
|
111
|
+
return { objects, items };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parse3MF(bytes) {
|
|
115
|
+
const u8 = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
|
|
116
|
+
let raw;
|
|
117
|
+
try {
|
|
118
|
+
raw = unzipSync(u8);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
throw new Error(`3mf import: not a readable zip archive (${e?.message || e})`, { cause: e });
|
|
63
121
|
}
|
|
64
|
-
const
|
|
122
|
+
const files = new Map(Object.entries(raw).map(([name, data]) => [partKey(name), data]));
|
|
123
|
+
const root = rootModelPath(files, [...files.keys()]);
|
|
124
|
+
if (!root) throw new Error("3mf import: archive has no 3D model part (*.model)");
|
|
125
|
+
|
|
126
|
+
const parts = new Map();
|
|
127
|
+
const partFor = (key) => {
|
|
128
|
+
if (!parts.has(key)) {
|
|
129
|
+
const data = files.get(key);
|
|
130
|
+
if (!data) throw new Error(`3mf import: a component references a missing model part (/${key})`);
|
|
131
|
+
parts.set(key, parseModelPart(new TextDecoder().decode(data), key));
|
|
132
|
+
}
|
|
133
|
+
return parts.get(key);
|
|
134
|
+
};
|
|
65
135
|
|
|
66
136
|
const V = [], Tr = [];
|
|
67
|
-
|
|
68
|
-
const o = objects.get(id);
|
|
69
|
-
if (!o) continue;
|
|
137
|
+
const emit = ({ P, I }, t) => {
|
|
70
138
|
const base = V.length / 3;
|
|
71
|
-
for (let i = 0; i <
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const y2 = t[1] * x + t[4] * y + t[7] * z + t[10] * scale;
|
|
79
|
-
const z2 = t[2] * x + t[5] * y + t[8] * z + t[11] * scale;
|
|
80
|
-
x = x2; y = y2; z = z2;
|
|
81
|
-
}
|
|
82
|
-
V.push(x, y, z);
|
|
139
|
+
for (let i = 0; i < P.length; i += 3) {
|
|
140
|
+
const x = P[i], y = P[i + 1], z = P[i + 2];
|
|
141
|
+
V.push(
|
|
142
|
+
t[0] * x + t[3] * y + t[6] * z + t[9],
|
|
143
|
+
t[1] * x + t[4] * y + t[7] * z + t[10],
|
|
144
|
+
t[2] * x + t[5] * y + t[8] * z + t[11],
|
|
145
|
+
);
|
|
83
146
|
}
|
|
84
|
-
for (const idx of
|
|
147
|
+
for (const idx of I) Tr.push(base + idx);
|
|
148
|
+
};
|
|
149
|
+
const place = (key, id, t, stack) => {
|
|
150
|
+
const ref = `${key}#${id}`;
|
|
151
|
+
if (stack.includes(ref) || stack.length > MAX_COMPONENT_DEPTH) {
|
|
152
|
+
throw new Error("3mf import: components reference each other in a loop");
|
|
153
|
+
}
|
|
154
|
+
const o = partFor(key).objects.get(id);
|
|
155
|
+
if (!o) return;
|
|
156
|
+
// Support and "other" objects are not part of the printed shape.
|
|
157
|
+
if (o.type === "support" || o.type === "other") return;
|
|
158
|
+
if (o.mesh) emit(o.mesh, t);
|
|
159
|
+
for (const c of o.components) place(c.key, c.id, compose(c.t, t), [...stack, ref]);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// No <build> items: every object in the root part that no component uses,
|
|
163
|
+
// at identity.
|
|
164
|
+
const rootPart = partFor(root);
|
|
165
|
+
let items = rootPart.items;
|
|
166
|
+
if (!items.length) {
|
|
167
|
+
const used = new Set();
|
|
168
|
+
for (const o of rootPart.objects.values()) for (const c of o.components) if (c.key === root) used.add(c.id);
|
|
169
|
+
items = [...rootPart.objects.keys()].filter((id) => !used.has(id)).map((id) => ({ key: root, id, t: IDENTITY }));
|
|
85
170
|
}
|
|
171
|
+
for (const { key, id, t } of items) place(key, id, t, []);
|
|
172
|
+
|
|
173
|
+
if (Tr.length === 0) throw new Error("3mf import: model contains no mesh geometry");
|
|
86
174
|
return { positions: Float32Array.from(V), indices: Uint32Array.from(Tr) };
|
|
87
175
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// triangle, no `indices`) or OCCT indexed form (`positions` = 3 floats/vertex +
|
|
4
4
|
// `indices` = 3 vertex-indices/triangle). A reusable spatial index: nearest ray hit
|
|
5
5
|
// (raycast), nearest surface point (closestPoint), and exact mesh-to-mesh distance
|
|
6
|
-
// (distanceTo). AABB tree
|
|
7
|
-
// test with pruning.
|
|
6
|
+
// (distanceTo). AABB tree built by a binned surface-area heuristic (sahSplit),
|
|
7
|
+
// slab ray–box test with nearer-child-first traversal and pruning.
|
|
8
8
|
//
|
|
9
9
|
// STORAGE — four flat typed arrays, no per-triangle and no per-node JS objects.
|
|
10
10
|
// The nested-array representation this replaced ([[x,y,z],[x,y,z],[x,y,z]] per
|
|
@@ -26,21 +26,50 @@
|
|
|
26
26
|
// triangles). Read it through readTriangleInto() rather than
|
|
27
27
|
// open-coding the stride.
|
|
28
28
|
// order Uint32 triangle ids, permuted by the build so each leaf owns a
|
|
29
|
-
// contiguous run. The vertices themselves never
|
|
29
|
+
// contiguous run (at most MAX_LEAF). The vertices themselves never
|
|
30
|
+
// move.
|
|
30
31
|
// bounds Float64, 6 per node: [minx,miny,minz, maxx,maxy,maxz]. Kept at double
|
|
31
32
|
// precision whatever the vertices are — a narrowed bound would have to
|
|
32
33
|
// be rounded outward to stay conservative, and node bounds are a small
|
|
33
|
-
// fraction of the total anyway (
|
|
34
|
+
// fraction of the total anyway (well under one node per triangle).
|
|
34
35
|
// meta Uint32, 2 per node. A LEAF is [firstIndexIntoOrder, count + 1]; an
|
|
35
36
|
// INTERNAL node is [rightChildIndex, 0]. The +1 is what lets an empty
|
|
36
37
|
// mesh's root still read as a leaf instead of as an internal node
|
|
37
38
|
// pointing at itself. Nodes are laid out in pre-order, so an internal
|
|
38
39
|
// node's left child is always the next node.
|
|
39
40
|
|
|
40
|
-
import { core,
|
|
41
|
+
import { core, poisonCore } from "../core/core.js";
|
|
41
42
|
import { buildCoreBVH } from "../core/bvh-core.js";
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
// The build (buildBVH): a binned surface-area heuristic. Each node is split
|
|
45
|
+
// where the two children's box areas, weighted by their triangle counts, sum
|
|
46
|
+
// least — the split that makes a query least likely to have to visit both — so
|
|
47
|
+
// sibling boxes overlap far less than a median split's do. Measured on the
|
|
48
|
+
// reference parts against the median build this replaced (native core): min-wall
|
|
49
|
+
// ~2.9x, closest point ~2x, mesh-to-mesh distance ~36x (lattice-box ~400x, whose
|
|
50
|
+
// median boxes overlapped so badly that its gap check was nearly brute force),
|
|
51
|
+
// and the build itself ~1.2x FASTER; in this JS, min-wall ~2.2x and the build
|
|
52
|
+
// ~1.1x faster.
|
|
53
|
+
//
|
|
54
|
+
// It is DETERMINISTIC by construction and has a C++ twin (native/bvh.cpp) that
|
|
55
|
+
// must build the same tree bit for bit, so: no sort anywhere (a NaN key would
|
|
56
|
+
// make a comparison sort's order the algorithm's own), a STABLE partition, the
|
|
57
|
+
// bin index and every cost computed by the same expressions in the same order,
|
|
58
|
+
// ties going to the first candidate (lowest axis, then lowest bin).
|
|
59
|
+
const BINS = 16; // candidate split planes per axis, per node
|
|
60
|
+
const MAX_LEAF = 8; // a node over more triangles is always split
|
|
61
|
+
const DEPTH_LIMIT = 48; // past this depth, split ranges in half (bounds the tree's depth)
|
|
62
|
+
|
|
63
|
+
// Which of the BINS bins a centroid coordinate falls in. NaN lands in bin 0.
|
|
64
|
+
function binOf(c, lo, scale) {
|
|
65
|
+
const b = Math.floor((c - lo) * scale);
|
|
66
|
+
return b >= BINS ? BINS - 1 : b >= 0 ? b : 0;
|
|
67
|
+
}
|
|
68
|
+
// Half the surface area of a box (the factor of 2 cancels in every comparison).
|
|
69
|
+
const halfArea = (x0, y0, z0, x1, y1, z1) => {
|
|
70
|
+
const x = x1 - x0, y = y1 - y0, z = z1 - z0;
|
|
71
|
+
return x * y + y * z + z * x;
|
|
72
|
+
};
|
|
44
73
|
|
|
45
74
|
// Coords per triangle in a `vertices` store: v0,v1,v2 interleaved. The ONE place
|
|
46
75
|
// this layout is decoded outside the queries below — copy triangle `t` of `V` into
|
|
@@ -106,35 +135,22 @@ export function triangleVertices(mesh) {
|
|
|
106
135
|
return verts;
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
// `bounds`/`meta` be allocated once at the right size rather than grown or trimmed.
|
|
113
|
-
function countNodes(len, memo) {
|
|
114
|
-
if (len <= LEAF) return 1;
|
|
115
|
-
const hit = memo.get(len);
|
|
116
|
-
if (hit !== undefined) return hit;
|
|
117
|
-
const mid = len >> 1;
|
|
118
|
-
const n = 1 + countNodes(mid, memo) + countNodes(len - mid, memo);
|
|
119
|
-
memo.set(len, n);
|
|
120
|
-
return n;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// slab test: does the ray meet node `nb`'s box within (tMin, best]?
|
|
124
|
-
function rayHitsBox(ox, oy, oz, ix, iy, iz, B, nb, tMin, best) {
|
|
138
|
+
// slab test: where the ray enters node `nb`'s box within (tMin, best], or
|
|
139
|
+
// Infinity if it doesn't meet it there.
|
|
140
|
+
function rayEntry(ox, oy, oz, ix, iy, iz, B, nb, tMin, best) {
|
|
125
141
|
let t0 = tMin, t1 = best;
|
|
126
142
|
let lo = (B[nb] - ox) * ix, hi = (B[nb + 3] - ox) * ix;
|
|
127
143
|
if (lo > hi) { const s = lo; lo = hi; hi = s; }
|
|
128
144
|
if (lo > t0) t0 = lo; if (hi < t1) t1 = hi;
|
|
129
|
-
if (t0 > t1) return
|
|
145
|
+
if (t0 > t1) return Infinity;
|
|
130
146
|
lo = (B[nb + 1] - oy) * iy; hi = (B[nb + 4] - oy) * iy;
|
|
131
147
|
if (lo > hi) { const s = lo; lo = hi; hi = s; }
|
|
132
148
|
if (lo > t0) t0 = lo; if (hi < t1) t1 = hi;
|
|
133
|
-
if (t0 > t1) return
|
|
149
|
+
if (t0 > t1) return Infinity;
|
|
134
150
|
lo = (B[nb + 2] - oz) * iz; hi = (B[nb + 5] - oz) * iz;
|
|
135
151
|
if (lo > hi) { const s = lo; lo = hi; hi = s; }
|
|
136
152
|
if (lo > t0) t0 = lo; if (hi < t1) t1 = hi;
|
|
137
|
-
return t0 <= t1;
|
|
153
|
+
return t0 <= t1 ? t0 : Infinity;
|
|
138
154
|
}
|
|
139
155
|
|
|
140
156
|
// nearest point on triangle `base` of `V` to P (Ericson), returns { point, d2 }
|
|
@@ -315,9 +331,7 @@ function indexFor(mesh, allowCore) {
|
|
|
315
331
|
const c = allowCore ? core() : null;
|
|
316
332
|
if (c) {
|
|
317
333
|
try {
|
|
318
|
-
|
|
319
|
-
if (index) return index;
|
|
320
|
-
countRefusedMesh(); // a mesh the core declines (not a fault): the JS index below
|
|
334
|
+
return buildCoreBVH(c, triangleVertices(mesh), () => buildBVH(mesh));
|
|
321
335
|
} catch (err) {
|
|
322
336
|
poisonCore(c, err); // a fault while building: this realm's core is done; JS from here
|
|
323
337
|
}
|
|
@@ -335,73 +349,176 @@ export function buildBVH(mesh) {
|
|
|
335
349
|
const verts = triangleVertices(mesh);
|
|
336
350
|
const count = verts.length / 9;
|
|
337
351
|
|
|
352
|
+
// Per-triangle boxes, held in BUILD order (permuted with `order`) so a node's
|
|
353
|
+
// pass over its range reads them sequentially. A box corner is a vertex
|
|
354
|
+
// coordinate, so the vertices' own array type holds it exactly. Transient,
|
|
355
|
+
// like the partition scratch.
|
|
356
|
+
const Store = verts.constructor;
|
|
338
357
|
const order = new Uint32Array(count);
|
|
339
|
-
|
|
340
|
-
// median split reads them, and they are released explicitly below.
|
|
341
|
-
let cent = new Float64Array(count * 3);
|
|
358
|
+
let tb = new Store(count * 6), tbTmp = new Store(count * 6), orderTmp = new Uint32Array(count);
|
|
342
359
|
for (let t = 0; t < count; t++) {
|
|
343
360
|
order[t] = t;
|
|
344
361
|
const o = t * 9;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
362
|
+
let x0 = Infinity, y0 = Infinity, z0 = Infinity, x1 = -Infinity, y1 = -Infinity, z1 = -Infinity;
|
|
363
|
+
for (let j = 0; j < 9; j += 3) {
|
|
364
|
+
const x = verts[o + j], y = verts[o + j + 1], z = verts[o + j + 2];
|
|
365
|
+
if (x < x0) x0 = x; if (x > x1) x1 = x;
|
|
366
|
+
if (y < y0) y0 = y; if (y > y1) y1 = y;
|
|
367
|
+
if (z < z0) z0 = z; if (z > z1) z1 = z;
|
|
350
368
|
}
|
|
369
|
+
tb[t * 6] = x0; tb[t * 6 + 1] = y0; tb[t * 6 + 2] = z0; tb[t * 6 + 3] = x1; tb[t * 6 + 4] = y1; tb[t * 6 + 5] = z1;
|
|
351
370
|
}
|
|
352
371
|
|
|
353
|
-
|
|
354
|
-
|
|
372
|
+
// A binary tree over n >= 1 leaves has at most 2n - 1 nodes; trimmed below.
|
|
373
|
+
let bounds = new Float64Array(Math.max(1, 2 * count - 1) * 6);
|
|
374
|
+
let meta = new Uint32Array(bounds.length / 3);
|
|
355
375
|
let next = 0;
|
|
376
|
+
// Bin scratch, reused by every node (done with before it recurses).
|
|
377
|
+
const binN = new Float64Array(BINS), binB = new Float64Array(BINS * 6);
|
|
378
|
+
const rightArea = new Float64Array(BINS), rightN = new Float64Array(BINS);
|
|
356
379
|
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
380
|
+
// A range's box and its centroids' box, as 12 numbers: lo xyz, hi xyz,
|
|
381
|
+
// centroid lo xyz, centroid hi xyz. The parent's partition pass fills one
|
|
382
|
+
// per child (same triangles, same order — the same values a pass of the
|
|
383
|
+
// child's own would find), so a node never rescans its range just for these.
|
|
384
|
+
const rangeBox = () => Float64Array.of(Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity,
|
|
385
|
+
Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity);
|
|
386
|
+
function addTo(R, o) {
|
|
387
|
+
const l0 = tb[o], l1 = tb[o + 1], l2 = tb[o + 2], h0 = tb[o + 3], h1 = tb[o + 4], h2 = tb[o + 5];
|
|
388
|
+
const c0 = (l0 + h0) / 2, c1 = (l1 + h1) / 2, c2 = (l2 + h2) / 2;
|
|
389
|
+
if (l0 < R[0]) R[0] = l0; if (h0 > R[3]) R[3] = h0; if (c0 < R[6]) R[6] = c0; if (c0 > R[9]) R[9] = c0;
|
|
390
|
+
if (l1 < R[1]) R[1] = l1; if (h1 > R[4]) R[4] = h1; if (c1 < R[7]) R[7] = c1; if (c1 > R[10]) R[10] = c1;
|
|
391
|
+
if (l2 < R[2]) R[2] = l2; if (h2 > R[5]) R[5] = h2; if (c2 < R[8]) R[8] = c2; if (c2 > R[11]) R[11] = c2;
|
|
392
|
+
}
|
|
393
|
+
function measureRange(start, len) {
|
|
394
|
+
const R = rangeBox();
|
|
395
|
+
for (let k = start; k < start + len; k++) addTo(R, k * 6);
|
|
396
|
+
return R;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Pre-order emit: a node claims its slot and writes its own AABB (`R`, its
|
|
400
|
+
// range's boxes), then either becomes a leaf over order[start, start+len) or
|
|
401
|
+
// partitions that range and recurses. The left child lands at self+1 by
|
|
402
|
+
// construction, so only the right child's index needs storing.
|
|
403
|
+
function emit(start, len, depth, R) {
|
|
362
404
|
const self = next++, nb = self * 6;
|
|
363
|
-
let
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
405
|
+
for (let j = 0; j < 6; j++) bounds[nb + j] = R[j];
|
|
406
|
+
if (len <= 1) { meta[self * 2] = start; meta[self * 2 + 1] = len + 1; return self; }
|
|
407
|
+
|
|
408
|
+
if (depth < DEPTH_LIMIT) {
|
|
409
|
+
// The split search: the axis of the widest centroid spread (ties to the
|
|
410
|
+
// lower axis), its BINS bins, the cheapest plane between them. One axis,
|
|
411
|
+
// not three: measured, binning all three bought queries 5-10% for a
|
|
412
|
+
// build ~1.8x as long in the native core.
|
|
413
|
+
const ex = R[9] - R[6], ey = R[10] - R[7], ez = R[11] - R[8];
|
|
414
|
+
const axis = ex >= ey && ex >= ez ? 0 : ey >= ez ? 1 : 2;
|
|
415
|
+
const ext = R[9 + axis] - R[6 + axis];
|
|
416
|
+
let bestCost = Infinity, bestAxis = -1, bestSplit = 0;
|
|
417
|
+
if (ext > 0) {
|
|
418
|
+
const lo = R[6 + axis], scale = BINS / ext;
|
|
419
|
+
binN.fill(0);
|
|
420
|
+
for (let i = 0; i < BINS; i++) {
|
|
421
|
+
binB[i * 6] = binB[i * 6 + 1] = binB[i * 6 + 2] = Infinity;
|
|
422
|
+
binB[i * 6 + 3] = binB[i * 6 + 4] = binB[i * 6 + 5] = -Infinity;
|
|
423
|
+
}
|
|
424
|
+
for (let k = start; k < start + len; k++) {
|
|
425
|
+
const o = k * 6, bin = binOf((tb[o + axis] + tb[o + 3 + axis]) / 2, lo, scale), b = bin * 6;
|
|
426
|
+
binN[bin]++;
|
|
427
|
+
const l0 = tb[o], l1 = tb[o + 1], l2 = tb[o + 2], h0 = tb[o + 3], h1 = tb[o + 4], h2 = tb[o + 5];
|
|
428
|
+
if (l0 < binB[b]) binB[b] = l0; if (h0 > binB[b + 3]) binB[b + 3] = h0;
|
|
429
|
+
if (l1 < binB[b + 1]) binB[b + 1] = l1; if (h1 > binB[b + 4]) binB[b + 4] = h1;
|
|
430
|
+
if (l2 < binB[b + 2]) binB[b + 2] = l2; if (h2 > binB[b + 5]) binB[b + 5] = h2;
|
|
431
|
+
}
|
|
432
|
+
// Sweep from the right for each candidate's right side, then from the
|
|
433
|
+
// left, costing every plane that leaves triangles on both sides.
|
|
434
|
+
let rx0 = Infinity, ry0 = Infinity, rz0 = Infinity, rx1 = -Infinity, ry1 = -Infinity, rz1 = -Infinity, rn = 0;
|
|
435
|
+
for (let i = BINS - 1; i > 0; i--) {
|
|
436
|
+
const o = i * 6;
|
|
437
|
+
if (binB[o] < rx0) rx0 = binB[o]; if (binB[o + 3] > rx1) rx1 = binB[o + 3];
|
|
438
|
+
if (binB[o + 1] < ry0) ry0 = binB[o + 1]; if (binB[o + 4] > ry1) ry1 = binB[o + 4];
|
|
439
|
+
if (binB[o + 2] < rz0) rz0 = binB[o + 2]; if (binB[o + 5] > rz1) rz1 = binB[o + 5];
|
|
440
|
+
rn += binN[i];
|
|
441
|
+
rightArea[i] = halfArea(rx0, ry0, rz0, rx1, ry1, rz1); rightN[i] = rn;
|
|
442
|
+
}
|
|
443
|
+
let lx0 = Infinity, ly0 = Infinity, lz0 = Infinity, lx1 = -Infinity, ly1 = -Infinity, lz1 = -Infinity, ln = 0;
|
|
444
|
+
for (let i = 0; i < BINS - 1; i++) {
|
|
445
|
+
const o = i * 6;
|
|
446
|
+
if (binB[o] < lx0) lx0 = binB[o]; if (binB[o + 3] > lx1) lx1 = binB[o + 3];
|
|
447
|
+
if (binB[o + 1] < ly0) ly0 = binB[o + 1]; if (binB[o + 4] > ly1) ly1 = binB[o + 4];
|
|
448
|
+
if (binB[o + 2] < lz0) lz0 = binB[o + 2]; if (binB[o + 5] > lz1) lz1 = binB[o + 5];
|
|
449
|
+
ln += binN[i];
|
|
450
|
+
if (!ln || !rightN[i + 1]) continue;
|
|
451
|
+
const cost = halfArea(lx0, ly0, lz0, lx1, ly1, lz1) * ln + rightArea[i + 1] * rightN[i + 1];
|
|
452
|
+
if (cost < bestCost) { bestCost = cost; bestAxis = axis; bestSplit = i; }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (bestAxis >= 0) {
|
|
456
|
+
// A small node stays a leaf unless splitting beats testing its
|
|
457
|
+
// triangles outright (one box test ~ one triangle test).
|
|
458
|
+
const area = halfArea(R[0], R[1], R[2], R[3], R[4], R[5]);
|
|
459
|
+
if (len <= MAX_LEAF && area + bestCost >= area * len) {
|
|
460
|
+
meta[self * 2] = start; meta[self * 2 + 1] = len + 1; return self;
|
|
461
|
+
}
|
|
462
|
+
// Stable partition: bins <= bestSplit first, each side in its order.
|
|
463
|
+
// The left side is compacted in place (it never overtakes k); the
|
|
464
|
+
// right side waits in scratch and is appended after.
|
|
465
|
+
const lo = R[6 + bestAxis], scale = BINS / (R[9 + bestAxis] - R[6 + bestAxis]);
|
|
466
|
+
const L = rangeBox(), Rt = rangeBox();
|
|
467
|
+
let l = start, r = 0;
|
|
468
|
+
for (let k = start; k < start + len; k++) {
|
|
469
|
+
const o = k * 6;
|
|
470
|
+
if (binOf((tb[o + bestAxis] + tb[o + 3 + bestAxis]) / 2, lo, scale) <= bestSplit) {
|
|
471
|
+
addTo(L, o);
|
|
472
|
+
order[l] = order[k];
|
|
473
|
+
for (let j = 0; j < 6; j++) tb[l * 6 + j] = tb[o + j];
|
|
474
|
+
l++;
|
|
475
|
+
} else {
|
|
476
|
+
addTo(Rt, o);
|
|
477
|
+
orderTmp[r] = order[k];
|
|
478
|
+
for (let j = 0; j < 6; j++) tbTmp[r * 6 + j] = tb[o + j];
|
|
479
|
+
r++;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
for (let i = 0; i < r; i++) {
|
|
483
|
+
order[l + i] = orderTmp[i];
|
|
484
|
+
for (let j = 0; j < 6; j++) tb[(l + i) * 6 + j] = tbTmp[i * 6 + j];
|
|
485
|
+
}
|
|
486
|
+
emit(start, l - start, depth + 1, L);
|
|
487
|
+
meta[self * 2] = emit(l, r, depth + 1, Rt);
|
|
488
|
+
meta[self * 2 + 1] = 0;
|
|
489
|
+
return self;
|
|
371
490
|
}
|
|
372
491
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (len <=
|
|
376
|
-
const ex = x1 - x0, ey = y1 - y0, ez = z1 - z0;
|
|
377
|
-
const axis = ex >= ey && ex >= ez ? 0 : ey >= ez ? 1 : 2;
|
|
378
|
-
order.subarray(start, start + len).sort((p, q) => cent[p * 3 + axis] - cent[q * 3 + axis]);
|
|
379
|
-
// No empty-half guard: len > LEAF here (LEAF >= 1), so mid = len>>1 >= 2 and
|
|
380
|
-
// len - mid >= 3 — both halves always non-empty. The nested-array build this
|
|
381
|
-
// replaced carried such a check; it was dead code there too, and countNodes()
|
|
382
|
-
// assumes this same split, so a guard that ever fired would mis-size `bounds`.
|
|
492
|
+
// No plane separates the centroids (they coincide), or the depth limit:
|
|
493
|
+
// halve the range as it stands.
|
|
494
|
+
if (len <= MAX_LEAF) { meta[self * 2] = start; meta[self * 2 + 1] = len + 1; return self; }
|
|
383
495
|
const mid = len >> 1;
|
|
384
|
-
emit(start, mid);
|
|
385
|
-
meta[self * 2] = emit(start + mid, len - mid);
|
|
496
|
+
emit(start, mid, depth + 1, measureRange(start, mid));
|
|
497
|
+
meta[self * 2] = emit(start + mid, len - mid, depth + 1, measureRange(start + mid, len - mid));
|
|
386
498
|
meta[self * 2 + 1] = 0;
|
|
387
499
|
return self;
|
|
388
500
|
}
|
|
389
|
-
emit(0, count);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
501
|
+
emit(0, count, 0, measureRange(0, count));
|
|
502
|
+
bounds = bounds.slice(0, next * 6);
|
|
503
|
+
meta = meta.slice(0, next * 2);
|
|
504
|
+
// Dropped explicitly, not left to scope: `emit` and the three query closures
|
|
505
|
+
// below share one function context, so build scratch merely gone out of use
|
|
506
|
+
// would stay reachable from every returned BVH for the life of the index.
|
|
507
|
+
tb = tbTmp = orderTmp = null;
|
|
395
508
|
|
|
396
509
|
function raycast(origin, dir, { tMin = 1e-6, tMax = Infinity, skipTri = -1 } = {}) {
|
|
397
510
|
const ox = origin[0], oy = origin[1], oz = origin[2];
|
|
398
511
|
const dx = dir[0], dy = dir[1], dz = dir[2];
|
|
399
512
|
const ix = 1 / dx, iy = 1 / dy, iz = 1 / dz;
|
|
400
513
|
let best = tMax, bestTri = -1;
|
|
401
|
-
|
|
514
|
+
// Nearer child first, each pushed with its entry distance: a node whose box
|
|
515
|
+
// starts beyond the best hit found since is dropped on the pop.
|
|
516
|
+
const stack = [], entry = [];
|
|
517
|
+
const e0 = rayEntry(ox, oy, oz, ix, iy, iz, bounds, 0, tMin, best);
|
|
518
|
+
if (e0 !== Infinity) { stack.push(0); entry.push(e0); }
|
|
402
519
|
while (stack.length) {
|
|
403
520
|
const n = stack.pop();
|
|
404
|
-
if (
|
|
521
|
+
if (entry.pop() > best) continue;
|
|
405
522
|
const packed = meta[n * 2 + 1];
|
|
406
523
|
if (packed) {
|
|
407
524
|
const start = meta[n * 2];
|
|
@@ -411,7 +528,18 @@ export function buildBVH(mesh) {
|
|
|
411
528
|
const t = rayTri(ox, oy, oz, dx, dy, dz, verts, tri * 9, tMin);
|
|
412
529
|
if (t < best) { best = t; bestTri = tri; }
|
|
413
530
|
}
|
|
414
|
-
} else {
|
|
531
|
+
} else {
|
|
532
|
+
const l = n + 1, r = meta[n * 2];
|
|
533
|
+
const tl = rayEntry(ox, oy, oz, ix, iy, iz, bounds, l * 6, tMin, best);
|
|
534
|
+
const tr = rayEntry(ox, oy, oz, ix, iy, iz, bounds, r * 6, tMin, best);
|
|
535
|
+
if (tl <= tr) {
|
|
536
|
+
if (tr !== Infinity) { stack.push(r); entry.push(tr); }
|
|
537
|
+
if (tl !== Infinity) { stack.push(l); entry.push(tl); }
|
|
538
|
+
} else {
|
|
539
|
+
if (tl !== Infinity) { stack.push(l); entry.push(tl); }
|
|
540
|
+
if (tr !== Infinity) { stack.push(r); entry.push(tr); }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
415
543
|
}
|
|
416
544
|
return bestTri === -1 ? null : { t: best, tri: bestTri };
|
|
417
545
|
}
|