@flighthq/spatial 0.4.0-next.1663.d9c5746 → 0.4.0-next.1694.598ef6f

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