@flighthq/spatial 0.3.1-next.975.01aea94 → 0.4.0-edge.1908.751021c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bvh3D.d.ts +3 -0
- package/dist/bvh3D.d.ts.map +1 -0
- package/dist/bvh3D.js +575 -0
- package/dist/bvh3D.js.map +1 -0
- package/dist/contract.d.ts +6 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +6 -1
- package/dist/contract.js.map +1 -1
- package/dist/explainSpatialIndexing2D.d.ts +3 -0
- package/dist/explainSpatialIndexing2D.d.ts.map +1 -0
- package/dist/{explainSpatialIndexing.js → explainSpatialIndexing2D.js} +2 -2
- package/dist/explainSpatialIndexing2D.js.map +1 -0
- package/dist/explainSpatialIndexing3D.d.ts +3 -0
- package/dist/explainSpatialIndexing3D.d.ts.map +1 -0
- package/dist/explainSpatialIndexing3D.js +15 -0
- package/dist/explainSpatialIndexing3D.js.map +1 -0
- package/dist/formatSpatialIndexingNotice.d.ts.map +1 -1
- package/dist/formatSpatialIndexingNotice.js +8 -2
- package/dist/formatSpatialIndexingNotice.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/spatialIndex.d.ts +10 -10
- package/dist/spatialIndex.d.ts.map +1 -1
- package/dist/spatialIndex.js +14 -14
- package/dist/spatialIndex.js.map +1 -1
- package/dist/spatialIndex3D.d.ts +13 -0
- package/dist/spatialIndex3D.d.ts.map +1 -0
- package/dist/spatialIndex3D.js +166 -0
- package/dist/spatialIndex3D.js.map +1 -0
- package/dist/spatialIndexingGuard.d.ts +4 -0
- package/dist/spatialIndexingGuard.d.ts.map +1 -0
- package/dist/spatialIndexingGuard.js +23 -0
- package/dist/spatialIndexingGuard.js.map +1 -0
- package/dist/uniformGrid.d.ts +2 -3
- package/dist/uniformGrid.d.ts.map +1 -1
- package/dist/uniformGrid.js +5 -15
- package/dist/uniformGrid.js.map +1 -1
- package/dist/uniformGrid3D.d.ts +3 -0
- package/dist/uniformGrid3D.d.ts.map +1 -0
- package/dist/uniformGrid3D.js +627 -0
- package/dist/uniformGrid3D.js.map +1 -0
- package/package.json +3 -3
- package/src/bvh3D.test.ts +294 -0
- package/src/explainSpatialIndexing2D.test.ts +81 -0
- package/src/explainSpatialIndexing3D.test.ts +59 -0
- package/src/formatSpatialIndexingNotice.test.ts +30 -4
- package/src/spatialIndex.test.ts +65 -65
- package/src/spatialIndex3D.test.ts +273 -0
- package/src/spatialIndexingGuard.test.ts +188 -0
- package/src/uniformGrid.test.ts +54 -182
- package/src/uniformGrid3D.test.ts +358 -0
- package/dist/explainSpatialIndexing.d.ts +0 -3
- package/dist/explainSpatialIndexing.d.ts.map +0 -1
- package/dist/explainSpatialIndexing.js.map +0 -1
- package/src/explainSpatialIndexing.test.ts +0 -76
package/src/uniformGrid.test.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SpatialAabb2D, SpatialObjectId, SpatialPair } from '@flighthq/types/contract';
|
|
2
2
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { setSpatialIndexingGuard } from './spatialIndexingGuard';
|
|
5
|
+
import { MAX_INDEXED_CELLS_PER_OBJECT, createUniformGridSpatialBackend2D } from './uniformGrid';
|
|
5
6
|
|
|
6
7
|
afterEach(() => {
|
|
7
8
|
setSpatialIndexingGuard(null);
|
|
@@ -9,7 +10,7 @@ afterEach(() => {
|
|
|
9
10
|
|
|
10
11
|
// A plain AABB-overlap confirmation used to turn broadphase candidate pairs into confirmed pairs (the
|
|
11
12
|
// narrow-phase stand-in): exactly the check the caller would apply downstream.
|
|
12
|
-
function boundsOverlap(a: Readonly<
|
|
13
|
+
function boundsOverlap(a: Readonly<SpatialAabb2D>, b: Readonly<SpatialAabb2D>): boolean {
|
|
13
14
|
return a.minX < b.maxX && a.maxX > b.minX && a.minY < b.maxY && a.maxY > b.minY;
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -17,7 +18,10 @@ function pairKeys(pairs: readonly SpatialPair[]): string[] {
|
|
|
17
18
|
return pairs.map((p) => `${Math.min(p.a, p.b)}-${Math.max(p.a, p.b)}`).sort();
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function bruteForcePairKeys(
|
|
21
|
+
function bruteForcePairKeys(
|
|
22
|
+
objects: ReadonlyMap<SpatialObjectId, Readonly<SpatialAabb2D>>,
|
|
23
|
+
cellSize: number,
|
|
24
|
+
): string[] {
|
|
21
25
|
const entries = [...objects];
|
|
22
26
|
const pairs: string[] = [];
|
|
23
27
|
for (let i = 0; i < entries.length; i++) {
|
|
@@ -36,11 +40,11 @@ function bruteForcePairKeys(objects: ReadonlyMap<SpatialObjectId, Readonly<Spati
|
|
|
36
40
|
return pairs.sort();
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
function boundsContainPoint(bounds: Readonly<
|
|
43
|
+
function boundsContainPoint(bounds: Readonly<SpatialAabb2D>, x: number, y: number): boolean {
|
|
40
44
|
return x >= bounds.minX && x <= bounds.maxX && y >= bounds.minY && y <= bounds.maxY;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
function boundsIntersectRay(bounds: Readonly<
|
|
47
|
+
function boundsIntersectRay(bounds: Readonly<SpatialAabb2D>, ox: number, oy: number, dx: number, dy: number): boolean {
|
|
44
48
|
let tMin = -Infinity;
|
|
45
49
|
let tMax = Infinity;
|
|
46
50
|
if (dx === 0) {
|
|
@@ -62,7 +66,7 @@ function boundsIntersectRay(bounds: Readonly<SpatialAabb>, ox: number, oy: numbe
|
|
|
62
66
|
return tMax >= tMin && tMax >= 0;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
function boundsShareCell(a: Readonly<
|
|
69
|
+
function boundsShareCell(a: Readonly<SpatialAabb2D>, b: Readonly<SpatialAabb2D>, cellSize: number): boolean {
|
|
66
70
|
return (
|
|
67
71
|
Math.max(Math.floor(a.minX / cellSize), Math.floor(b.minX / cellSize)) <=
|
|
68
72
|
Math.min(Math.floor(a.maxX / cellSize), Math.floor(b.maxX / cellSize)) &&
|
|
@@ -81,7 +85,7 @@ function createTestRandom(seed: number): () => number {
|
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function isOverflowBounds(bounds: Readonly<
|
|
88
|
+
function isOverflowBounds(bounds: Readonly<SpatialAabb2D>, cellSize: number): boolean {
|
|
85
89
|
const width = Math.floor(bounds.maxX / cellSize) - Math.floor(bounds.minX / cellSize) + 1;
|
|
86
90
|
const height = Math.floor(bounds.maxY / cellSize) - Math.floor(bounds.minY / cellSize) + 1;
|
|
87
91
|
return width * height > MAX_INDEXED_CELLS_PER_OBJECT;
|
|
@@ -96,10 +100,10 @@ describe('brute-force property coverage', () => {
|
|
|
96
100
|
const cellSize = 4;
|
|
97
101
|
for (const seed of [0x1357_9bdf, 0x2468_ace0, 0x5eed_f00d, 0xc0ff_ee42]) {
|
|
98
102
|
const random = createTestRandom(seed);
|
|
99
|
-
const grid =
|
|
100
|
-
const objects = new Map<SpatialObjectId,
|
|
103
|
+
const grid = createUniformGridSpatialBackend2D(cellSize);
|
|
104
|
+
const objects = new Map<SpatialObjectId, SpatialAabb2D>();
|
|
101
105
|
|
|
102
|
-
function randomBounds(overflow: boolean):
|
|
106
|
+
function randomBounds(overflow: boolean): SpatialAabb2D {
|
|
103
107
|
const minX = Math.floor(random() * 160) - 80 + random();
|
|
104
108
|
const minY = Math.floor(random() * 160) - 80 + random();
|
|
105
109
|
const width = overflow ? cellSize * (34 + Math.floor(random() * 12)) : 0.25 + random() * 12;
|
|
@@ -114,7 +118,7 @@ describe('brute-force property coverage', () => {
|
|
|
114
118
|
|
|
115
119
|
const regionMinX = random() * 240 - 120;
|
|
116
120
|
const regionMinY = random() * 240 - 120;
|
|
117
|
-
const region:
|
|
121
|
+
const region: SpatialAabb2D = {
|
|
118
122
|
minX: regionMinX,
|
|
119
123
|
minY: regionMinY,
|
|
120
124
|
maxX: regionMinX + 1 + random() * 80,
|
|
@@ -202,7 +206,7 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
202
206
|
|
|
203
207
|
// Correctness only. Used where the occupied cell range is legitimately wide, so the walk is
|
|
204
208
|
// genuinely proportional to it — that is the documented conservative over-walk, not the defect.
|
|
205
|
-
function rayIds(grid: ReturnType<typeof
|
|
209
|
+
function rayIds(grid: ReturnType<typeof createUniformGridSpatialBackend2D>): SpatialObjectId[] {
|
|
206
210
|
const out: SpatialObjectId[] = [];
|
|
207
211
|
grid.querySpatialRay(-5, 0.5, 1, 0, out);
|
|
208
212
|
return out.sort((a, b) => a - b);
|
|
@@ -212,7 +216,7 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
212
216
|
// empty or tight, so any millions-of-cells walk means the range was stranded. Keeping these two
|
|
213
217
|
// helpers apart is the point: a single budgeted helper made two honest cases fail, because their
|
|
214
218
|
// titles claimed a seeding/removal property while the assertion silently demanded O(1).
|
|
215
|
-
function rayIdsFast(grid: ReturnType<typeof
|
|
219
|
+
function rayIdsFast(grid: ReturnType<typeof createUniformGridSpatialBackend2D>): SpatialObjectId[] {
|
|
216
220
|
const started = performance.now();
|
|
217
221
|
const ids = rayIds(grid);
|
|
218
222
|
expect(performance.now() - started).toBeLessThan(RAY_BUDGET_MS);
|
|
@@ -220,8 +224,8 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
// Two small objects placed FAR apart, so any stranded range spans millions of cells.
|
|
223
|
-
function gridWithWideRange(): ReturnType<typeof
|
|
224
|
-
const grid =
|
|
227
|
+
function gridWithWideRange(): ReturnType<typeof createUniformGridSpatialBackend2D> {
|
|
228
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
225
229
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
226
230
|
grid.insertSpatialObject(2, { minX: FAR, minY: 0, maxX: FAR + 1, maxY: 1 });
|
|
227
231
|
return grid;
|
|
@@ -266,7 +270,7 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
266
270
|
|
|
267
271
|
it('updates the last celled object into overflow, emptying the cells', () => {
|
|
268
272
|
// The overflow *transition* proper: no remove call, the object simply stops being celled.
|
|
269
|
-
const grid =
|
|
273
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
270
274
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
271
275
|
grid.insertSpatialObject(2, { minX: FAR, minY: 0, maxX: FAR + 1, maxY: 1 });
|
|
272
276
|
grid.updateSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
@@ -275,7 +279,7 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
275
279
|
});
|
|
276
280
|
|
|
277
281
|
it('updates an overflowed object back into cells, re-seeding rather than widening', () => {
|
|
278
|
-
const grid =
|
|
282
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
279
283
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
280
284
|
grid.insertSpatialObject(2, { minX: FAR, minY: 0, maxX: FAR + 1, maxY: 1 });
|
|
281
285
|
grid.updateSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
@@ -314,14 +318,14 @@ describe('cell range across every transition that can strand it', () => {
|
|
|
314
318
|
});
|
|
315
319
|
});
|
|
316
320
|
|
|
317
|
-
describe('
|
|
321
|
+
describe('createUniformGridSpatialBackend2D', () => {
|
|
318
322
|
it('keeps region comparisons reentrant when a bounds getter starts a nested query', () => {
|
|
319
|
-
const grid =
|
|
323
|
+
const grid = createUniformGridSpatialBackend2D(0);
|
|
320
324
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
321
325
|
grid.insertSpatialObject(2, { minX: 100, minY: 100, maxX: 101, maxY: 101 });
|
|
322
326
|
let minXReads = 0;
|
|
323
327
|
let nestedCalls = 0;
|
|
324
|
-
const region:
|
|
328
|
+
const region: SpatialAabb2D = {
|
|
325
329
|
get minX() {
|
|
326
330
|
minXReads++;
|
|
327
331
|
if (minXReads === 2) {
|
|
@@ -343,7 +347,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
343
347
|
});
|
|
344
348
|
|
|
345
349
|
it('emits a pair spanning several shared cells exactly once', () => {
|
|
346
|
-
const grid =
|
|
350
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
347
351
|
// Both objects cover the same 3x3 block of cells (0..2, 0..2), so they co-occupy nine cells.
|
|
348
352
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 25, maxY: 25 });
|
|
349
353
|
grid.insertSpatialObject(2, { minX: 5, minY: 5, maxX: 29, maxY: 29 });
|
|
@@ -356,7 +360,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
356
360
|
});
|
|
357
361
|
|
|
358
362
|
it('never pairs an object with itself', () => {
|
|
359
|
-
const grid =
|
|
363
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
360
364
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
|
|
361
365
|
|
|
362
366
|
const pairs: SpatialPair[] = [];
|
|
@@ -365,7 +369,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
365
369
|
});
|
|
366
370
|
|
|
367
371
|
it('returns nothing from every query on an empty grid', () => {
|
|
368
|
-
const grid =
|
|
372
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
369
373
|
|
|
370
374
|
const pairs: SpatialPair[] = [];
|
|
371
375
|
grid.querySpatialPairs(pairs);
|
|
@@ -385,7 +389,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
385
389
|
});
|
|
386
390
|
|
|
387
391
|
it('indexes objects at negative coordinates', () => {
|
|
388
|
-
const grid =
|
|
392
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
389
393
|
grid.insertSpatialObject(1, { minX: -8, minY: -8, maxX: -4, maxY: -4 });
|
|
390
394
|
grid.insertSpatialObject(2, { minX: -6, minY: -6, maxX: -2, maxY: -2 });
|
|
391
395
|
|
|
@@ -399,7 +403,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
399
403
|
});
|
|
400
404
|
|
|
401
405
|
it('gives the same confirmed pairs across different cell sizes', () => {
|
|
402
|
-
const objects: { id: SpatialObjectId; bounds:
|
|
406
|
+
const objects: { id: SpatialObjectId; bounds: SpatialAabb2D }[] = [
|
|
403
407
|
{ id: 1, bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 } },
|
|
404
408
|
{ id: 2, bounds: { minX: 5, minY: 5, maxX: 15, maxY: 15 } },
|
|
405
409
|
{ id: 3, bounds: { minX: 12, minY: 12, maxX: 20, maxY: 20 } },
|
|
@@ -408,7 +412,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
408
412
|
const boundsOf = new Map(objects.map((o) => [o.id, o.bounds]));
|
|
409
413
|
|
|
410
414
|
function confirmedPairs(cellSize: number): string[] {
|
|
411
|
-
const grid =
|
|
415
|
+
const grid = createUniformGridSpatialBackend2D(cellSize);
|
|
412
416
|
for (const o of objects) grid.insertSpatialObject(o.id, o.bounds);
|
|
413
417
|
const candidates: SpatialPair[] = [];
|
|
414
418
|
grid.querySpatialPairs(candidates);
|
|
@@ -426,7 +430,7 @@ describe('createUniformGridSpatialBackend', () => {
|
|
|
426
430
|
|
|
427
431
|
describe('MAX_INDEXED_CELLS_PER_OBJECT', () => {
|
|
428
432
|
it('is the per-object cell budget the oversized path is chosen by', () => {
|
|
429
|
-
const grid =
|
|
433
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
430
434
|
// A square block of exactly the budget stays on the ordinary path; one cell more does not. The
|
|
431
435
|
// budget is a count of cells, not an extent, so it is asserted through the count.
|
|
432
436
|
const side = Math.sqrt(MAX_INDEXED_CELLS_PER_OBJECT);
|
|
@@ -446,7 +450,7 @@ describe('MAX_INDEXED_CELLS_PER_OBJECT', () => {
|
|
|
446
450
|
|
|
447
451
|
describe('non-finite bounds', () => {
|
|
448
452
|
it('declines rather than indexing, and answers with the false sentinel', () => {
|
|
449
|
-
const grid =
|
|
453
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
450
454
|
expect(grid.insertSpatialObject(1, { minX: NaN, minY: 0, maxX: 10, maxY: 10 })).toBe(false);
|
|
451
455
|
expect(grid.insertSpatialObject(2, { minX: 0, minY: 0, maxX: Infinity, maxY: 10 })).toBe(false);
|
|
452
456
|
expect(grid.insertSpatialObject(3, { minX: 0, minY: -Infinity, maxX: 10, maxY: 10 })).toBe(false);
|
|
@@ -459,12 +463,12 @@ describe('non-finite bounds', () => {
|
|
|
459
463
|
});
|
|
460
464
|
|
|
461
465
|
it('returns true for finite bounds, so the sentinel distinguishes decline from success', () => {
|
|
462
|
-
const grid =
|
|
466
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
463
467
|
expect(grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 10, maxY: 10 })).toBe(true);
|
|
464
468
|
});
|
|
465
469
|
|
|
466
470
|
it('keeps a declined object out of every query rather than out of some', () => {
|
|
467
|
-
const grid =
|
|
471
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
468
472
|
grid.insertSpatialObject(1, { minX: NaN, minY: NaN, maxX: NaN, maxY: NaN });
|
|
469
473
|
grid.insertSpatialObject(2, { minX: 0, minY: 0, maxX: 10, maxY: 10 });
|
|
470
474
|
|
|
@@ -486,7 +490,7 @@ describe('non-finite bounds', () => {
|
|
|
486
490
|
});
|
|
487
491
|
|
|
488
492
|
it('drops an object that updates to non-finite bounds instead of stranding it at its old ones', () => {
|
|
489
|
-
const grid =
|
|
493
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
490
494
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 10, maxY: 10 });
|
|
491
495
|
expect(grid.updateSpatialObject(1, { minX: NaN, minY: NaN, maxX: NaN, maxY: NaN })).toBe(false);
|
|
492
496
|
expect(grid.explainSpatialIndexing(1).mode).toBe('declined');
|
|
@@ -496,7 +500,7 @@ describe('non-finite bounds', () => {
|
|
|
496
500
|
});
|
|
497
501
|
|
|
498
502
|
it('lets a declined object recover on a later finite update', () => {
|
|
499
|
-
const grid =
|
|
503
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
500
504
|
grid.insertSpatialObject(1, { minX: NaN, minY: NaN, maxX: NaN, maxY: NaN });
|
|
501
505
|
expect(grid.updateSpatialObject(1, { minX: 0, minY: 0, maxX: 10, maxY: 10 })).toBe(true);
|
|
502
506
|
expect(grid.explainSpatialIndexing(1).mode).toBe('cells');
|
|
@@ -511,7 +515,7 @@ describe('oversized region query', () => {
|
|
|
511
515
|
// wider than the world would walk extent-squared cells against a grid holding almost nothing.
|
|
512
516
|
// Measured at 69 ms for a 1000x1000-cell region over a one-object grid before the bound.
|
|
513
517
|
it('answers a region far wider than the grid without walking it cell by cell', () => {
|
|
514
|
-
const grid =
|
|
518
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
515
519
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
516
520
|
const out: SpatialObjectId[] = [];
|
|
517
521
|
grid.querySpatialRegion({ minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 }, out);
|
|
@@ -519,7 +523,7 @@ describe('oversized region query', () => {
|
|
|
519
523
|
});
|
|
520
524
|
|
|
521
525
|
it('returns the same objects either way it walks', () => {
|
|
522
|
-
const grid =
|
|
526
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
523
527
|
for (let i = 0; i < 40; i++) grid.insertSpatialObject(i, { minX: i, minY: 0, maxX: i + 0.5, maxY: 1 });
|
|
524
528
|
const wide: SpatialObjectId[] = [];
|
|
525
529
|
grid.querySpatialRegion({ minX: -1e9, minY: -1e9, maxX: 1e9, maxY: 1e9 }, wide);
|
|
@@ -530,7 +534,7 @@ describe('oversized region query', () => {
|
|
|
530
534
|
});
|
|
531
535
|
|
|
532
536
|
it('still excludes objects the wide region misses', () => {
|
|
533
|
-
const grid =
|
|
537
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
534
538
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
535
539
|
grid.insertSpatialObject(2, { minX: 5e11, minY: 0, maxX: 5e11 + 1, maxY: 1 });
|
|
536
540
|
const out: SpatialObjectId[] = [];
|
|
@@ -548,7 +552,7 @@ describe('oversized-extent bound', () => {
|
|
|
548
552
|
// wide, which is what a diverging rigid-body simulation actually produces) would be 1e24 cells and
|
|
549
553
|
// would never return, which is exactly why the bound cannot be tested at its motivating scale.
|
|
550
554
|
it('holds an oversized object without writing a cell per unit of its extent', () => {
|
|
551
|
-
const grid =
|
|
555
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
552
556
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 200, maxY: 200 });
|
|
553
557
|
const explanation = grid.explainSpatialIndexing(1);
|
|
554
558
|
expect(explanation.mode).toBe('overflow');
|
|
@@ -559,13 +563,13 @@ describe('oversized-extent bound', () => {
|
|
|
559
563
|
// Unreachable for an unbounded build (1e24 cells), so this one cannot be written as a
|
|
560
564
|
// before/after assertion — it is the proof that the motivating case is now O(1). Safe to run only
|
|
561
565
|
// because the assertion above fails first if the bound is ever removed.
|
|
562
|
-
const grid =
|
|
566
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
563
567
|
expect(grid.insertSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 })).toBe(true);
|
|
564
568
|
expect(grid.explainSpatialIndexing(1).mode).toBe('overflow');
|
|
565
569
|
});
|
|
566
570
|
|
|
567
571
|
it('keeps an oversized object queryable — the bound is a cost decision, not a dropped object', () => {
|
|
568
|
-
const grid =
|
|
572
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
569
573
|
grid.insertSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
570
574
|
grid.insertSpatialObject(2, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
571
575
|
|
|
@@ -587,7 +591,7 @@ describe('oversized-extent bound', () => {
|
|
|
587
591
|
});
|
|
588
592
|
|
|
589
593
|
it('emits each overflow pair once, including overflow against overflow', () => {
|
|
590
|
-
const grid =
|
|
594
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
591
595
|
grid.insertSpatialObject(1, { minX: -1e9, minY: -1e9, maxX: 1e9, maxY: 1e9 });
|
|
592
596
|
grid.insertSpatialObject(2, { minX: -1e9, minY: -1e9, maxX: 1e9, maxY: 1e9 });
|
|
593
597
|
grid.insertSpatialObject(3, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
@@ -597,7 +601,7 @@ describe('oversized-extent bound', () => {
|
|
|
597
601
|
});
|
|
598
602
|
|
|
599
603
|
it('does not pair an overflow object with a disjoint object', () => {
|
|
600
|
-
const grid =
|
|
604
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
601
605
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 100000, maxY: 100000 });
|
|
602
606
|
grid.insertSpatialObject(2, { minX: -50, minY: -50, maxX: -40, maxY: -40 });
|
|
603
607
|
const pairs: SpatialPair[] = [];
|
|
@@ -606,7 +610,7 @@ describe('oversized-extent bound', () => {
|
|
|
606
610
|
});
|
|
607
611
|
|
|
608
612
|
it('removes an oversized object without walking its extent, and stops returning it', () => {
|
|
609
|
-
const grid =
|
|
613
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
610
614
|
grid.insertSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
611
615
|
grid.removeSpatialObject(1);
|
|
612
616
|
expect(grid.explainSpatialIndexing(1).mode).toBe('absent');
|
|
@@ -616,7 +620,7 @@ describe('oversized-extent bound', () => {
|
|
|
616
620
|
});
|
|
617
621
|
|
|
618
622
|
it('moves an object between the celled and overflow paths as it grows and shrinks', () => {
|
|
619
|
-
const grid =
|
|
623
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
620
624
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 2, maxY: 2 });
|
|
621
625
|
expect(grid.explainSpatialIndexing(1).mode).toBe('cells');
|
|
622
626
|
grid.updateSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
@@ -631,7 +635,7 @@ describe('oversized-extent bound', () => {
|
|
|
631
635
|
});
|
|
632
636
|
|
|
633
637
|
it('clears overflow with the rest of the index', () => {
|
|
634
|
-
const grid =
|
|
638
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
635
639
|
grid.insertSpatialObject(1, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
636
640
|
grid.clearSpatialIndex();
|
|
637
641
|
expect(grid.explainSpatialIndexing(1).mode).toBe('absent');
|
|
@@ -644,7 +648,7 @@ describe('oversized-extent bound', () => {
|
|
|
644
648
|
// Without the bound the occupied cell range stretches to the oversized object's span, and every
|
|
645
649
|
// subsequent ray walks it. Two small objects far apart bound the range; the oversized one must
|
|
646
650
|
// not widen it, which shows up as the ray still resolving correctly and promptly.
|
|
647
|
-
const grid =
|
|
651
|
+
const grid = createUniformGridSpatialBackend2D(1);
|
|
648
652
|
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 1, maxY: 1 });
|
|
649
653
|
grid.insertSpatialObject(2, { minX: -1e12, minY: -1e12, maxX: 1e12, maxY: 1e12 });
|
|
650
654
|
const out: SpatialObjectId[] = [];
|
|
@@ -655,7 +659,7 @@ describe('oversized-extent bound', () => {
|
|
|
655
659
|
|
|
656
660
|
describe('ray edge cases', () => {
|
|
657
661
|
it('finds objects along a ray that passes exactly through cell corners', () => {
|
|
658
|
-
const grid =
|
|
662
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
659
663
|
grid.insertSpatialObject(1, { minX: 10, minY: 10, maxX: 12, maxY: 12 });
|
|
660
664
|
grid.insertSpatialObject(2, { minX: 20, minY: 20, maxX: 22, maxY: 22 });
|
|
661
665
|
const out: SpatialObjectId[] = [];
|
|
@@ -664,7 +668,7 @@ describe('ray edge cases', () => {
|
|
|
664
668
|
});
|
|
665
669
|
|
|
666
670
|
it('finds an object when the ray starts inside its bounds', () => {
|
|
667
|
-
const grid =
|
|
671
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
668
672
|
grid.insertSpatialObject(1, { minX: 5, minY: 5, maxX: 15, maxY: 15 });
|
|
669
673
|
const out: SpatialObjectId[] = [];
|
|
670
674
|
grid.querySpatialRay(10, 10, -1, 0, out);
|
|
@@ -672,7 +676,7 @@ describe('ray edge cases', () => {
|
|
|
672
676
|
});
|
|
673
677
|
|
|
674
678
|
it('clips a ray entering the occupied range from far outside', () => {
|
|
675
|
-
const grid =
|
|
679
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
676
680
|
grid.insertSpatialObject(1, { minX: 100, minY: 30, maxX: 110, maxY: 40 });
|
|
677
681
|
const out: SpatialObjectId[] = [];
|
|
678
682
|
grid.querySpatialRay(-1e9, 35, 1, 0, out);
|
|
@@ -680,141 +684,9 @@ describe('ray edge cases', () => {
|
|
|
680
684
|
});
|
|
681
685
|
});
|
|
682
686
|
|
|
683
|
-
describe('
|
|
684
|
-
it('reports a decline with its reason, and no span', () => {
|
|
685
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
686
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
687
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
688
|
-
grid.insertSpatialObject(7, { minX: NaN, minY: 0, maxX: 10, maxY: 10 });
|
|
689
|
-
expect(notices).toEqual([
|
|
690
|
-
{
|
|
691
|
-
cellSize: 10,
|
|
692
|
-
id: 7,
|
|
693
|
-
mode: 'declined',
|
|
694
|
-
operation: 'insert',
|
|
695
|
-
reason: 'non-finite-bounds',
|
|
696
|
-
wouldOccupyBucketCount: 0,
|
|
697
|
-
},
|
|
698
|
-
]);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it('reports an overflow with the span the bound refused to walk', () => {
|
|
702
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
703
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
704
|
-
const grid = createUniformGridSpatialBackend(1);
|
|
705
|
-
grid.insertSpatialObject(7, { minX: 0, minY: 0, maxX: 199, maxY: 199 });
|
|
706
|
-
expect(notices).toEqual([
|
|
707
|
-
{
|
|
708
|
-
cellSize: 1,
|
|
709
|
-
id: 7,
|
|
710
|
-
mode: 'overflow',
|
|
711
|
-
operation: 'insert',
|
|
712
|
-
reason: null,
|
|
713
|
-
wouldOccupyBucketCount: 40000,
|
|
714
|
-
},
|
|
715
|
-
]);
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
it('reports an invalid cell size and keeps results correct through the bounded overflow path', () => {
|
|
719
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
720
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
721
|
-
for (const cellSize of [0, -1]) {
|
|
722
|
-
const grid = createUniformGridSpatialBackend(cellSize);
|
|
723
|
-
expect(grid.insertSpatialObject(7, { minX: 0, minY: 0, maxX: 10, maxY: 10 })).toBe(true);
|
|
724
|
-
const point: SpatialObjectId[] = [];
|
|
725
|
-
grid.querySpatialPoint(5, 5, point);
|
|
726
|
-
expect(point).toEqual([7]);
|
|
727
|
-
}
|
|
728
|
-
expect(notices).toEqual(
|
|
729
|
-
[0, -1].map((cellSize) => ({
|
|
730
|
-
cellSize,
|
|
731
|
-
id: 7,
|
|
732
|
-
mode: 'overflow',
|
|
733
|
-
operation: 'insert',
|
|
734
|
-
reason: 'invalid-cell-size',
|
|
735
|
-
wouldOccupyBucketCount: 0,
|
|
736
|
-
})),
|
|
737
|
-
);
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
it('declines and reports inverted bounds', () => {
|
|
741
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
742
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
743
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
744
|
-
expect(grid.insertSpatialObject(7, { minX: 10, minY: 0, maxX: 0, maxY: 10 })).toBe(false);
|
|
745
|
-
expect(grid.explainSpatialIndexing(7)).toEqual({
|
|
746
|
-
bucketCount: 0,
|
|
747
|
-
id: 7,
|
|
748
|
-
mode: 'declined',
|
|
749
|
-
reason: 'inverted-bounds',
|
|
750
|
-
});
|
|
751
|
-
expect(notices).toEqual([
|
|
752
|
-
{
|
|
753
|
-
cellSize: 10,
|
|
754
|
-
id: 7,
|
|
755
|
-
mode: 'declined',
|
|
756
|
-
operation: 'insert',
|
|
757
|
-
reason: 'inverted-bounds',
|
|
758
|
-
wouldOccupyBucketCount: 0,
|
|
759
|
-
},
|
|
760
|
-
]);
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
it('reports update and remove operations whose id was never inserted', () => {
|
|
764
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
765
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
766
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
767
|
-
expect(grid.updateSpatialObject(7, { minX: 0, minY: 0, maxX: 5, maxY: 5 })).toBe(true);
|
|
768
|
-
grid.removeSpatialObject(8);
|
|
769
|
-
expect(notices).toEqual([
|
|
770
|
-
{
|
|
771
|
-
cellSize: 10,
|
|
772
|
-
id: 7,
|
|
773
|
-
mode: 'cells',
|
|
774
|
-
operation: 'update',
|
|
775
|
-
reason: 'missing-id',
|
|
776
|
-
wouldOccupyBucketCount: 0,
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
cellSize: 10,
|
|
780
|
-
id: 8,
|
|
781
|
-
mode: 'absent',
|
|
782
|
-
operation: 'remove',
|
|
783
|
-
reason: 'missing-id',
|
|
784
|
-
wouldOccupyBucketCount: 0,
|
|
785
|
-
},
|
|
786
|
-
]);
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
it('stays silent on the ordinary path', () => {
|
|
790
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
791
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
792
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
793
|
-
grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 10, maxY: 10 });
|
|
794
|
-
grid.removeSpatialObject(1);
|
|
795
|
-
expect(notices).toEqual([]);
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
it('null uninstalls it', () => {
|
|
799
|
-
const notices: SpatialIndexingNotice[] = [];
|
|
800
|
-
setSpatialIndexingGuard((notice) => notices.push({ ...notice }));
|
|
801
|
-
setSpatialIndexingGuard(null);
|
|
802
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
803
|
-
grid.insertSpatialObject(1, { minX: NaN, minY: 0, maxX: 10, maxY: 10 });
|
|
804
|
-
expect(notices).toEqual([]);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
it('does not change what insert returns', () => {
|
|
808
|
-
setSpatialIndexingGuard(() => {});
|
|
809
|
-
const grid = createUniformGridSpatialBackend(10);
|
|
810
|
-
expect(grid.insertSpatialObject(1, { minX: NaN, minY: 0, maxX: 10, maxY: 10 })).toBe(false);
|
|
811
|
-
expect(grid.insertSpatialObject(2, { minX: 0, minY: 0, maxX: 10, maxY: 10 })).toBe(true);
|
|
812
|
-
});
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
describe('updateSpatialObject', () => {
|
|
687
|
+
describe('updateSpatialObject2D', () => {
|
|
816
688
|
it('refreshes exact bounds without retaining the caller object when the covered cells stay unchanged', () => {
|
|
817
|
-
const grid =
|
|
689
|
+
const grid = createUniformGridSpatialBackend2D(10);
|
|
818
690
|
grid.insertSpatialObject(1, { minX: 1, minY: 1, maxX: 18, maxY: 18 });
|
|
819
691
|
const updated = { minX: 4, minY: 4, maxX: 19, maxY: 19 };
|
|
820
692
|
|