trid3d 0.5.0 → 0.7.0

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.
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Questions about a mesh's surface, asked in code: where a ray first meets
3
+ * it, whether a point is inside it, how far a point is from it, and whether
4
+ * it's closed. Also the same ray question against a whole scene.
5
+ *
6
+ * const q = meshQuery(body); // prepared once (a BVH)
7
+ * const hit = q.raycast(from, { x: 0, y: 0, z: 1 });
8
+ * q.contains(p); q.distance(p); q.closest(p);
9
+ *
10
+ * A query is a snapshot: it copies the mesh's triangles when it's made, so
11
+ * editing the mesh afterwards doesn't change its answers. Make a new one.
12
+ *
13
+ * Faces are clockwise from outside (like the built-in shapes), so normals
14
+ * here point out of the mesh, whichever side a ray comes from.
15
+ */
16
+ import { invertMat4, determinantMat4, transformPoint, transformDirection, normalMatrix, transformNormal } from "../math/matrix.js";
17
+ import { worldObjects, ancestors } from "../sceneGraph.js";
18
+ import { isPickable } from "../tools/helpers.js";
19
+ import { samePlace } from "./cleanup.js";
20
+
21
+ /** Most triangles in a BVH leaf */
22
+ const LEAF = 4;
23
+ /** Bins per split when building the BVH (surface area heuristic) */
24
+ const BINS = 12;
25
+
26
+ const SIDES = ["both", "front", "back"];
27
+
28
+ /** Odd-looking directions, so rays rarely run exactly along edges (for `contains`) */
29
+ const PROBES = [
30
+ [0.5383, 0.6412, 0.5468], [-0.7071, 0.1934, 0.6797], [0.2311, -0.8417, -0.4878],
31
+ [-0.3122, -0.4519, 0.8357], [0.8829, -0.2711, 0.3833], [-0.5867, 0.7713, -0.2467],
32
+ [0.1471, 0.3378, -0.9296],
33
+ ].map(([x, y, z]) => {
34
+ const l = Math.hypot(x, y, z);
35
+ return [x / l, y / l, z / l];
36
+ });
37
+
38
+ const finite = (v) => !!v && Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z);
39
+
40
+ function checkPoint(p, what) {
41
+ if (!finite(p)) throw new Error(`${what} must be a point { x, y, z } of finite numbers`);
42
+ }
43
+
44
+ function unitDirection(d) {
45
+ const l = finite(d) ? Math.hypot(d.x, d.y, d.z) : NaN;
46
+ if (!(l > 0) || !Number.isFinite(l)) throw new Error("A ray needs a direction { x, y, z } that isn't zero");
47
+ return { x: d.x / l, y: d.y / l, z: d.z / l };
48
+ }
49
+
50
+ function checkSide(side) {
51
+ if (!SIDES.includes(side)) throw new Error(`Unknown side "${side}": use "both", "front" or "back"`);
52
+ }
53
+
54
+ /**
55
+ * Prepares a mesh for many questions: splits its faces into triangles (fans
56
+ * from each face's first corner, like `pickHit`) and builds a bounding volume
57
+ * hierarchy (boxes inside boxes) so each question only looks at the few
58
+ * triangles near it. Making it costs about as much as one slow question.
59
+ *
60
+ * @param {Object} mesh - A mesh with `vertices` and `faces`
61
+ * @returns {MeshQuery}
62
+ */
63
+ export function meshQuery(mesh) {
64
+ return new MeshQuery(mesh);
65
+ }
66
+
67
+ class MeshQuery {
68
+ constructor(mesh) {
69
+ const v = mesh?.vertices ?? [];
70
+ const corners = [], faceOf = [];
71
+ (mesh?.faces ?? []).forEach((f, fi) => {
72
+ if (!Array.isArray(f)) return;
73
+ for (let i = 1; i + 1 < f.length; i++) {
74
+ if (!finite(v[f[0]]) || !finite(v[f[i]]) || !finite(v[f[i + 1]])) continue;
75
+ corners.push(f[0], f[i], f[i + 1]);
76
+ faceOf.push(fi);
77
+ }
78
+ });
79
+ const n = faceOf.length;
80
+ const tris = new Float64Array(n * 9);
81
+ for (let t = 0; t < n; t++) {
82
+ for (let k = 0; k < 3; k++) {
83
+ const p = v[corners[t * 3 + k]];
84
+ tris[t * 9 + k * 3] = p.x;
85
+ tris[t * 9 + k * 3 + 1] = p.y;
86
+ tris[t * 9 + k * 3 + 2] = p.z;
87
+ }
88
+ }
89
+ /** Triangles, 9 numbers each (in BVH order after building) */
90
+ this._tris = tris;
91
+ this._corners = Int32Array.from(corners);
92
+ this._faceOf = Int32Array.from(faceOf);
93
+ /** Number of triangles */
94
+ this.triangleCount = n;
95
+ buildBVH(this);
96
+
97
+ const b = this._box;
98
+ this.bounds = n
99
+ ? { min: { x: b[0], y: b[1], z: b[2] }, max: { x: b[3], y: b[4], z: b[5] } }
100
+ : { min: { x: 0, y: 0, z: 0 }, max: { x: 0, y: 0, z: 0 } };
101
+ const size = n ? Math.max(b[3] - b[0], b[4] - b[1], b[5] - b[2]) || 1 : 1;
102
+ // Tolerances relative to the mesh's size
103
+ this._minDist = size * 1e-9;
104
+ this._detEps = size * size * 1e-14;
105
+ this._edgeEps = 1e-7;
106
+ this._hit = { t: 0, tri: -1, u: 0, w: 0 };
107
+ this._near = new Float64Array(5);
108
+ }
109
+
110
+ /**
111
+ * The first place a ray meets the surface, or null.
112
+ *
113
+ * @param {Vec3} origin
114
+ * @param {Vec3} direction - Any length but zero
115
+ * @param {{maxDistance?:number, side?:"both"|"front"|"back"}} [opts]
116
+ * `side`: "front" only counts faces hit from outside, "back" from inside
117
+ * @returns {{point, normal, distance, face, front, triangle, barycentric} | null}
118
+ */
119
+ raycast(origin, direction, { maxDistance = Infinity, side = "both" } = {}) {
120
+ checkPoint(origin, "raycast: origin");
121
+ checkSide(side);
122
+ const d = unitDirection(direction);
123
+ if (!this._cast(origin, d, maxDistance, side)) return null;
124
+ const { t, tri, u, w } = this._hit;
125
+ return this._describe(tri, u, w, { x: origin.x + d.x * t, y: origin.y + d.y * t, z: origin.z + d.z * t }, t, d);
126
+ }
127
+
128
+ /**
129
+ * Whether a point is inside. Only meaningful for a closed mesh (see
130
+ * `isClosed`). Rays are sent in a few directions and their crossings
131
+ * counted; rays that pass too close to an edge are ignored.
132
+ *
133
+ * @param {Vec3} p
134
+ * @returns {boolean}
135
+ */
136
+ contains(p) {
137
+ checkPoint(p, "contains: the point");
138
+ const b = this._box;
139
+ if (!this.triangleCount || p.x < b[0] || p.y < b[1] || p.z < b[2] || p.x > b[3] || p.y > b[4] || p.z > b[5]) return false;
140
+ let inside = 0, outside = 0, unsureIn = 0, unsureOut = 0;
141
+ for (const dir of PROBES) {
142
+ const r = this._crossings(p, dir);
143
+ if (r.sure) (r.count % 2 ? inside++ : outside++);
144
+ else (r.count % 2 ? unsureIn++ : unsureOut++);
145
+ if (inside >= 2 || outside >= 2) break;
146
+ }
147
+ if (inside !== outside) return inside > outside;
148
+ return unsureIn > unsureOut;
149
+ }
150
+
151
+ /**
152
+ * The nearest point of the surface, or null when there's none within `maxDistance`.
153
+ *
154
+ * @param {Vec3} p
155
+ * @param {{maxDistance?:number}} [opts]
156
+ * @returns {{point, normal, distance, face, triangle, barycentric} | null}
157
+ */
158
+ closest(p, { maxDistance = Infinity } = {}) {
159
+ checkPoint(p, "closest: the point");
160
+ const found = this._nearest(p, maxDistance);
161
+ if (found < 0) return null;
162
+ const c = this._near;
163
+ const w1 = c[3], w2 = c[4];
164
+ const point = { x: c[0], y: c[1], z: c[2] };
165
+ return this._describe(found, w1, w2, point, Math.hypot(p.x - c[0], p.y - c[1], p.z - c[2]), null);
166
+ }
167
+
168
+ /**
169
+ * Distance from a point to the surface (Infinity when nothing is within `maxDistance`).
170
+ *
171
+ * @param {Vec3} p
172
+ * @param {number} [maxDistance=Infinity] - Stop looking past this (faster)
173
+ * @returns {number}
174
+ */
175
+ distance(p, maxDistance = Infinity) {
176
+ checkPoint(p, "distance: the point");
177
+ const found = this._nearest(p, maxDistance);
178
+ if (found < 0) return Infinity;
179
+ const c = this._near;
180
+ return Math.hypot(p.x - c[0], p.y - c[1], p.z - c[2]);
181
+ }
182
+
183
+ /** Unit normal of triangle `tri`, pointing out (clockwise faces). */
184
+ _normal(tri) {
185
+ const q = this._tris, i = tri * 9;
186
+ const e1x = q[i + 3] - q[i], e1y = q[i + 4] - q[i + 1], e1z = q[i + 5] - q[i + 2];
187
+ const e2x = q[i + 6] - q[i], e2y = q[i + 7] - q[i + 1], e2z = q[i + 8] - q[i + 2];
188
+ const nx = e1y * e2z - e1z * e2y, ny = e1z * e2x - e1x * e2z, nz = e1x * e2y - e1y * e2x;
189
+ const l = Math.hypot(nx, ny, nz) || 1;
190
+ return { x: nx / l, y: ny / l, z: nz / l };
191
+ }
192
+
193
+ _describe(tri, u, w, point, distance, dir) {
194
+ const normal = this._normal(tri);
195
+ const out = {
196
+ point,
197
+ normal,
198
+ distance,
199
+ face: this._faceOf[tri],
200
+ triangle: [this._corners[tri * 3], this._corners[tri * 3 + 1], this._corners[tri * 3 + 2]],
201
+ barycentric: [1 - u - w, u, w],
202
+ };
203
+ if (dir) out.front = dir.x * normal.x + dir.y * normal.y + dir.z * normal.z < 0;
204
+ return out;
205
+ }
206
+
207
+ /**
208
+ * First hit of the ray o + t·d (d any length), 0 < t < maxT in units of d.
209
+ * Fills `this._hit` and returns true when something is hit.
210
+ */
211
+ _cast(o, d, maxT, side) {
212
+ if (!this.triangleCount) return false;
213
+ const dl = Math.hypot(d.x, d.y, d.z);
214
+ const minT = this._minDist / dl, detEps = this._detEps * dl;
215
+ const sign = side === "front" ? 1 : side === "back" ? -1 : 0;
216
+ const ix = 1 / d.x, iy = 1 / d.y, iz = 1 / d.z;
217
+ const box = this._box, first = this._first, count = this._count, tris = this._tris, stack = this._stack;
218
+ const hit = this._hit;
219
+ let best = maxT, found = -1, bu = 0, bw = 0;
220
+ let top = 0;
221
+ if (enter(box, 0, o, ix, iy, iz, best) === Infinity) return false;
222
+ stack[top++] = 0;
223
+ while (top) {
224
+ const node = stack[--top];
225
+ const n = count[node];
226
+ if (n > 0) {
227
+ for (let k = first[node], end = first[node] + n; k < end; k++) {
228
+ const i = k * 9;
229
+ const e1x = tris[i + 3] - tris[i], e1y = tris[i + 4] - tris[i + 1], e1z = tris[i + 5] - tris[i + 2];
230
+ const e2x = tris[i + 6] - tris[i], e2y = tris[i + 7] - tris[i + 1], e2z = tris[i + 8] - tris[i + 2];
231
+ const px = d.y * e2z - d.z * e2y, py = d.z * e2x - d.x * e2z, pz = d.x * e2y - d.y * e2x;
232
+ const det = e1x * px + e1y * py + e1z * pz;
233
+ // det > 0: the ray meets the face from outside
234
+ if (Math.abs(det) <= detEps || det * sign < 0) continue;
235
+ const inv = 1 / det;
236
+ const sx = o.x - tris[i], sy = o.y - tris[i + 1], sz = o.z - tris[i + 2];
237
+ const u = (sx * px + sy * py + sz * pz) * inv;
238
+ if (u < 0 || u > 1) continue;
239
+ const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
240
+ const w = (d.x * qx + d.y * qy + d.z * qz) * inv;
241
+ if (w < 0 || u + w > 1) continue;
242
+ const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
243
+ if (t > minT && t < best) {
244
+ best = t;
245
+ found = k;
246
+ bu = u;
247
+ bw = w;
248
+ }
249
+ }
250
+ } else {
251
+ // Visit the nearer child first; skip boxes past the best hit
252
+ const a = first[node], b = a + 1;
253
+ const ta = enter(box, a * 6, o, ix, iy, iz, best), tb = enter(box, b * 6, o, ix, iy, iz, best);
254
+ if (ta <= tb) {
255
+ if (tb !== Infinity) stack[top++] = b;
256
+ if (ta !== Infinity) stack[top++] = a;
257
+ } else {
258
+ if (ta !== Infinity) stack[top++] = a;
259
+ stack[top++] = b;
260
+ }
261
+ }
262
+ }
263
+ if (found < 0) return false;
264
+ hit.t = best;
265
+ hit.tri = found;
266
+ hit.u = bu;
267
+ hit.w = bw;
268
+ return true;
269
+ }
270
+
271
+ /** How many faces a ray from p crosses, and whether every crossing was clear of edges. */
272
+ _crossings(p, [dx, dy, dz]) {
273
+ const tris = this._tris, box = this._box, first = this._first, count = this._count, stack = this._stack;
274
+ const ix = 1 / dx, iy = 1 / dy, iz = 1 / dz, minT = this._minDist, eps = this._edgeEps, detEps = this._detEps;
275
+ let hits = 0, sure = true, top = 0;
276
+ stack[top++] = 0;
277
+ while (top) {
278
+ const node = stack[--top];
279
+ if (enter(box, node * 6, p, ix, iy, iz, Infinity) === Infinity) continue;
280
+ const n = count[node];
281
+ if (n === 0) {
282
+ stack[top++] = first[node];
283
+ stack[top++] = first[node] + 1;
284
+ continue;
285
+ }
286
+ for (let k = first[node], end = first[node] + n; k < end; k++) {
287
+ const i = k * 9;
288
+ const e1x = tris[i + 3] - tris[i], e1y = tris[i + 4] - tris[i + 1], e1z = tris[i + 5] - tris[i + 2];
289
+ const e2x = tris[i + 6] - tris[i], e2y = tris[i + 7] - tris[i + 1], e2z = tris[i + 8] - tris[i + 2];
290
+ const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x;
291
+ const det = e1x * px + e1y * py + e1z * pz;
292
+ if (Math.abs(det) <= detEps) continue;
293
+ const inv = 1 / det;
294
+ const sx = p.x - tris[i], sy = p.y - tris[i + 1], sz = p.z - tris[i + 2];
295
+ const u = (sx * px + sy * py + sz * pz) * inv;
296
+ if (u < -eps || u > 1 + eps) continue;
297
+ const qx = sy * e1z - sz * e1y, qy = sz * e1x - sx * e1z, qz = sx * e1y - sy * e1x;
298
+ const w = (dx * qx + dy * qy + dz * qz) * inv;
299
+ if (w < -eps || u + w > 1 + eps) continue;
300
+ const t = (e2x * qx + e2y * qy + e2z * qz) * inv;
301
+ if (t <= -minT) continue;
302
+ // Near an edge or the start point: this ray can't be trusted
303
+ if (u < eps || w < eps || u + w > 1 - eps || t <= minT) sure = false;
304
+ if (t > minT && u >= 0 && w >= 0 && u + w <= 1) hits++;
305
+ }
306
+ }
307
+ return { count: hits, sure };
308
+ }
309
+
310
+ /** Nearest triangle to p within maxDist (its closest point in `this._near`), or -1. */
311
+ _nearest(p, maxDist) {
312
+ if (!this.triangleCount) return -1;
313
+ const box = this._box, first = this._first, count = this._count, tris = this._tris, stack = this._stack;
314
+ const near = this._near, tmp = new Float64Array(5);
315
+ let best2 = maxDist === Infinity ? Infinity : maxDist * maxDist, found = -1, top = 0;
316
+ if (boxDist2(box, 0, p) > best2) return -1;
317
+ stack[top++] = 0;
318
+ while (top) {
319
+ const node = stack[--top];
320
+ if (boxDist2(box, node * 6, p) > best2) continue;
321
+ const n = count[node];
322
+ if (n > 0) {
323
+ for (let k = first[node], end = first[node] + n; k < end; k++) {
324
+ const d2 = closestOnTriangle(tris, k * 9, p, tmp);
325
+ if (d2 <= best2) {
326
+ best2 = d2;
327
+ found = k;
328
+ near.set(tmp);
329
+ }
330
+ }
331
+ } else {
332
+ const a = first[node], b = a + 1;
333
+ const da = boxDist2(box, a * 6, p), db = boxDist2(box, b * 6, p);
334
+ // Push the farther first, so the nearer is looked at first
335
+ if (da <= db) {
336
+ if (db <= best2) stack[top++] = b;
337
+ if (da <= best2) stack[top++] = a;
338
+ } else {
339
+ if (da <= best2) stack[top++] = a;
340
+ if (db <= best2) stack[top++] = b;
341
+ }
342
+ }
343
+ }
344
+ return found;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Builds the BVH on `q`, sorting its triangles so each leaf's are together.
350
+ * Nodes: `_box` (6 numbers each), `_count` (> 0 for a leaf: its triangles
351
+ * from `_first`; 0 for an inner node: its children are `_first` and `_first + 1`).
352
+ */
353
+ function buildBVH(q) {
354
+ const n = q.triangleCount, src = q._tris;
355
+ const triBox = new Float64Array(n * 6), cen = new Float64Array(n * 3);
356
+ for (let t = 0; t < n; t++) {
357
+ for (let a = 0; a < 3; a++) {
358
+ const x = src[t * 9 + a], y = src[t * 9 + 3 + a], z = src[t * 9 + 6 + a];
359
+ const lo = Math.min(x, y, z), hi = Math.max(x, y, z);
360
+ triBox[t * 6 + a] = lo;
361
+ triBox[t * 6 + 3 + a] = hi;
362
+ cen[t * 3 + a] = (lo + hi) / 2;
363
+ }
364
+ }
365
+ const order = new Int32Array(n);
366
+ for (let t = 0; t < n; t++) order[t] = t;
367
+
368
+ const maxNodes = Math.max(1, 2 * n - 1);
369
+ const box = new Float64Array(maxNodes * 6), first = new Int32Array(maxNodes), count = new Int32Array(maxNodes);
370
+ let nodes = 1, depth = 1;
371
+ const binCount = new Int32Array(BINS), binBox = new Float64Array(BINS * 6);
372
+ const rightArea = new Float64Array(BINS), rightCount = new Int32Array(BINS);
373
+
374
+ const work = [[0, 0, n, 1]];
375
+ while (work.length) {
376
+ const [node, start, end, level] = work.pop();
377
+ depth = Math.max(depth, level);
378
+ // Node box and centroid box
379
+ const k = node * 6;
380
+ box.fill(Infinity, k, k + 3);
381
+ box.fill(-Infinity, k + 3, k + 6);
382
+ let cminx = Infinity, cminy = Infinity, cminz = Infinity, cmaxx = -Infinity, cmaxy = -Infinity, cmaxz = -Infinity;
383
+ for (let i = start; i < end; i++) {
384
+ const t = order[i];
385
+ for (let a = 0; a < 3; a++) {
386
+ if (triBox[t * 6 + a] < box[k + a]) box[k + a] = triBox[t * 6 + a];
387
+ if (triBox[t * 6 + 3 + a] > box[k + 3 + a]) box[k + 3 + a] = triBox[t * 6 + 3 + a];
388
+ }
389
+ const cx = cen[t * 3], cy = cen[t * 3 + 1], cz = cen[t * 3 + 2];
390
+ if (cx < cminx) cminx = cx;
391
+ if (cy < cminy) cminy = cy;
392
+ if (cz < cminz) cminz = cz;
393
+ if (cx > cmaxx) cmaxx = cx;
394
+ if (cy > cmaxy) cmaxy = cy;
395
+ if (cz > cmaxz) cmaxz = cz;
396
+ }
397
+ const size = end - start;
398
+ if (size <= LEAF) {
399
+ first[node] = start;
400
+ count[node] = size;
401
+ continue;
402
+ }
403
+
404
+ // Split along the longest side of the centroids' box
405
+ const ex = cmaxx - cminx, ey = cmaxy - cminy, ez = cmaxz - cminz;
406
+ const axis = ex >= ey && ex >= ez ? 0 : ey >= ez ? 1 : 2;
407
+ const lo = axis === 0 ? cminx : axis === 1 ? cminy : cminz;
408
+ const extent = axis === 0 ? ex : axis === 1 ? ey : ez;
409
+ let mid = -1;
410
+ if (extent > 0) {
411
+ // Surface area heuristic over BINS bins
412
+ const scale = BINS / extent;
413
+ const binOf = (t) => Math.min(BINS - 1, Math.floor((cen[t * 3 + axis] - lo) * scale));
414
+ binCount.fill(0);
415
+ for (let b = 0; b < BINS; b++) {
416
+ binBox.fill(Infinity, b * 6, b * 6 + 3);
417
+ binBox.fill(-Infinity, b * 6 + 3, b * 6 + 6);
418
+ }
419
+ for (let i = start; i < end; i++) {
420
+ const t = order[i], b = binOf(t);
421
+ binCount[b]++;
422
+ for (let a = 0; a < 3; a++) {
423
+ if (triBox[t * 6 + a] < binBox[b * 6 + a]) binBox[b * 6 + a] = triBox[t * 6 + a];
424
+ if (triBox[t * 6 + 3 + a] > binBox[b * 6 + 3 + a]) binBox[b * 6 + 3 + a] = triBox[t * 6 + 3 + a];
425
+ }
426
+ }
427
+ // Areas from the right, then sweep from the left
428
+ const acc = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];
429
+ let c = 0;
430
+ for (let b = BINS - 1; b > 0; b--) {
431
+ grow(acc, binBox, b * 6);
432
+ c += binCount[b];
433
+ rightArea[b] = c ? area(acc) : 0;
434
+ rightCount[b] = c;
435
+ }
436
+ acc.splice(0, 6, Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity);
437
+ c = 0;
438
+ let bestCost = Infinity, bestBin = -1;
439
+ for (let b = 1; b < BINS; b++) {
440
+ grow(acc, binBox, (b - 1) * 6);
441
+ c += binCount[b - 1];
442
+ if (!c || !rightCount[b]) continue;
443
+ const cost = area(acc) * c + rightArea[b] * rightCount[b];
444
+ if (cost < bestCost) {
445
+ bestCost = cost;
446
+ bestBin = b;
447
+ }
448
+ }
449
+ if (bestBin > 0) {
450
+ // Partition: bins below bestBin to the left
451
+ let i = start, j = end - 1;
452
+ while (i <= j) {
453
+ if (binOf(order[i]) < bestBin) i++;
454
+ else {
455
+ const s = order[i];
456
+ order[i] = order[j];
457
+ order[j--] = s;
458
+ }
459
+ }
460
+ mid = i;
461
+ }
462
+ }
463
+ // All centroids at one place, or no useful split: cut the list in half
464
+ if (mid <= start || mid >= end) mid = (start + end) >> 1;
465
+
466
+ const left = nodes;
467
+ nodes += 2;
468
+ first[node] = left;
469
+ count[node] = 0;
470
+ work.push([left + 1, mid, end, level + 1], [left, start, mid, level + 1]);
471
+ }
472
+
473
+ // Triangles in leaf order, so leaves read them in a row
474
+ const tris = new Float64Array(n * 9), corners = new Int32Array(n * 3), faceOf = new Int32Array(n);
475
+ for (let i = 0; i < n; i++) {
476
+ const t = order[i];
477
+ tris.set(src.subarray(t * 9, t * 9 + 9), i * 9);
478
+ corners.set(q._corners.subarray(t * 3, t * 3 + 3), i * 3);
479
+ faceOf[i] = q._faceOf[t];
480
+ }
481
+ q._tris = tris;
482
+ q._corners = corners;
483
+ q._faceOf = faceOf;
484
+ q._box = box.subarray(0, nodes * 6);
485
+ q._first = first.subarray(0, nodes);
486
+ q._count = count.subarray(0, nodes);
487
+ // An empty mesh: one empty leaf whose box nothing enters
488
+ if (!n) q._box.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
489
+ q._stack = new Int32Array(depth * 2 + 2);
490
+ }
491
+
492
+ function grow(acc, b, k) {
493
+ for (let a = 0; a < 3; a++) {
494
+ if (b[k + a] < acc[a]) acc[a] = b[k + a];
495
+ if (b[k + 3 + a] > acc[3 + a]) acc[3 + a] = b[k + 3 + a];
496
+ }
497
+ }
498
+
499
+ function area(b) {
500
+ const x = b[3] - b[0], y = b[4] - b[1], z = b[5] - b[2];
501
+ return x * y + y * z + z * x;
502
+ }
503
+
504
+ /** Where the ray o + t·(1/inv) enters box k, within [0, far]; Infinity if it doesn't. */
505
+ function enter(box, k, o, ix, iy, iz, far) {
506
+ let t0 = 0, t1 = far;
507
+ const lx = box[k], ly = box[k + 1], lz = box[k + 2], hx = box[k + 3], hy = box[k + 4], hz = box[k + 5];
508
+ if (ix === Infinity || ix === -Infinity) {
509
+ if (o.x < lx || o.x > hx) return Infinity;
510
+ } else {
511
+ let a = (lx - o.x) * ix, b = (hx - o.x) * ix;
512
+ if (a > b) [a, b] = [b, a];
513
+ if (a > t0) t0 = a;
514
+ if (b < t1) t1 = b;
515
+ }
516
+ if (iy === Infinity || iy === -Infinity) {
517
+ if (o.y < ly || o.y > hy) return Infinity;
518
+ } else {
519
+ let a = (ly - o.y) * iy, b = (hy - o.y) * iy;
520
+ if (a > b) [a, b] = [b, a];
521
+ if (a > t0) t0 = a;
522
+ if (b < t1) t1 = b;
523
+ }
524
+ if (iz === Infinity || iz === -Infinity) {
525
+ if (o.z < lz || o.z > hz) return Infinity;
526
+ } else {
527
+ let a = (lz - o.z) * iz, b = (hz - o.z) * iz;
528
+ if (a > b) [a, b] = [b, a];
529
+ if (a > t0) t0 = a;
530
+ if (b < t1) t1 = b;
531
+ }
532
+ // A little slack, so rays along a box's face aren't lost to rounding
533
+ return t0 <= t1 * (1 + 1e-12) + 1e-12 ? t0 : Infinity;
534
+ }
535
+
536
+ /** Squared distance from p to box k (0 inside). */
537
+ function boxDist2(box, k, p) {
538
+ const dx = Math.max(box[k] - p.x, 0, p.x - box[k + 3]);
539
+ const dy = Math.max(box[k + 1] - p.y, 0, p.y - box[k + 4]);
540
+ const dz = Math.max(box[k + 2] - p.z, 0, p.z - box[k + 5]);
541
+ return dx * dx + dy * dy + dz * dz;
542
+ }
543
+
544
+ /**
545
+ * Closest point on the triangle at `i` to p (Ericson's method): writes
546
+ * x, y, z and the weights of the 2nd and 3rd corners into `out`, returns
547
+ * the squared distance.
548
+ */
549
+ function closestOnTriangle(q, i, p, out) {
550
+ const ax = q[i], ay = q[i + 1], az = q[i + 2];
551
+ const abx = q[i + 3] - ax, aby = q[i + 4] - ay, abz = q[i + 5] - az;
552
+ const acx = q[i + 6] - ax, acy = q[i + 7] - ay, acz = q[i + 8] - az;
553
+ const apx = p.x - ax, apy = p.y - ay, apz = p.z - az;
554
+ const d1 = abx * apx + aby * apy + abz * apz, d2 = acx * apx + acy * apy + acz * apz;
555
+ let v, w;
556
+ if (d1 <= 0 && d2 <= 0) {
557
+ v = 0; w = 0;
558
+ } else {
559
+ const bpx = p.x - q[i + 3], bpy = p.y - q[i + 4], bpz = p.z - q[i + 5];
560
+ const d3 = abx * bpx + aby * bpy + abz * bpz, d4 = acx * bpx + acy * bpy + acz * bpz;
561
+ const cpx = p.x - q[i + 6], cpy = p.y - q[i + 7], cpz = p.z - q[i + 8];
562
+ const d5 = abx * cpx + aby * cpy + abz * cpz, d6 = acx * cpx + acy * cpy + acz * cpz;
563
+ const vc = d1 * d4 - d3 * d2, vb = d5 * d2 - d1 * d6, va = d3 * d6 - d5 * d4;
564
+ if (d3 >= 0 && d4 <= d3) {
565
+ v = 1; w = 0;
566
+ } else if (d6 >= 0 && d5 <= d6) {
567
+ v = 0; w = 1;
568
+ } else if (vc <= 0 && d1 >= 0 && d3 <= 0) {
569
+ v = d1 / (d1 - d3); w = 0;
570
+ } else if (vb <= 0 && d2 >= 0 && d6 <= 0) {
571
+ v = 0; w = d2 / (d2 - d6);
572
+ } else if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
573
+ w = (d4 - d3) / (d4 - d3 + (d5 - d6)); v = 1 - w;
574
+ } else {
575
+ const den = 1 / (va + vb + vc);
576
+ v = vb * den; w = vc * den;
577
+ }
578
+ }
579
+ const x = ax + abx * v + acx * w, y = ay + aby * v + acy * w, z = az + abz * v + acz * w;
580
+ out[0] = x; out[1] = y; out[2] = z; out[3] = v; out[4] = w;
581
+ return (p.x - x) ** 2 + (p.y - y) ** 2 + (p.z - z) ** 2;
582
+ }
583
+
584
+ // One-off questions: each builds a query and throws it away. For many
585
+ // questions on one mesh, use meshQuery.
586
+
587
+ /** The first place a ray meets a mesh, or null. Same as `meshQuery(mesh).raycast(...)`. */
588
+ export function raycastMesh(mesh, origin, direction, opts) {
589
+ return meshQuery(mesh).raycast(origin, direction, opts);
590
+ }
591
+
592
+ /** Whether a point is inside a closed mesh. Same as `meshQuery(mesh).contains(p)`. */
593
+ export function pointInMesh(mesh, p) {
594
+ return meshQuery(mesh).contains(p);
595
+ }
596
+
597
+ /** Distance from a point to a mesh's surface. Same as `meshQuery(mesh).distance(p, maxDistance)`. */
598
+ export function distanceToMesh(mesh, p, maxDistance) {
599
+ return meshQuery(mesh).distance(p, maxDistance);
600
+ }
601
+
602
+ /** The nearest point of a mesh's surface. Same as `meshQuery(mesh).closest(p, opts)`. */
603
+ export function closestPointOnMesh(mesh, p, opts) {
604
+ return meshQuery(mesh).closest(p, opts);
605
+ }
606
+
607
+ /**
608
+ * Whether a mesh is closed (watertight): every edge, matched by vertex
609
+ * position (texture seams split vertices), is shared by exactly two faces.
610
+ * Only a closed mesh has an inside.
611
+ *
612
+ * @param {Object} mesh
613
+ * @returns {boolean}
614
+ */
615
+ export function isClosed(mesh) {
616
+ const v = mesh?.vertices ?? [];
617
+ if (!mesh?.faces?.length || !v.length) return false;
618
+ let size = 0;
619
+ for (const p of v) if (finite(p)) size = Math.max(size, Math.abs(p.x), Math.abs(p.y), Math.abs(p.z));
620
+ const same = samePlace(v, (size || 1) * 1e-7);
621
+ const edges = new Map();
622
+ for (const f of mesh.faces) {
623
+ if (!Array.isArray(f)) return false;
624
+ for (let i = 0; i < f.length; i++) {
625
+ const a = same[f[i]], b = same[f[(i + 1) % f.length]];
626
+ if (a === undefined || b === undefined) return false;
627
+ if (a === b) continue; // a collapsed edge (a sphere's poles)
628
+ const key = a < b ? `${a},${b}` : `${b},${a}`;
629
+ edges.set(key, (edges.get(key) ?? 0) + 1);
630
+ }
631
+ }
632
+ if (!edges.size) return false;
633
+ for (const n of edges.values()) if (n !== 2) return false;
634
+ return true;
635
+ }
636
+
637
+ // Scene rays
638
+
639
+ /** Prepared queries for meshes in scenes, rebuilt when a mesh changes. */
640
+ const cache = new WeakMap();
641
+
642
+ function cachedQuery(mesh) {
643
+ const c = cache.get(mesh);
644
+ if (c && !mesh.dirty && c.vertices === mesh.vertices && c.faces === mesh.faces
645
+ && c.vertexCount === mesh.vertices.length && c.faceCount === mesh.faces.length) return c.query;
646
+ const query = meshQuery(mesh);
647
+ cache.set(mesh, { query, vertices: mesh.vertices, faces: mesh.faces, vertexCount: mesh.vertices.length, faceCount: mesh.faces.length });
648
+ return query;
649
+ }
650
+
651
+ /**
652
+ * Forgets the prepared query `raycastScene` keeps for a mesh, so the next
653
+ * ray rebuilds it. Needed only after changing a mesh's vertices in place
654
+ * once a renderer has already cleared its `dirty` flag.
655
+ *
656
+ * @param {Object} mesh
657
+ */
658
+ export function clearQueryCache(mesh) {
659
+ if (mesh) cache.delete(mesh);
660
+ }
661
+
662
+ /**
663
+ * The first object a ray meets in a scene, in world space: the 3D version
664
+ * of `pickHit`. Each object's position, rotation, scale and groups are
665
+ * taken into account. Hidden objects, objects with `pickable: false` and
666
+ * objects without faces are skipped.
667
+ *
668
+ * Each mesh's query is prepared once and kept until the mesh changes
669
+ * (new arrays, a different count, or `mesh.dirty`).
670
+ *
671
+ * @param {Object} scene - A Scene, or a list of objects
672
+ * @param {Vec3} origin - In the world
673
+ * @param {Vec3} direction - In the world, any length but zero
674
+ * @param {Object} [opts]
675
+ * @param {(obj:Object) => boolean} [opts.filter] - Return false to skip an object
676
+ * @param {number} [opts.maxDistance=Infinity]
677
+ * @param {"both"|"front"|"back"} [opts.side="both"]
678
+ * @param {Object[]} [opts.objects] - Only these objects (and their children)
679
+ * @returns {Object|null} The hit: object, path, point, local, normal, distance, face, front, triangle, barycentric
680
+ */
681
+ export function raycastScene(scene, origin, direction, { filter = () => true, maxDistance = Infinity, side = "both", objects } = {}) {
682
+ checkPoint(origin, "raycastScene: origin");
683
+ checkSide(side);
684
+ const d = unitDirection(direction);
685
+ let best = maxDistance, hit = null;
686
+ for (const object of worldObjects(objects ?? scene)) {
687
+ const mesh = object.mesh;
688
+ if (!mesh?.faces?.length || !Array.isArray(mesh.vertices) || !isPickable(object) || !filter(object)) continue;
689
+ const world = object.world;
690
+ if (!(Math.abs(determinantMat4(world)) > 1e-18)) continue; // scaled to nothing
691
+ const inv = invertMat4(world);
692
+ // The ray in the mesh's space; t stays the world distance since d is unit length
693
+ const lo = transformPoint(inv, origin), ld = transformDirection(inv, d);
694
+ const q = cachedQuery(mesh);
695
+ if (!q._cast(lo, ld, best, side)) continue;
696
+ const { t, tri, u, w } = q._hit;
697
+ best = t;
698
+ hit = { object, world, q, tri, u, w, t, lo, ld };
699
+ }
700
+ if (!hit) return null;
701
+ const { object, world, q, tri, u, w, t, lo, ld } = hit;
702
+ const local = { x: lo.x + ld.x * t, y: lo.y + ld.y * t, z: lo.z + ld.z * t };
703
+ const out = q._describe(tri, u, w, { x: origin.x + d.x * t, y: origin.y + d.y * t, z: origin.z + d.z * t }, t, ld);
704
+ out.normal = transformNormal(normalMatrix(world), out.normal);
705
+ return { object, path: ancestors(object), local, ...out };
706
+ }