@flighthq/spatial 0.4.0-next.1752.f250cb9 → 0.4.0-next.1763.f98ae25

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,3 @@
1
+ import type { SpatialIndexBackend3D } from '@flighthq/types/contract';
2
+ export declare function createBvhSpatialBackend3D(margin?: number): SpatialIndexBackend3D;
3
+ //# sourceMappingURL=bvh3D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh3D.d.ts","sourceRoot":"","sources":["../src/bvh3D.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EAKtB,MAAM,0BAA0B,CAAC;AA4BlC,wBAAgB,yBAAyB,CAAC,MAAM,SAAwB,GAAG,qBAAqB,CAa/F"}
package/dist/bvh3D.js ADDED
@@ -0,0 +1,545 @@
1
+ import { reportSpatialIndexing } from './spatialIndexingGuard';
2
+ // A dynamic bounding-volume hierarchy: the second 3D backend, beside the uniform grid.
3
+ //
4
+ // The grid is the right structure when objects are roughly one size and the world has a scale; a tree
5
+ // is the right one when neither holds. A grid cell is a fixed world length, so an object much larger
6
+ // than a cell occupies many of them and an object much smaller wastes one — and an unbounded world has
7
+ // no cell size that is right anywhere. A tree adapts: it subdivides where objects are and nowhere else,
8
+ // costs O(log n) per query regardless of extent, and has no tuning constant that can be wrong.
9
+ //
10
+ // It is DYNAMIC in the specific sense that leaves carry FAT bounds — the object's box grown by a
11
+ // margin — so a body that moves a little stays inside the bounds already in the tree and needs no
12
+ // reinsertion at all. That is what makes it usable for a physics broadphase, where nearly every object
13
+ // moves nearly every step: without the margin, every step would be a full remove-and-reinsert.
14
+ //
15
+ // The margin buys cheap updates at the cost of looser traversal, which is why queries test a leaf's
16
+ // EXACT bounds before publishing it. Traversal uses the fat box; the answer uses the real one. Skipping
17
+ // that second test would make this backend report overlaps the grid does not, and the two are supposed
18
+ // to be interchangeable.
19
+ // Creates a BVH backend. `margin` is how far each leaf's stored box is grown beyond its object; a
20
+ // larger margin means fewer reinsertions and looser internal nodes.
21
+ //
22
+ // The default suits a world whose objects are order-of-magnitude single-digit units and move a few
23
+ // units per step. It is a world-space LENGTH, like the grid's cell size, so the two tune against the
24
+ // same intuition.
25
+ export function createBvhSpatialBackend3D(margin = DEFAULT_BVH_MARGIN_3D) {
26
+ const tree = createBvh3D(margin);
27
+ return {
28
+ clearSpatialIndex: () => clearBvh3D(tree),
29
+ explainSpatialIndexing: (id) => explainBvh3D(tree, id),
30
+ insertSpatialObject: (id, bounds) => insertBvh3D(tree, id, bounds, 'insert'),
31
+ querySpatialPairs: (out) => queryBvh3DPairs(tree, out),
32
+ querySpatialPoint: (x, y, z, out) => queryBvh3DPoint(tree, x, y, z, out),
33
+ querySpatialRay: (x, y, z, dx, dy, dz, out) => queryBvh3DRay(tree, x, y, z, dx, dy, dz, out),
34
+ querySpatialRegion: (region, out) => queryBvh3DRegion(tree, region, out),
35
+ removeSpatialObject: (id) => removeBvh3D(tree, id),
36
+ updateSpatialObject: (id, bounds) => insertBvh3D(tree, id, bounds, 'update'),
37
+ };
38
+ }
39
+ function createBvh3D(margin) {
40
+ return {
41
+ bounds: new Map(),
42
+ child1: [],
43
+ child2: [],
44
+ count: 0,
45
+ declined: new Map(),
46
+ freeList: [],
47
+ height: [],
48
+ leafByObject: new Map(),
49
+ margin: Number.isFinite(margin) && margin >= 0 ? margin : 0,
50
+ maxX: [],
51
+ maxY: [],
52
+ maxZ: [],
53
+ minX: [],
54
+ minY: [],
55
+ minZ: [],
56
+ object: [],
57
+ parent: [],
58
+ root: NIL,
59
+ stack: [],
60
+ };
61
+ }
62
+ function clearBvh3D(tree) {
63
+ tree.root = NIL;
64
+ tree.count = 0;
65
+ tree.freeList.length = 0;
66
+ tree.minX.length = 0;
67
+ tree.minY.length = 0;
68
+ tree.minZ.length = 0;
69
+ tree.maxX.length = 0;
70
+ tree.maxY.length = 0;
71
+ tree.maxZ.length = 0;
72
+ tree.parent.length = 0;
73
+ tree.child1.length = 0;
74
+ tree.child2.length = 0;
75
+ tree.height.length = 0;
76
+ tree.object.length = 0;
77
+ tree.leafByObject.clear();
78
+ tree.bounds.clear();
79
+ tree.declined.clear();
80
+ }
81
+ // A tree reports `cells` for an indexed object and a bucket count of zero.
82
+ //
83
+ // The mode vocabulary is shared with the grid and is grid-SHAPED — `cells` names a structure this one
84
+ // does not have. It is used anyway because it is the only member meaning "indexed normally", and
85
+ // `SpatialIndexingExplanation` already anticipates a bucketless structure by defining `bucketCount` as
86
+ // zero for one. Renaming the mode would change a vocabulary every backend and both dimensions share.
87
+ function explainBvh3D(tree, id) {
88
+ const reason = tree.declined.get(id);
89
+ if (reason !== undefined)
90
+ return { bucketCount: 0, id, mode: 'declined', reason };
91
+ if (!tree.leafByObject.has(id))
92
+ return { bucketCount: 0, id, mode: 'absent', reason: null };
93
+ return { bucketCount: 0, id, mode: 'cells', reason: null };
94
+ }
95
+ function insertBvh3D(tree, id, bounds, operation) {
96
+ if (!Number.isFinite(bounds.minX) ||
97
+ !Number.isFinite(bounds.minY) ||
98
+ !Number.isFinite(bounds.minZ) ||
99
+ !Number.isFinite(bounds.maxX) ||
100
+ !Number.isFinite(bounds.maxY) ||
101
+ !Number.isFinite(bounds.maxZ)) {
102
+ removeBvh3D(tree, id);
103
+ tree.declined.set(id, 'non-finite-bounds');
104
+ reportBvh3DIndexing(tree, id, 'declined', operation, 'non-finite-bounds');
105
+ return false;
106
+ }
107
+ if (bounds.maxX < bounds.minX || bounds.maxY < bounds.minY || bounds.maxZ < bounds.minZ) {
108
+ removeBvh3D(tree, id);
109
+ tree.declined.set(id, 'inverted-bounds');
110
+ reportBvh3DIndexing(tree, id, 'declined', operation, 'inverted-bounds');
111
+ return false;
112
+ }
113
+ tree.declined.delete(id);
114
+ const existing = tree.leafByObject.get(id);
115
+ if (existing !== undefined) {
116
+ // The whole point of the margin: if the object still sits inside the box already in the tree, the
117
+ // tree does not change at all. Only the exact bounds are refreshed, because queries answer from
118
+ // those and a stale one would report last step's overlap as this step's.
119
+ if (bounds.minX >= tree.minX[existing] &&
120
+ bounds.minY >= tree.minY[existing] &&
121
+ bounds.minZ >= tree.minZ[existing] &&
122
+ bounds.maxX <= tree.maxX[existing] &&
123
+ bounds.maxY <= tree.maxY[existing] &&
124
+ bounds.maxZ <= tree.maxZ[existing]) {
125
+ copyBounds3D(bounds, tree.bounds.get(id));
126
+ reportBvh3DIndexing(tree, id, 'cells', operation, null);
127
+ return true;
128
+ }
129
+ removeBvh3D(tree, id);
130
+ }
131
+ const leaf = allocateBvh3DNode(tree);
132
+ const margin = tree.margin;
133
+ tree.minX[leaf] = bounds.minX - margin;
134
+ tree.minY[leaf] = bounds.minY - margin;
135
+ tree.minZ[leaf] = bounds.minZ - margin;
136
+ tree.maxX[leaf] = bounds.maxX + margin;
137
+ tree.maxY[leaf] = bounds.maxY + margin;
138
+ tree.maxZ[leaf] = bounds.maxZ + margin;
139
+ tree.object[leaf] = id;
140
+ tree.height[leaf] = 0;
141
+ tree.child1[leaf] = NIL;
142
+ tree.child2[leaf] = NIL;
143
+ tree.leafByObject.set(id, leaf);
144
+ tree.bounds.set(id, {
145
+ maxX: bounds.maxX,
146
+ maxY: bounds.maxY,
147
+ maxZ: bounds.maxZ,
148
+ minX: bounds.minX,
149
+ minY: bounds.minY,
150
+ minZ: bounds.minZ,
151
+ });
152
+ tree.count += 1;
153
+ insertBvh3DLeaf(tree, leaf);
154
+ reportBvh3DIndexing(tree, id, 'cells', operation, null);
155
+ return true;
156
+ }
157
+ function removeBvh3D(tree, id) {
158
+ tree.declined.delete(id);
159
+ const leaf = tree.leafByObject.get(id);
160
+ if (leaf === undefined)
161
+ return;
162
+ tree.leafByObject.delete(id);
163
+ tree.bounds.delete(id);
164
+ tree.count -= 1;
165
+ removeBvh3DLeaf(tree, leaf);
166
+ freeBvh3DNode(tree, leaf);
167
+ }
168
+ // Descends to the leaf whose enclosing box grows LEAST by taking this one, which is the surface-area
169
+ // heuristic in its simplest form. Descending by centre distance instead builds a tree that looks
170
+ // balanced and traverses badly, because a node's cost is the area a ray or box must test against, not
171
+ // how evenly its children are counted.
172
+ function insertBvh3DLeaf(tree, leaf) {
173
+ if (tree.root === NIL) {
174
+ tree.root = leaf;
175
+ tree.parent[leaf] = NIL;
176
+ return;
177
+ }
178
+ let index = tree.root;
179
+ while (tree.height[index] > 0) {
180
+ const child1 = tree.child1[index];
181
+ const child2 = tree.child2[index];
182
+ const area = boundsArea(tree, index);
183
+ const combined = combinedArea(tree, index, leaf);
184
+ // The cost of putting the leaf here rather than deeper: a new parent covering both, plus the growth
185
+ // this node's own box would take on the way down.
186
+ const branchCost = 2 * combined;
187
+ const inheritanceCost = 2 * (combined - area);
188
+ const cost1 = descentCost(tree, child1, leaf, inheritanceCost);
189
+ const cost2 = descentCost(tree, child2, leaf, inheritanceCost);
190
+ if (branchCost < cost1 && branchCost < cost2)
191
+ break;
192
+ index = cost1 < cost2 ? child1 : child2;
193
+ }
194
+ const sibling = index;
195
+ const oldParent = tree.parent[sibling];
196
+ const newParent = allocateBvh3DNode(tree);
197
+ tree.parent[newParent] = oldParent;
198
+ tree.object[newParent] = NIL;
199
+ tree.child1[newParent] = sibling;
200
+ tree.child2[newParent] = leaf;
201
+ tree.height[newParent] = tree.height[sibling] + 1;
202
+ unionBounds(tree, newParent, sibling, leaf);
203
+ tree.parent[sibling] = newParent;
204
+ tree.parent[leaf] = newParent;
205
+ if (oldParent === NIL)
206
+ tree.root = newParent;
207
+ else if (tree.child1[oldParent] === sibling)
208
+ tree.child1[oldParent] = newParent;
209
+ else
210
+ tree.child2[oldParent] = newParent;
211
+ refitBvh3DAncestors(tree, tree.parent[leaf]);
212
+ }
213
+ function removeBvh3DLeaf(tree, leaf) {
214
+ if (leaf === tree.root) {
215
+ tree.root = NIL;
216
+ return;
217
+ }
218
+ const parent = tree.parent[leaf];
219
+ const grandParent = tree.parent[parent];
220
+ const sibling = tree.child1[parent] === leaf ? tree.child2[parent] : tree.child1[parent];
221
+ // The parent existed only to hold two children; with one gone it is pure indirection, so the sibling
222
+ // takes its place rather than the parent surviving with a single child.
223
+ if (grandParent === NIL) {
224
+ tree.root = sibling;
225
+ tree.parent[sibling] = NIL;
226
+ freeBvh3DNode(tree, parent);
227
+ return;
228
+ }
229
+ if (tree.child1[grandParent] === parent)
230
+ tree.child1[grandParent] = sibling;
231
+ else
232
+ tree.child2[grandParent] = sibling;
233
+ tree.parent[sibling] = grandParent;
234
+ freeBvh3DNode(tree, parent);
235
+ refitBvh3DAncestors(tree, grandParent);
236
+ }
237
+ // Walks to the root re-deriving each node's box and height, rebalancing as it goes.
238
+ //
239
+ // Both must happen on the way UP and in that order: a node's box is the union of its children's, so a
240
+ // change at a leaf is only visible to a query once every ancestor has been rewritten. Refitting lazily
241
+ // would leave a parent claiming a region its children no longer cover, which loses objects silently
242
+ // rather than loudly.
243
+ function refitBvh3DAncestors(tree, start) {
244
+ let index = start;
245
+ while (index !== NIL) {
246
+ index = balanceBvh3D(tree, index);
247
+ const child1 = tree.child1[index];
248
+ const child2 = tree.child2[index];
249
+ tree.height[index] = 1 + Math.max(tree.height[child1], tree.height[child2]);
250
+ unionBounds(tree, index, child1, child2);
251
+ index = tree.parent[index];
252
+ }
253
+ }
254
+ // One AVL-style rotation when a node's children differ in height by more than one, returning whichever
255
+ // node now occupies the position. Without it a run of sorted insertions degenerates to a list and every
256
+ // query becomes linear.
257
+ function balanceBvh3D(tree, index) {
258
+ if (tree.height[index] < 2)
259
+ return index;
260
+ const child1 = tree.child1[index];
261
+ const child2 = tree.child2[index];
262
+ const balance = tree.height[child2] - tree.height[child1];
263
+ if (balance > 1)
264
+ return rotateBvh3D(tree, index, child2);
265
+ if (balance < -1)
266
+ return rotateBvh3D(tree, index, child1);
267
+ return index;
268
+ }
269
+ // Promotes `heavy` above `index`, re-parenting whichever of its children is taller to keep the result
270
+ // balanced.
271
+ function rotateBvh3D(tree, index, heavy) {
272
+ const heavyChild1 = tree.child1[heavy];
273
+ const heavyChild2 = tree.child2[heavy];
274
+ tree.child1[heavy] = index;
275
+ tree.parent[heavy] = tree.parent[index];
276
+ tree.parent[index] = heavy;
277
+ const grandParent = tree.parent[heavy];
278
+ if (grandParent === NIL)
279
+ tree.root = heavy;
280
+ else if (tree.child1[grandParent] === index)
281
+ tree.child1[grandParent] = heavy;
282
+ else
283
+ tree.child2[grandParent] = heavy;
284
+ const promote = tree.height[heavyChild1] > tree.height[heavyChild2] ? heavyChild1 : heavyChild2;
285
+ const demote = promote === heavyChild1 ? heavyChild2 : heavyChild1;
286
+ tree.child2[heavy] = promote;
287
+ if (tree.child1[index] === heavy)
288
+ tree.child1[index] = demote;
289
+ else
290
+ tree.child2[index] = demote;
291
+ tree.parent[demote] = index;
292
+ unionBounds(tree, index, tree.child1[index], tree.child2[index]);
293
+ unionBounds(tree, heavy, tree.child1[heavy], tree.child2[heavy]);
294
+ tree.height[index] = 1 + Math.max(tree.height[tree.child1[index]], tree.height[tree.child2[index]]);
295
+ tree.height[heavy] = 1 + Math.max(tree.height[tree.child1[heavy]], tree.height[tree.child2[heavy]]);
296
+ return heavy;
297
+ }
298
+ function queryBvh3DPairs(tree, out) {
299
+ out.length = 0;
300
+ if (tree.root === NIL)
301
+ return;
302
+ for (const [id, leaf] of tree.leafByObject) {
303
+ const exact = tree.bounds.get(id);
304
+ if (exact === undefined)
305
+ continue;
306
+ const stack = tree.stack;
307
+ stack.length = 0;
308
+ stack.push(tree.root);
309
+ while (stack.length > 0) {
310
+ const node = stack.pop();
311
+ if (node === NIL || node === leaf)
312
+ continue;
313
+ if (!nodeOverlapsBounds(tree, node, exact))
314
+ continue;
315
+ if (tree.height[node] === 0) {
316
+ const other = tree.object[node];
317
+ // Each unordered pair once. Comparing ids rather than tracking a seen-set is what keeps this
318
+ // allocation-free, and it is why a pair is emitted by the lower-id member only.
319
+ if (other <= id)
320
+ continue;
321
+ const otherExact = tree.bounds.get(other);
322
+ if (otherExact !== undefined && boundsOverlap3D(exact, otherExact))
323
+ out.push({ a: id, b: other });
324
+ continue;
325
+ }
326
+ stack.push(tree.child1[node]);
327
+ stack.push(tree.child2[node]);
328
+ }
329
+ }
330
+ }
331
+ function queryBvh3DPoint(tree, x, y, z, out) {
332
+ out.length = 0;
333
+ if (tree.root === NIL)
334
+ return;
335
+ const stack = tree.stack;
336
+ stack.length = 0;
337
+ stack.push(tree.root);
338
+ while (stack.length > 0) {
339
+ const node = stack.pop();
340
+ if (node === NIL)
341
+ continue;
342
+ if (x < tree.minX[node] ||
343
+ x > tree.maxX[node] ||
344
+ y < tree.minY[node] ||
345
+ y > tree.maxY[node] ||
346
+ z < tree.minZ[node] ||
347
+ z > tree.maxZ[node]) {
348
+ continue;
349
+ }
350
+ if (tree.height[node] === 0) {
351
+ const exact = tree.bounds.get(tree.object[node]);
352
+ // Half-open [min, max) on every axis, matching the grid: a point exactly on a maximum face is
353
+ // outside. Sharing the convention is what keeps a point on a shared face from belonging to two
354
+ // adjacent boxes in one backend and one box in the other.
355
+ if (exact !== undefined &&
356
+ x >= exact.minX &&
357
+ x < exact.maxX &&
358
+ y >= exact.minY &&
359
+ y < exact.maxY &&
360
+ z >= exact.minZ &&
361
+ z < exact.maxZ) {
362
+ out.push(tree.object[node]);
363
+ }
364
+ continue;
365
+ }
366
+ stack.push(tree.child1[node]);
367
+ stack.push(tree.child2[node]);
368
+ }
369
+ }
370
+ function queryBvh3DRay(tree, x, y, z, dx, dy, dz, out) {
371
+ out.length = 0;
372
+ if (tree.root === NIL)
373
+ return;
374
+ const stack = tree.stack;
375
+ stack.length = 0;
376
+ stack.push(tree.root);
377
+ while (stack.length > 0) {
378
+ const node = stack.pop();
379
+ if (node === NIL)
380
+ continue;
381
+ if (!raySlabsHit(x, y, z, dx, dy, dz, tree.minX[node], tree.minY[node], tree.minZ[node], tree.maxX[node], tree.maxY[node], tree.maxZ[node])) {
382
+ continue;
383
+ }
384
+ if (tree.height[node] === 0) {
385
+ const exact = tree.bounds.get(tree.object[node]);
386
+ if (exact !== undefined &&
387
+ raySlabsHit(x, y, z, dx, dy, dz, exact.minX, exact.minY, exact.minZ, exact.maxX, exact.maxY, exact.maxZ)) {
388
+ out.push(tree.object[node]);
389
+ }
390
+ continue;
391
+ }
392
+ stack.push(tree.child1[node]);
393
+ stack.push(tree.child2[node]);
394
+ }
395
+ }
396
+ function queryBvh3DRegion(tree, region, out) {
397
+ out.length = 0;
398
+ if (tree.root === NIL)
399
+ return;
400
+ const stack = tree.stack;
401
+ stack.length = 0;
402
+ stack.push(tree.root);
403
+ while (stack.length > 0) {
404
+ const node = stack.pop();
405
+ if (node === NIL || !nodeOverlapsBounds(tree, node, region))
406
+ continue;
407
+ if (tree.height[node] === 0) {
408
+ const exact = tree.bounds.get(tree.object[node]);
409
+ if (exact !== undefined && boundsOverlap3D(exact, region))
410
+ out.push(tree.object[node]);
411
+ continue;
412
+ }
413
+ stack.push(tree.child1[node]);
414
+ stack.push(tree.child2[node]);
415
+ }
416
+ }
417
+ function allocateBvh3DNode(tree) {
418
+ const recycled = tree.freeList.pop();
419
+ if (recycled !== undefined) {
420
+ tree.parent[recycled] = NIL;
421
+ tree.child1[recycled] = NIL;
422
+ tree.child2[recycled] = NIL;
423
+ tree.height[recycled] = 0;
424
+ tree.object[recycled] = NIL;
425
+ return recycled;
426
+ }
427
+ const index = tree.minX.length;
428
+ tree.minX.push(0);
429
+ tree.minY.push(0);
430
+ tree.minZ.push(0);
431
+ tree.maxX.push(0);
432
+ tree.maxY.push(0);
433
+ tree.maxZ.push(0);
434
+ tree.parent.push(NIL);
435
+ tree.child1.push(NIL);
436
+ tree.child2.push(NIL);
437
+ tree.height.push(0);
438
+ tree.object.push(NIL);
439
+ return index;
440
+ }
441
+ function freeBvh3DNode(tree, index) {
442
+ tree.parent[index] = NIL;
443
+ tree.child1[index] = NIL;
444
+ tree.child2[index] = NIL;
445
+ tree.object[index] = NIL;
446
+ tree.height[index] = -1;
447
+ tree.freeList.push(index);
448
+ }
449
+ function boundsArea(tree, index) {
450
+ const dx = tree.maxX[index] - tree.minX[index];
451
+ const dy = tree.maxY[index] - tree.minY[index];
452
+ const dz = tree.maxZ[index] - tree.minZ[index];
453
+ return 2 * (dx * dy + dy * dz + dz * dx);
454
+ }
455
+ // STRICT on every axis, so two boxes that merely touch do not overlap — the convention the uniform grid
456
+ // already uses. The two backends sit behind one seam and a caller may swap them, so a boundary case must
457
+ // not answer differently depending on which is installed. Node traversal above stays INCLUSIVE on
458
+ // purpose: a conservative descent may visit a leaf that then fails this test, but it must never prune one
459
+ // that would have passed it.
460
+ function boundsOverlap3D(a, b) {
461
+ return a.minX < b.maxX && a.maxX > b.minX && a.minY < b.maxY && a.maxY > b.minY && a.minZ < b.maxZ && a.maxZ > b.minZ;
462
+ }
463
+ function combinedArea(tree, a, b) {
464
+ const dx = Math.max(tree.maxX[a], tree.maxX[b]) - Math.min(tree.minX[a], tree.minX[b]);
465
+ const dy = Math.max(tree.maxY[a], tree.maxY[b]) - Math.min(tree.minY[a], tree.minY[b]);
466
+ const dz = Math.max(tree.maxZ[a], tree.maxZ[b]) - Math.min(tree.minZ[a], tree.minZ[b]);
467
+ return 2 * (dx * dy + dy * dz + dz * dx);
468
+ }
469
+ function copyBounds3D(source, target) {
470
+ target.minX = source.minX;
471
+ target.minY = source.minY;
472
+ target.minZ = source.minZ;
473
+ target.maxX = source.maxX;
474
+ target.maxY = source.maxY;
475
+ target.maxZ = source.maxZ;
476
+ }
477
+ // What descending into `child` would cost. A leaf costs the box that would enclose it and the newcomer;
478
+ // an internal node costs only the GROWTH, because its existing area is already paid for.
479
+ function descentCost(tree, child, leaf, inheritanceCost) {
480
+ const combined = combinedArea(tree, child, leaf);
481
+ if (tree.height[child] === 0)
482
+ return combined + inheritanceCost;
483
+ return combined - boundsArea(tree, child) + inheritanceCost;
484
+ }
485
+ function nodeOverlapsBounds(tree, index, region) {
486
+ return (tree.minX[index] <= region.maxX &&
487
+ tree.maxX[index] >= region.minX &&
488
+ tree.minY[index] <= region.maxY &&
489
+ tree.maxY[index] >= region.minY &&
490
+ tree.minZ[index] <= region.maxZ &&
491
+ tree.maxZ[index] >= region.minZ);
492
+ }
493
+ // Slab test for a FORWARD ray, matching the uniform grid's reading: the ray extends from its origin in
494
+ // the direction given and not behind it. A zero component is parallel to that pair of planes, which is
495
+ // a hit exactly when the origin already lies between them.
496
+ function raySlabsHit(x, y, z, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ) {
497
+ let near = 0;
498
+ let far = Number.POSITIVE_INFINITY;
499
+ for (let axis = 0; axis < 3; axis += 1) {
500
+ const origin = axis === 0 ? x : axis === 1 ? y : z;
501
+ const direction = axis === 0 ? dx : axis === 1 ? dy : dz;
502
+ const low = axis === 0 ? minX : axis === 1 ? minY : minZ;
503
+ const high = axis === 0 ? maxX : axis === 1 ? maxY : maxZ;
504
+ if (direction === 0) {
505
+ if (origin < low || origin > high)
506
+ return false;
507
+ continue;
508
+ }
509
+ const inverse = 1 / direction;
510
+ let t0 = (low - origin) * inverse;
511
+ let t1 = (high - origin) * inverse;
512
+ if (t0 > t1) {
513
+ const swap = t0;
514
+ t0 = t1;
515
+ t1 = swap;
516
+ }
517
+ if (t0 > near)
518
+ near = t0;
519
+ if (t1 < far)
520
+ far = t1;
521
+ if (near > far)
522
+ return false;
523
+ }
524
+ return true;
525
+ }
526
+ function reportBvh3DIndexing(tree, id, mode, operation, reason) {
527
+ // `cellSize` is the grid's tuning constant and a tree has none, so the margin is reported in its
528
+ // place: it is the number a caller would tune for the same reason, and leaving the field at zero
529
+ // would tell a diagnostic reader the structure was misconfigured.
530
+ reportSpatialIndexing({ cellSize: tree.margin, id, mode, operation, reason, wouldOccupyBucketCount: 0 });
531
+ }
532
+ function unionBounds(tree, target, a, b) {
533
+ tree.minX[target] = Math.min(tree.minX[a], tree.minX[b]);
534
+ tree.minY[target] = Math.min(tree.minY[a], tree.minY[b]);
535
+ tree.minZ[target] = Math.min(tree.minZ[a], tree.minZ[b]);
536
+ tree.maxX[target] = Math.max(tree.maxX[a], tree.maxX[b]);
537
+ tree.maxY[target] = Math.max(tree.maxY[a], tree.maxY[b]);
538
+ tree.maxZ[target] = Math.max(tree.maxZ[a], tree.maxZ[b]);
539
+ }
540
+ // The sentinel for "no node", matching the -1 an index into a parallel array cannot hold.
541
+ const NIL = -1;
542
+ // A world-space length, chosen against the same intuition as the grid's default cell size: a few units,
543
+ // suiting objects of order single-digit units moving a few units per step.
544
+ const DEFAULT_BVH_MARGIN_3D = 2;
545
+ //# sourceMappingURL=bvh3D.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bvh3D.js","sourceRoot":"","sources":["../src/bvh3D.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  export * from './explainSpatialIndexing2D';
2
+ export * from './bvh3D';
2
3
  export * from './explainSpatialIndexing3D';
3
4
  export * from './formatSpatialIndexingNotice';
4
5
  export * from './spatialIndex';
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,SAAS,CAAC;AACxB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
package/dist/contract.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './explainSpatialIndexing2D';
2
+ export * from './bvh3D';
2
3
  export * from './explainSpatialIndexing3D';
3
4
  export * from './formatSpatialIndexingNotice';
4
5
  export * from './spatialIndex';
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,SAAS,CAAC;AACxB,cAAc,4BAA4B,CAAC;AAC3C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { MAX_INDEXED_CELLS_PER_OBJECT, clearSpatialIndex2D, clearSpatialIndex3D, createSpatialIndex2D, createSpatialIndex3D, createUniformGridSpatialBackend2D, createUniformGridSpatialBackend3D, explainSpatialIndexing2D, explainSpatialIndexing3D, formatSpatialIndexingNotice, insertSpatialObject2D, insertSpatialObject3D, querySpatialPairs2D, querySpatialPairs3D, querySpatialPoint2D, querySpatialPoint3D, querySpatialRay2D, querySpatialRay3D, querySpatialRegion2D, querySpatialRegion3D, removeSpatialObject2D, removeSpatialObject3D, setSpatialIndexingGuard, updateSpatialObject2D, updateSpatialObject3D, } from './contract';
1
+ export { MAX_INDEXED_CELLS_PER_OBJECT, clearSpatialIndex2D, clearSpatialIndex3D, createSpatialIndex2D, createSpatialIndex3D, createUniformGridSpatialBackend2D, createBvhSpatialBackend3D, createUniformGridSpatialBackend3D, explainSpatialIndexing2D, explainSpatialIndexing3D, formatSpatialIndexingNotice, insertSpatialObject2D, insertSpatialObject3D, querySpatialPairs2D, querySpatialPairs3D, querySpatialPoint2D, querySpatialPoint3D, querySpatialRay2D, querySpatialRay3D, querySpatialRegion2D, querySpatialFrustum3D, querySpatialRegion3D, querySpatialSphere3D, removeSpatialObject2D, removeSpatialObject3D, setSpatialIndexingGuard, updateSpatialObject2D, updateSpatialObject3D, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,iCAAiC,EACjC,iCAAiC,EACjC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,iCAAiC,EACjC,yBAAyB,EACzB,iCAAiC,EACjC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { MAX_INDEXED_CELLS_PER_OBJECT, clearSpatialIndex2D, clearSpatialIndex3D, createSpatialIndex2D, createSpatialIndex3D, createUniformGridSpatialBackend2D, createUniformGridSpatialBackend3D, explainSpatialIndexing2D, explainSpatialIndexing3D, formatSpatialIndexingNotice, insertSpatialObject2D, insertSpatialObject3D, querySpatialPairs2D, querySpatialPairs3D, querySpatialPoint2D, querySpatialPoint3D, querySpatialRay2D, querySpatialRay3D, querySpatialRegion2D, querySpatialRegion3D, removeSpatialObject2D, removeSpatialObject3D, setSpatialIndexingGuard, updateSpatialObject2D, updateSpatialObject3D, } from './contract';
1
+ export { MAX_INDEXED_CELLS_PER_OBJECT, clearSpatialIndex2D, clearSpatialIndex3D, createSpatialIndex2D, createSpatialIndex3D, createUniformGridSpatialBackend2D, createBvhSpatialBackend3D, createUniformGridSpatialBackend3D, explainSpatialIndexing2D, explainSpatialIndexing3D, formatSpatialIndexingNotice, insertSpatialObject2D, insertSpatialObject3D, querySpatialPairs2D, querySpatialPairs3D, querySpatialPoint2D, querySpatialPoint3D, querySpatialRay2D, querySpatialRay3D, querySpatialRegion2D, querySpatialFrustum3D, querySpatialRegion3D, querySpatialSphere3D, removeSpatialObject2D, removeSpatialObject3D, setSpatialIndexingGuard, updateSpatialObject2D, updateSpatialObject3D, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,iCAAiC,EACjC,iCAAiC,EACjC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,iCAAiC,EACjC,yBAAyB,EACzB,iCAAiC,EACjC,wBAAwB,EACxB,wBAAwB,EACxB,2BAA2B,EAC3B,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
@@ -1,11 +1,13 @@
1
- import type { SpatialAabb3D, SpatialIndex3D, SpatialIndexBackend3D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
1
+ import type { SpatialAabb3D, SpatialFrustum3D, SpatialIndex3D, SpatialIndexBackend3D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
2
2
  export declare function clearSpatialIndex3D(index: Readonly<SpatialIndex3D>): void;
3
3
  export declare function createSpatialIndex3D(backend?: SpatialIndexBackend3D): SpatialIndex3D;
4
4
  export declare function insertSpatialObject3D(index: Readonly<SpatialIndex3D>, id: SpatialObjectId, bounds: Readonly<SpatialAabb3D>): boolean;
5
+ export declare function querySpatialFrustum3D(index: Readonly<SpatialIndex3D>, frustum: Readonly<SpatialFrustum3D>, out: SpatialObjectId[], slices?: number): void;
5
6
  export declare function querySpatialPairs3D(index: Readonly<SpatialIndex3D>, out: SpatialPair[]): void;
6
7
  export declare function querySpatialPoint3D(index: Readonly<SpatialIndex3D>, x: number, y: number, z: number, out: SpatialObjectId[]): void;
7
8
  export declare function querySpatialRay3D(index: Readonly<SpatialIndex3D>, x: number, y: number, z: number, dx: number, dy: number, dz: number, out: SpatialObjectId[]): void;
8
9
  export declare function querySpatialRegion3D(index: Readonly<SpatialIndex3D>, region: Readonly<SpatialAabb3D>, out: SpatialObjectId[]): void;
10
+ export declare function querySpatialSphere3D(index: Readonly<SpatialIndex3D>, x: number, y: number, z: number, radius: number, out: SpatialObjectId[]): void;
9
11
  export declare function removeSpatialObject3D(index: Readonly<SpatialIndex3D>, id: SpatialObjectId): void;
10
12
  export declare function updateSpatialObject3D(index: Readonly<SpatialIndex3D>, id: SpatialObjectId, bounds: Readonly<SpatialAabb3D>): boolean;
11
13
  //# sourceMappingURL=spatialIndex3D.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spatialIndex3D.d.ts","sourceRoot":"","sources":["../src/spatialIndex3D.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,IAAI,CAEzE;AAMD,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAMpF;AAOD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,GAC9B,OAAO,CAET;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,IAAI,CAE7F;AAGD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAKD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAGD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC/B,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAGD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,eAAe,GAAG,IAAI,CAEhG;AAMD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,GAC9B,OAAO,CAET"}
1
+ {"version":3,"file":"spatialIndex3D.d.ts","sourceRoot":"","sources":["../src/spatialIndex3D.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAKlC,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,GAAG,IAAI,CAEzE;AAMD,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,cAAc,CAMpF;AAOD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,GAC9B,OAAO,CAET;AAmBD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,OAAO,EAAE,QAAQ,CAAC,gBAAgB,CAAC,EACnC,GAAG,EAAE,eAAe,EAAE,EACtB,MAAM,SAAyB,GAC9B,IAAI,CAqDN;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,IAAI,CAE7F;AAGD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAKD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAGD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC/B,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AASD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAUN;AAGD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,EAAE,EAAE,eAAe,GAAG,IAAI,CAEhG;AAMD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,QAAQ,CAAC,cAAc,CAAC,EAC/B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,GAC9B,OAAO,CAET"}
@@ -22,6 +22,82 @@ export function createSpatialIndex3D(backend) {
22
22
  export function insertSpatialObject3D(index, id, bounds) {
23
23
  return index.runtime.backend.insertSpatialObject(id, bounds);
24
24
  }
25
+ // Fills `out` (cleared first) with the ids whose bounds may lie inside the view volume — the candidate
26
+ // set for view culling.
27
+ //
28
+ // Built on the region query rather than on a new backend method, so every present and future backend
29
+ // answers it without implementing anything, and the ratified eight-method seam does not widen for a
30
+ // query that needs no structural knowledge.
31
+ //
32
+ // The volume is covered by `slices` axis-aligned boxes taken along its DEPTH rather than by one box
33
+ // around the whole thing. A single AABB is correct but nearly useless for culling: a long perspective
34
+ // frustum's bounding box approaches the whole world, so the query would hand back almost every object
35
+ // and cull nothing. Slicing costs one region query per slice and shrinks the covered volume toward the
36
+ // frustum itself — eight slices is a good default, and more is a straight trade of query time against
37
+ // candidates the caller would otherwise reject.
38
+ //
39
+ // The result is a CANDIDATE set, in the same sense as `querySpatialPairs3D`: every id whose bounds the
40
+ // covering boxes touch, which is a superset of what is truly visible. The caller applies the exact
41
+ // plane test to what comes back.
42
+ export function querySpatialFrustum3D(index, frustum, out, slices = DEFAULT_FRUSTUM_SLICES) {
43
+ out.length = 0;
44
+ const corners = frustum.corners;
45
+ if (corners.length < 24 || !(slices >= 1))
46
+ return;
47
+ const steps = Math.floor(slices);
48
+ frustumSeen.clear();
49
+ for (let slice = 0; slice < steps; slice += 1) {
50
+ const nearT = slice / steps;
51
+ const farT = (slice + 1) / steps;
52
+ let minX = Infinity;
53
+ let minY = Infinity;
54
+ let minZ = Infinity;
55
+ let maxX = -Infinity;
56
+ let maxY = -Infinity;
57
+ let maxZ = -Infinity;
58
+ // Both ends of the slice, for all four edges: the volume between two depths is bounded by the eight
59
+ // points at those depths. Taking only the far end would clip the slice's own near face away.
60
+ for (let corner = 0; corner < 4; corner += 1) {
61
+ const near = corner * 3;
62
+ const far = 12 + corner * 3;
63
+ for (const t of [nearT, farT]) {
64
+ const x = corners[near] + (corners[far] - corners[near]) * t;
65
+ const y = corners[near + 1] + (corners[far + 1] - corners[near + 1]) * t;
66
+ const z = corners[near + 2] + (corners[far + 2] - corners[near + 2]) * t;
67
+ if (x < minX)
68
+ minX = x;
69
+ if (x > maxX)
70
+ maxX = x;
71
+ if (y < minY)
72
+ minY = y;
73
+ if (y > maxY)
74
+ maxY = y;
75
+ if (z < minZ)
76
+ minZ = z;
77
+ if (z > maxZ)
78
+ maxZ = z;
79
+ }
80
+ }
81
+ if (!Number.isFinite(minX) || !Number.isFinite(maxZ))
82
+ return;
83
+ queryRegionScratch.minX = minX;
84
+ queryRegionScratch.minY = minY;
85
+ queryRegionScratch.minZ = minZ;
86
+ queryRegionScratch.maxX = maxX;
87
+ queryRegionScratch.maxY = maxY;
88
+ queryRegionScratch.maxZ = maxZ;
89
+ index.runtime.backend.querySpatialRegion(queryRegionScratch, frustumSlice);
90
+ // Slices share objects that straddle their boundary, so the union has to be deduplicated — without
91
+ // this the same id appears once per slice it touches, and a caller counting results would over-count
92
+ // a large object exactly in proportion to how large it is.
93
+ for (const id of frustumSlice) {
94
+ if (frustumSeen.has(id))
95
+ continue;
96
+ frustumSeen.add(id);
97
+ out.push(id);
98
+ }
99
+ }
100
+ }
25
101
  // Fills `out` (cleared first) with every deduplicated candidate pair — each unordered pair at most
26
102
  // once, never an object with itself. A pair is a broadphase candidate: the two objects are close
27
103
  // enough to be worth a narrow-phase test, which the caller (or @flighthq/collision) performs.
@@ -42,6 +118,25 @@ export function querySpatialRay3D(index, x, y, z, dx, dy, dz, out) {
42
118
  export function querySpatialRegion3D(index, region, out) {
43
119
  index.runtime.backend.querySpatialRegion(region, out);
44
120
  }
121
+ // Fills `out` (cleared first) with the ids whose bounds may lie within `radius` of (`x`,`y`,`z`).
122
+ //
123
+ // A CANDIDATE set over the sphere's bounding cube, not an exact sphere test — the index stores bounds
124
+ // and answers box questions, and the corners of that cube reach past the sphere by a factor of the
125
+ // square root of three. That is the same contract every other query here keeps, and the caller that
126
+ // needs exactness applies the distance test to what comes back. The value of the export is that the
127
+ // cube is derived once, correctly, instead of at every callsite.
128
+ export function querySpatialSphere3D(index, x, y, z, radius, out) {
129
+ out.length = 0;
130
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z) || !(radius >= 0))
131
+ return;
132
+ queryRegionScratch.minX = x - radius;
133
+ queryRegionScratch.minY = y - radius;
134
+ queryRegionScratch.minZ = z - radius;
135
+ queryRegionScratch.maxX = x + radius;
136
+ queryRegionScratch.maxY = y + radius;
137
+ queryRegionScratch.maxZ = z + radius;
138
+ index.runtime.backend.querySpatialRegion(queryRegionScratch, out);
139
+ }
45
140
  // Removes an object from the index. A no-op if the id is not present.
46
141
  export function removeSpatialObject3D(index, id) {
47
142
  index.runtime.backend.removeSpatialObject(id);
@@ -59,4 +154,13 @@ export function updateSpatialObject3D(index, id, bounds) {
59
154
  // workload with a known typical object size should pass its own grid — and in 3D it pays off sooner,
60
155
  // since the per-object cell budget is a ~10-cell cube rather than a 32-cell square.
61
156
  const DEFAULT_SPATIAL_CELL_SIZE_3D = 128;
157
+ // How many depth slices cover a frustum when the caller does not choose. Eight brings a typical
158
+ // perspective volume's covered bounds close to the volume itself while keeping the query to eight
159
+ // region calls; one slice degenerates to the single-AABB behaviour that makes frustum culling pointless.
160
+ const DEFAULT_FRUSTUM_SLICES = 8;
161
+ // Reused across calls: a culling pass runs every frame, and these would otherwise be three allocations
162
+ // per frame in the hot path. Never held beyond the call that fills them.
163
+ const queryRegionScratch = { maxX: 0, maxY: 0, maxZ: 0, minX: 0, minY: 0, minZ: 0 };
164
+ const frustumSeen = new Set();
165
+ const frustumSlice = [];
62
166
  //# sourceMappingURL=spatialIndex3D.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"spatialIndex3D.js","sourceRoot":"","sources":["../src/spatialIndex3D.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,iCAAiC,EAAE,MAAM,iBAAiB,CAAC;AAEpE,gFAAgF;AAChF,MAAM,UAAU,mBAAmB,CAAC,KAA+B;IACjE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;AAC5C,CAAC;AAED,yFAAyF;AACzF,qGAAqG;AACrG,kGAAkG;AAClG,4CAA4C;AAC5C,MAAM,UAAU,oBAAoB,CAAC,OAA+B;IAClE,OAAO;QACL,OAAO,EAAE;YACP,OAAO,EAAE,OAAO,IAAI,iCAAiC,CAAC,4BAA4B,CAAC;SACpF;KACF,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,4FAA4F;AAC5F,iGAAiG;AACjG,kGAAkG;AAClG,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CACnC,KAA+B,EAC/B,EAAmB,EACnB,MAA+B;IAE/B,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AAC/D,CAAC;AAED,mGAAmG;AACnG,iGAAiG;AACjG,8FAA8F;AAC9F,MAAM,UAAU,mBAAmB,CAAC,KAA+B,EAAE,GAAkB;IACrF,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAC/C,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,mBAAmB,CACjC,KAA+B,EAC/B,CAAS,EACT,CAAS,EACT,CAAS,EACT,GAAsB;IAEtB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,yFAAyF;AACzF,qGAAqG;AACrG,gBAAgB;AAChB,MAAM,UAAU,iBAAiB,CAC/B,KAA+B,EAC/B,CAAS,EACT,CAAS,EACT,CAAS,EACT,EAAU,EACV,EAAU,EACV,EAAU,EACV,GAAsB;IAEtB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;AAClE,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAClC,KAA+B,EAC/B,MAA+B,EAC/B,GAAsB;IAEtB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,qBAAqB,CAAC,KAA+B,EAAE,EAAmB;IACxF,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;AAChD,CAAC;AAED,oGAAoG;AACpG,uGAAuG;AACvG,sGAAsG;AACtG,oBAAoB;AACpB,MAAM,UAAU,qBAAqB,CACnC,KAA+B,EAC/B,EAAmB,EACnB,MAA+B;IAE/B,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AAC/D,CAAC;AAED,sGAAsG;AACtG,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,oFAAoF;AACpF,MAAM,4BAA4B,GAAG,GAAG,CAAC"}
1
+ {"version":3,"file":"spatialIndex3D.js","sourceRoot":"","sources":["../src/spatialIndex3D.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/spatial",
3
- "version": "0.4.0-next.1752.f250cb9",
3
+ "version": "0.4.0-next.1763.f98ae25",
4
4
  "author": "Joshua Granick and other contributors",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,8 +38,8 @@
38
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@flighthq/geometry": "0.4.0-next.1752.f250cb9",
42
- "@flighthq/types": "0.4.0-next.1752.f250cb9"
41
+ "@flighthq/geometry": "0.4.0-next.1763.f98ae25",
42
+ "@flighthq/types": "0.4.0-next.1763.f98ae25"
43
43
  },
44
44
  "devDependencies": {
45
45
  "typescript": "^5.3.0"
@@ -0,0 +1,231 @@
1
+ import type { SpatialAabb3D, SpatialIndexBackend3D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createBvhSpatialBackend3D } from './bvh3D';
5
+ import { createUniformGridSpatialBackend3D } from './uniformGrid3D';
6
+
7
+ // The BVH is tested against the UNIFORM GRID, not against itself. The grid is the proven incumbent and
8
+ // the two are meant to be interchangeable behind one seam, so any disagreement is a defect in one of
9
+ // them — which is a far stronger statement than "the tree returns something plausible".
10
+ //
11
+ // Sets are compared, never order: the seam promises which ids match, and two structures walk their
12
+ // contents in different orders by construction.
13
+
14
+ function createRandom(seed: number): () => number {
15
+ let state = seed | 0 || 1;
16
+ return () => {
17
+ state ^= state << 13;
18
+ state ^= state >>> 17;
19
+ state ^= state << 5;
20
+ return ((state >>> 0) % 100000) / 100000;
21
+ };
22
+ }
23
+
24
+ function box(x: number, y: number, z: number, size: number): SpatialAabb3D {
25
+ return { maxX: x + size, maxY: y + size, maxZ: z + size, minX: x, minY: y, minZ: z };
26
+ }
27
+
28
+ function sorted(ids: readonly SpatialObjectId[]): SpatialObjectId[] {
29
+ return [...ids].sort((a, b) => a - b);
30
+ }
31
+
32
+ function sortedPairs(pairs: readonly SpatialPair[]): string[] {
33
+ return pairs.map((pair) => `${String(Math.min(pair.a, pair.b))}-${String(Math.max(pair.a, pair.b))}`).sort();
34
+ }
35
+
36
+ describe('createBvhSpatialBackend3D', () => {
37
+ it('answers every query exactly as the uniform grid does, over a seeded scene', () => {
38
+ const random = createRandom(6112026);
39
+ const bvh = createBvhSpatialBackend3D(2);
40
+ const grid = createUniformGridSpatialBackend3D(16);
41
+
42
+ // Deliberately wide size variance — the case a uniform grid handles worst and a tree handles well.
43
+ // If the two agree here they agree anywhere.
44
+ for (let id = 0; id < 120; id += 1) {
45
+ const size = id % 12 === 0 ? 40 + random() * 30 : 0.5 + random() * 4;
46
+ const bounds = box((random() - 0.5) * 400, (random() - 0.5) * 400, (random() - 0.5) * 400, size);
47
+ expect(bvh.insertSpatialObject(id, bounds)).toBe(grid.insertSpatialObject(id, bounds));
48
+ }
49
+
50
+ const bvhOut: SpatialObjectId[] = [];
51
+ const gridOut: SpatialObjectId[] = [];
52
+
53
+ for (let probe = 0; probe < 60; probe += 1) {
54
+ const region = box((random() - 0.5) * 400, (random() - 0.5) * 400, (random() - 0.5) * 400, 10 + random() * 60);
55
+ bvh.querySpatialRegion(region, bvhOut);
56
+ grid.querySpatialRegion(region, gridOut);
57
+ expect(sorted(bvhOut), `region probe ${String(probe)}`).toEqual(sorted(gridOut));
58
+
59
+ const px = (random() - 0.5) * 400;
60
+ const py = (random() - 0.5) * 400;
61
+ const pz = (random() - 0.5) * 400;
62
+ bvh.querySpatialPoint(px, py, pz, bvhOut);
63
+ grid.querySpatialPoint(px, py, pz, gridOut);
64
+ expect(sorted(bvhOut), `point probe ${String(probe)}`).toEqual(sorted(gridOut));
65
+
66
+ const dx = random() - 0.5;
67
+ const dy = random() - 0.5;
68
+ const dz = random() - 0.5;
69
+ bvh.querySpatialRay(px, py, pz, dx, dy, dz, bvhOut);
70
+ grid.querySpatialRay(px, py, pz, dx, dy, dz, gridOut);
71
+ expect(sorted(bvhOut), `ray probe ${String(probe)}`).toEqual(sorted(gridOut));
72
+ }
73
+ });
74
+
75
+ it('reports every TRULY overlapping pair after inserts, moves, and removals', () => {
76
+ const random = createRandom(778291);
77
+ const bvh = createBvhSpatialBackend3D(1.5);
78
+ const grid = createUniformGridSpatialBackend3D(12);
79
+
80
+ // The oracle's own record of where everything ended up, kept alongside rather than read back out of
81
+ // either index — a truth set derived from the thing under test would prove nothing.
82
+ const live = new Map<number, SpatialAabb3D>();
83
+ for (let id = 0; id < 60; id += 1) {
84
+ const bounds = box((random() - 0.5) * 80, (random() - 0.5) * 80, (random() - 0.5) * 80, 1 + random() * 5);
85
+ bvh.insertSpatialObject(id, bounds);
86
+ grid.insertSpatialObject(id, bounds);
87
+ live.set(id, bounds);
88
+ }
89
+ for (let id = 0; id < 60; id += 3) {
90
+ const bounds = box((random() - 0.5) * 80, (random() - 0.5) * 80, (random() - 0.5) * 80, 1 + random() * 5);
91
+ bvh.updateSpatialObject(id, bounds);
92
+ grid.updateSpatialObject(id, bounds);
93
+ live.set(id, bounds);
94
+ }
95
+ for (let id = 1; id < 60; id += 7) {
96
+ bvh.removeSpatialObject(id);
97
+ grid.removeSpatialObject(id);
98
+ live.delete(id);
99
+ }
100
+
101
+ const bvhPairs: SpatialPair[] = [];
102
+ const gridPairs: SpatialPair[] = [];
103
+ bvh.querySpatialPairs(bvhPairs);
104
+ grid.querySpatialPairs(gridPairs);
105
+
106
+ // NOT set equality, and the difference is the contract rather than a defect. A pair is a CANDIDATE:
107
+ // the grid emits everything sharing a cell without testing bounds at all, so its answer is a strict
108
+ // superset, while the tree tests overlap at the leaf and returns the true set. Both are legal; what
109
+ // both owe is that no real overlap is missing. Asserting equality here would have been asserting that
110
+ // two backends make the same conservative approximation, which the seam never promised.
111
+ const truth = new Set<string>();
112
+ for (let a = 0; a < 60; a += 1) {
113
+ for (let b = a + 1; b < 60; b += 1) {
114
+ const boundsA = live.get(a);
115
+ const boundsB = live.get(b);
116
+ if (boundsA === undefined || boundsB === undefined) continue;
117
+ if (
118
+ boundsA.minX < boundsB.maxX &&
119
+ boundsA.maxX > boundsB.minX &&
120
+ boundsA.minY < boundsB.maxY &&
121
+ boundsA.maxY > boundsB.minY &&
122
+ boundsA.minZ < boundsB.maxZ &&
123
+ boundsA.maxZ > boundsB.minZ
124
+ ) {
125
+ truth.add(`${String(a)}-${String(b)}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ const bvhSet = new Set(sortedPairs(bvhPairs));
131
+ const gridSet = new Set(sortedPairs(gridPairs));
132
+ expect(truth.size).toBeGreaterThan(0);
133
+ for (const pair of truth) {
134
+ expect(bvhSet.has(pair), `bvh missed ${pair}`).toBe(true);
135
+ expect(gridSet.has(pair), `grid missed ${pair}`).toBe(true);
136
+ }
137
+ // No self-pairs and no duplicates, which both backends do owe exactly.
138
+ expect(sortedPairs(bvhPairs)).toEqual([...bvhSet].sort());
139
+ expect(bvhPairs.every((pair) => pair.a !== pair.b)).toBe(true);
140
+ });
141
+
142
+ it('keeps answering correctly while objects move within their margin', () => {
143
+ // The fat-bounds fast path: a small move leaves the tree untouched, so only the exact bounds are
144
+ // refreshed. If they were not, the query would answer from where the object USED to be — the exact
145
+ // failure the margin risks introducing.
146
+ const bvh = createBvhSpatialBackend3D(10);
147
+ bvh.insertSpatialObject(1, box(0, 0, 0, 1));
148
+
149
+ const out: SpatialObjectId[] = [];
150
+ bvh.querySpatialPoint(0.5, 0.5, 0.5, out);
151
+ expect(out).toEqual([1]);
152
+
153
+ // Well inside the 10-unit margin, so no reinsertion happens.
154
+ bvh.updateSpatialObject(1, box(4, 0, 0, 1));
155
+ bvh.querySpatialPoint(0.5, 0.5, 0.5, out);
156
+ expect(out).toEqual([]);
157
+ bvh.querySpatialPoint(4.5, 0.5, 0.5, out);
158
+ expect(out).toEqual([1]);
159
+ });
160
+
161
+ it('declines non-finite and inverted bounds with the same sentinel as the grid', () => {
162
+ const bvh = createBvhSpatialBackend3D();
163
+ const grid = createUniformGridSpatialBackend3D(16);
164
+ const nonFinite: SpatialAabb3D = { maxX: 1, maxY: 1, maxZ: 1, minX: NaN, minY: 0, minZ: 0 };
165
+ const inverted: SpatialAabb3D = { maxX: -1, maxY: 1, maxZ: 1, minX: 0, minY: 0, minZ: 0 };
166
+
167
+ expect(bvh.insertSpatialObject(1, nonFinite)).toBe(grid.insertSpatialObject(1, nonFinite));
168
+ expect(bvh.insertSpatialObject(2, inverted)).toBe(grid.insertSpatialObject(2, inverted));
169
+ expect(bvh.explainSpatialIndexing(1).mode).toBe('declined');
170
+ expect(bvh.explainSpatialIndexing(1).reason).toBe('non-finite-bounds');
171
+ expect(bvh.explainSpatialIndexing(2).reason).toBe('inverted-bounds');
172
+
173
+ const out: SpatialObjectId[] = [];
174
+ bvh.querySpatialRegion(box(-100, -100, -100, 200), out);
175
+ expect(out).toEqual([]);
176
+ });
177
+
178
+ it('removes an object whose update is declined rather than leaving it at its old bounds', () => {
179
+ // The seam's contract: a declined update must not read as a current position. Kept identical to the
180
+ // grid's behaviour, since a caller ignoring the sentinel must not see the two backends differ.
181
+ const bvh = createBvhSpatialBackend3D();
182
+ bvh.insertSpatialObject(1, box(0, 0, 0, 4));
183
+ expect(bvh.updateSpatialObject(1, { maxX: 1, maxY: 1, maxZ: 1, minX: Infinity, minY: 0, minZ: 0 })).toBe(false);
184
+
185
+ const out: SpatialObjectId[] = [];
186
+ bvh.querySpatialRegion(box(-10, -10, -10, 40), out);
187
+ expect(out).toEqual([]);
188
+ expect(bvh.explainSpatialIndexing(1).mode).toBe('declined');
189
+ });
190
+
191
+ it('reports absent for an id it has never seen, and after a clear', () => {
192
+ const bvh = createBvhSpatialBackend3D();
193
+ expect(bvh.explainSpatialIndexing(99).mode).toBe('absent');
194
+ bvh.insertSpatialObject(99, box(0, 0, 0, 1));
195
+ expect(bvh.explainSpatialIndexing(99).mode).toBe('cells');
196
+ bvh.clearSpatialIndex();
197
+ expect(bvh.explainSpatialIndexing(99).mode).toBe('absent');
198
+ });
199
+
200
+ it('stays balanced under sorted insertion, which is the case that degenerates a naive tree', () => {
201
+ // A run of monotonically increasing boxes is the worst input for an unbalanced tree: it builds a
202
+ // linked list and every query goes linear. Measured indirectly through the query still being exact
203
+ // over a large sorted set, which a broken rotation would break outright rather than merely slow.
204
+ const bvh = createBvhSpatialBackend3D(0);
205
+ const grid = createUniformGridSpatialBackend3D(8);
206
+ for (let id = 0; id < 400; id += 1) {
207
+ const bounds = box(id * 3, 0, 0, 2);
208
+ bvh.insertSpatialObject(id, bounds);
209
+ grid.insertSpatialObject(id, bounds);
210
+ }
211
+ const bvhOut: SpatialObjectId[] = [];
212
+ const gridOut: SpatialObjectId[] = [];
213
+ bvh.querySpatialRegion(box(300, -5, -5, 60), bvhOut);
214
+ grid.querySpatialRegion(box(300, -5, -5, 60), gridOut);
215
+ expect(sorted(bvhOut)).toEqual(sorted(gridOut));
216
+ expect(bvhOut.length).toBeGreaterThan(5);
217
+ });
218
+
219
+ it('is interchangeable with the grid behind createSpatialIndex3D', () => {
220
+ // The seam's whole promise. Both are SpatialIndexBackend3D and neither is named in the call.
221
+ const backends: SpatialIndexBackend3D[] = [createBvhSpatialBackend3D(), createUniformGridSpatialBackend3D(16)];
222
+ const results = backends.map((backend) => {
223
+ backend.insertSpatialObject(1, box(0, 0, 0, 5));
224
+ backend.insertSpatialObject(2, box(100, 0, 0, 5));
225
+ const out: SpatialObjectId[] = [];
226
+ backend.querySpatialRegion(box(-1, -1, -1, 10), out);
227
+ return sorted(out);
228
+ });
229
+ expect(results[0]).toEqual(results[1]);
230
+ });
231
+ });
@@ -1,14 +1,16 @@
1
- import type { SpatialAabb3D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
1
+ import type { SpatialAabb3D, SpatialFrustum3D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
2
2
  import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import {
5
5
  clearSpatialIndex3D,
6
6
  createSpatialIndex3D,
7
7
  insertSpatialObject3D,
8
+ querySpatialFrustum3D,
8
9
  querySpatialPairs3D,
9
10
  querySpatialPoint3D,
10
11
  querySpatialRay3D,
11
12
  querySpatialRegion3D,
13
+ querySpatialSphere3D,
12
14
  removeSpatialObject3D,
13
15
  updateSpatialObject3D,
14
16
  } from './spatialIndex3D';
@@ -74,6 +76,91 @@ describe('insertSpatialObject3D', () => {
74
76
  });
75
77
  });
76
78
 
79
+ // A frustum looking down +Z: a small near quad at z=0 widening to a large far quad at z=400. Corner
80
+ // order is the contracted one — four near corners, then the four far corners in the SAME winding.
81
+ function viewFrustum(nearHalf: number, farHalf: number, farZ: number): SpatialFrustum3D {
82
+ return {
83
+ corners: [
84
+ -nearHalf,
85
+ -nearHalf,
86
+ 0,
87
+ nearHalf,
88
+ -nearHalf,
89
+ 0,
90
+ nearHalf,
91
+ nearHalf,
92
+ 0,
93
+ -nearHalf,
94
+ nearHalf,
95
+ 0,
96
+ -farHalf,
97
+ -farHalf,
98
+ farZ,
99
+ farHalf,
100
+ -farHalf,
101
+ farZ,
102
+ farHalf,
103
+ farHalf,
104
+ farZ,
105
+ -farHalf,
106
+ farHalf,
107
+ farZ,
108
+ ],
109
+ };
110
+ }
111
+
112
+ describe('querySpatialFrustum3D', () => {
113
+ it('finds an object inside the volume and excludes one far outside it', () => {
114
+ const index = createSpatialIndex3D(createUniformGridSpatialBackend3D(32));
115
+ insertSpatialObject3D(index, 1, box(-5, -5, 100, 10));
116
+ insertSpatialObject3D(index, 2, box(900, 900, 100, 10));
117
+ const out: SpatialObjectId[] = [];
118
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), out);
119
+ expect(out).toContain(1);
120
+ expect(out).not.toContain(2);
121
+ });
122
+
123
+ it('reports each object once even when it straddles several slices', () => {
124
+ // A long object spanning most of the depth touches many slices. Without the dedup it would appear
125
+ // once per slice, so a caller counting results would over-count exactly the largest objects.
126
+ const index = createSpatialIndex3D(createUniformGridSpatialBackend3D(32));
127
+ insertSpatialObject3D(index, 7, { maxX: 5, maxY: 5, maxZ: 380, minX: -5, minY: -5, minZ: 10 });
128
+ const out: SpatialObjectId[] = [];
129
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), out, 8);
130
+ expect(out.filter((id) => id === 7)).toHaveLength(1);
131
+ });
132
+
133
+ it('covers strictly less than the single bounding box as slices increase', () => {
134
+ // The point of slicing. An object out near the far corners of the frustum's own AABB but well
135
+ // outside the cone is a candidate at one slice and rejected at many.
136
+ const index = createSpatialIndex3D(createUniformGridSpatialBackend3D(32));
137
+ insertSpatialObject3D(index, 3, box(180, 180, 20, 8));
138
+ const coarse: SpatialObjectId[] = [];
139
+ const fine: SpatialObjectId[] = [];
140
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), coarse, 1);
141
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), fine, 16);
142
+ expect(coarse).toContain(3);
143
+ expect(fine).not.toContain(3);
144
+ });
145
+
146
+ it('returns nothing for a malformed corner list rather than guessing', () => {
147
+ const index = createSpatialIndex3D();
148
+ insertSpatialObject3D(index, 1, box(0, 0, 0, 10));
149
+ const out: SpatialObjectId[] = [];
150
+ querySpatialFrustum3D(index, { corners: [0, 0, 0] }, out);
151
+ expect(out).toEqual([]);
152
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), out, 0);
153
+ expect(out).toEqual([]);
154
+ });
155
+
156
+ it('clears the output before filling it', () => {
157
+ const index = createSpatialIndex3D();
158
+ const out: SpatialObjectId[] = [99];
159
+ querySpatialFrustum3D(index, viewFrustum(10, 200, 400), out);
160
+ expect(out).not.toContain(99);
161
+ });
162
+ });
163
+
77
164
  describe('querySpatialPairs3D', () => {
78
165
  it('reports a co-located pair once', () => {
79
166
  const index = createSpatialIndex3D();
@@ -118,6 +205,40 @@ describe('querySpatialRegion3D', () => {
118
205
  });
119
206
  });
120
207
 
208
+ describe('querySpatialSphere3D', () => {
209
+ it('finds objects within the radius and excludes those beyond it', () => {
210
+ const index = createSpatialIndex3D(createUniformGridSpatialBackend3D(16));
211
+ insertSpatialObject3D(index, 1, box(0, 0, 0, 4));
212
+ insertSpatialObject3D(index, 2, box(500, 500, 500, 4));
213
+ const out: SpatialObjectId[] = [];
214
+ querySpatialSphere3D(index, 0, 0, 0, 20, out);
215
+ expect(out).toContain(1);
216
+ expect(out).not.toContain(2);
217
+ });
218
+
219
+ it('is a candidate set over the bounding cube, not an exact sphere test', () => {
220
+ // The corner of the cube reaches past the sphere by root three. This object sits in that corner —
221
+ // outside the sphere, inside the cube — and IS returned. Documented behaviour rather than a defect:
222
+ // the caller applies the exact distance test to what comes back.
223
+ const index = createSpatialIndex3D(createUniformGridSpatialBackend3D(16));
224
+ insertSpatialObject3D(index, 5, box(9, 9, 9, 0.5));
225
+ const out: SpatialObjectId[] = [];
226
+ querySpatialSphere3D(index, 0, 0, 0, 10, out);
227
+ expect(Math.hypot(9, 9, 9)).toBeGreaterThan(10);
228
+ expect(out).toContain(5);
229
+ });
230
+
231
+ it('returns nothing for a non-finite centre or negative radius', () => {
232
+ const index = createSpatialIndex3D();
233
+ insertSpatialObject3D(index, 1, box(0, 0, 0, 4));
234
+ const out: SpatialObjectId[] = [];
235
+ querySpatialSphere3D(index, NaN, 0, 0, 10, out);
236
+ expect(out).toEqual([]);
237
+ querySpatialSphere3D(index, 0, 0, 0, -1, out);
238
+ expect(out).toEqual([]);
239
+ });
240
+ });
241
+
121
242
  describe('removeSpatialObject3D', () => {
122
243
  it('drops the object and is a no-op for an unknown id', () => {
123
244
  const index = createSpatialIndex3D();