@flighthq/spatial 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ export * from './spatialIndex';
2
+ export * from './uniformGrid';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './spatialIndex';
2
+ export * from './uniformGrid';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { SpatialAabb, SpatialIndex, SpatialIndexBackend, SpatialObjectId, SpatialPair } from '@flighthq/types';
2
+ export declare function clearSpatialIndex(index: Readonly<SpatialIndex>): void;
3
+ export declare function createSpatialIndex(backend?: SpatialIndexBackend): SpatialIndex;
4
+ export declare function insertSpatialObject(index: Readonly<SpatialIndex>, id: SpatialObjectId, bounds: Readonly<SpatialAabb>): void;
5
+ export declare function querySpatialPairs(index: Readonly<SpatialIndex>, out: SpatialPair[]): void;
6
+ export declare function querySpatialPoint(index: Readonly<SpatialIndex>, x: number, y: number, out: SpatialObjectId[]): void;
7
+ export declare function querySpatialRay(index: Readonly<SpatialIndex>, x: number, y: number, dx: number, dy: number, out: SpatialObjectId[]): void;
8
+ export declare function querySpatialRegion(index: Readonly<SpatialIndex>, region: Readonly<SpatialAabb>, out: SpatialObjectId[]): void;
9
+ export declare function removeSpatialObject(index: Readonly<SpatialIndex>, id: SpatialObjectId): void;
10
+ export declare function updateSpatialObject(index: Readonly<SpatialIndex>, id: SpatialObjectId, bounds: Readonly<SpatialAabb>): void;
11
+ //# sourceMappingURL=spatialIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatialIndex.d.ts","sourceRoot":"","sources":["../src/spatialIndex.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKpH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAErE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAM9E;AAID,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC7B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC5B,IAAI,CAEN;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,IAAI,CAEzF;AAGD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,EAAE,GAAG,IAAI,CAEnH;AAID,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC7B,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,EACT,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAGD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC7B,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,EAC7B,GAAG,EAAE,eAAe,EAAE,GACrB,IAAI,CAEN;AAGD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,eAAe,GAAG,IAAI,CAE5F;AAGD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC7B,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,GAC5B,IAAI,CAEN"}
@@ -0,0 +1,52 @@
1
+ import { createUniformGridSpatialBackend } from './uniformGrid';
2
+ // Empties the index of all objects while keeping it (and its backend) reusable.
3
+ export function clearSpatialIndex(index) {
4
+ index.runtime.backend.clearSpatialIndex();
5
+ }
6
+ // Creates a 2D broadphase index. With no backend it defaults to a uniform grid sized for
7
+ // medium-scale scenes; pass an explicit backend (a differently-sized grid, or a future quadtree /
8
+ // sweep-and-prune) to select the structure for the workload. Constructing the default grid happens
9
+ // here, on call — importing the package has no side effect.
10
+ export function createSpatialIndex(backend) {
11
+ return {
12
+ runtime: {
13
+ backend: backend ?? createUniformGridSpatialBackend(DEFAULT_SPATIAL_CELL_SIZE),
14
+ },
15
+ };
16
+ }
17
+ // Adds an object to the index under `id` with its current bounds. The bounds are copied; the caller
18
+ // may reuse its own value afterward.
19
+ export function insertSpatialObject(index, id, bounds) {
20
+ index.runtime.backend.insertSpatialObject(id, bounds);
21
+ }
22
+ // Fills `out` (cleared first) with every deduplicated candidate pair — each unordered pair at most
23
+ // once, never an object with itself. A pair is a broadphase candidate: the two objects are close
24
+ // enough to be worth a narrow-phase test, which the caller (or @flighthq/collision) performs.
25
+ export function querySpatialPairs(index, out) {
26
+ index.runtime.backend.querySpatialPairs(out);
27
+ }
28
+ // Fills `out` (cleared first) with the ids whose bounds contain the point (`x`,`y`).
29
+ export function querySpatialPoint(index, x, y, out) {
30
+ index.runtime.backend.querySpatialPoint(x, y, out);
31
+ }
32
+ // Fills `out` (cleared first) with the ids whose bounds the ray from (`x`,`y`) along (`dx`,`dy`)
33
+ // intersects. The direction need not be normalized; the ray is treated as extending forward only.
34
+ export function querySpatialRay(index, x, y, dx, dy, out) {
35
+ index.runtime.backend.querySpatialRay(x, y, dx, dy, out);
36
+ }
37
+ // Fills `out` (cleared first) with the ids whose bounds overlap `region`.
38
+ export function querySpatialRegion(index, region, out) {
39
+ index.runtime.backend.querySpatialRegion(region, out);
40
+ }
41
+ // Removes an object from the index. A no-op if the id is not present.
42
+ export function removeSpatialObject(index, id) {
43
+ index.runtime.backend.removeSpatialObject(id);
44
+ }
45
+ // Moves an already-inserted object to new bounds. Inserting a not-yet-present id behaves as insert.
46
+ export function updateSpatialObject(index, id, bounds) {
47
+ index.runtime.backend.updateSpatialObject(id, bounds);
48
+ }
49
+ // The default uniform-grid cell size when createSpatialIndex is called without an explicit backend —
50
+ // a middle-of-the-road choice; a workload with a known typical object size should pass its own grid.
51
+ const DEFAULT_SPATIAL_CELL_SIZE = 128;
52
+ //# sourceMappingURL=spatialIndex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatialIndex.js","sourceRoot":"","sources":["../src/spatialIndex.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,+BAA+B,EAAE,MAAM,eAAe,CAAC;AAEhE,gFAAgF;AAChF,MAAM,UAAU,iBAAiB,CAAC,KAA6B;IAC7D,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC;AAC5C,CAAC;AAED,yFAAyF;AACzF,kGAAkG;AAClG,mGAAmG;AACnG,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC9D,OAAO;QACL,OAAO,EAAE;YACP,OAAO,EAAE,OAAO,IAAI,+BAA+B,CAAC,yBAAyB,CAAC;SAC/E;KACF,CAAC;AACJ,CAAC;AAED,oGAAoG;AACpG,qCAAqC;AACrC,MAAM,UAAU,mBAAmB,CACjC,KAA6B,EAC7B,EAAmB,EACnB,MAA6B;IAE7B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED,mGAAmG;AACnG,iGAAiG;AACjG,8FAA8F;AAC9F,MAAM,UAAU,iBAAiB,CAAC,KAA6B,EAAE,GAAkB;IACjF,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAC/C,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,KAA6B,EAAE,CAAS,EAAE,CAAS,EAAE,GAAsB;IAC3G,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;AACrD,CAAC;AAED,iGAAiG;AACjG,kGAAkG;AAClG,MAAM,UAAU,eAAe,CAC7B,KAA6B,EAC7B,CAAS,EACT,CAAS,EACT,EAAU,EACV,EAAU,EACV,GAAsB;IAEtB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;AAC3D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB,CAChC,KAA6B,EAC7B,MAA6B,EAC7B,GAAsB;IAEtB,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,mBAAmB,CAAC,KAA6B,EAAE,EAAmB;IACpF,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;AAChD,CAAC;AAED,oGAAoG;AACpG,MAAM,UAAU,mBAAmB,CACjC,KAA6B,EAC7B,EAAmB,EACnB,MAA6B;IAE7B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED,qGAAqG;AACrG,qGAAqG;AACrG,MAAM,yBAAyB,GAAG,GAAG,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { SpatialIndexBackend } from '@flighthq/types';
2
+ export declare function createUniformGridSpatialBackend(cellSize: number): SpatialIndexBackend;
3
+ //# sourceMappingURL=uniformGrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uniformGrid.d.ts","sourceRoot":"","sources":["../src/uniformGrid.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA8B,mBAAmB,EAAgC,MAAM,iBAAiB,CAAC;AASrH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CA0CrF"}
@@ -0,0 +1,350 @@
1
+ import { containsRectanglePointXY, intersectsRectangle } from '@flighthq/geometry';
2
+ // Builds a uniform-grid (spatial-hash) backend: an object's AABB is mapped to the rectangular block
3
+ // of fixed-size cells it covers, and each cell holds the ids overlapping it. Co-located objects share
4
+ // a cell, so candidate pairs and region/point/ray hits are found by looking only at the relevant
5
+ // cells instead of scanning every object. `cellSize` is the world-space side length of one cell; a
6
+ // good value is roughly the size of a typical object (too small over-spans large objects across many
7
+ // cells, too large lumps unrelated objects together). No import-time side effect — the caller
8
+ // constructs a grid explicitly, and createSpatialIndex uses this as its default index.
9
+ export function createUniformGridSpatialBackend(cellSize) {
10
+ const grid = {
11
+ cellSize,
12
+ cells: new Map(),
13
+ bounds: new Map(),
14
+ minCellX: 0,
15
+ minCellY: 0,
16
+ maxCellX: 0,
17
+ maxCellY: 0,
18
+ empty: true,
19
+ seen: new Set(),
20
+ };
21
+ return {
22
+ insertSpatialObject(id, bounds) {
23
+ _insertIntoGrid(grid, id, bounds);
24
+ },
25
+ updateSpatialObject(id, bounds) {
26
+ _removeFromGrid(grid, id);
27
+ _insertIntoGrid(grid, id, bounds);
28
+ },
29
+ removeSpatialObject(id) {
30
+ _removeFromGrid(grid, id);
31
+ },
32
+ clearSpatialIndex() {
33
+ grid.cells.clear();
34
+ grid.bounds.clear();
35
+ grid.seen.clear();
36
+ grid.empty = true;
37
+ },
38
+ querySpatialPairs(out) {
39
+ _queryGridPairs(grid, out);
40
+ },
41
+ querySpatialRegion(region, out) {
42
+ _queryGridRegion(grid, region, out);
43
+ },
44
+ querySpatialPoint(x, y, out) {
45
+ _queryGridPoint(grid, x, y, out);
46
+ },
47
+ querySpatialRay(x, y, dx, dy, out) {
48
+ _queryGridRay(grid, x, y, dx, dy, out);
49
+ },
50
+ };
51
+ }
52
+ // Maps a world coordinate to its cell index along one axis. Uses floor so negative coordinates map
53
+ // to consistently decreasing cell indices (world 0 is the boundary between cell -1 and cell 0).
54
+ function _cellIndex(coord, cellSize) {
55
+ return Math.floor(coord / cellSize);
56
+ }
57
+ // The map key for a cell. A string of the two signed integer coordinates — negatives and large
58
+ // magnitudes are represented exactly, unlike a numeric pairing that could overflow or collide.
59
+ function _cellKey(cx, cy) {
60
+ return `${cx},${cy}`;
61
+ }
62
+ // Fills a scratch RectangleLike (x/y/width/height) from a SpatialAabb (min/max corners) so the
63
+ // geometry rectangle-overlap and point-containment helpers can be reused instead of re-deriving the
64
+ // AABB math here. Assumes a normalized AABB (max >= min); a flipped AABB yields a negative extent,
65
+ // which the geometry helpers still normalize internally.
66
+ function _fillRectFromAabb(out, aabb) {
67
+ out.x = aabb.minX;
68
+ out.y = aabb.minY;
69
+ out.width = aabb.maxX - aabb.minX;
70
+ out.height = aabb.maxY - aabb.minY;
71
+ }
72
+ // Adds an object to every cell its AABB covers, storing a private copy of the bounds and expanding
73
+ // the occupied cell range. The caller may safely mutate or reuse the passed bounds afterward.
74
+ function _insertIntoGrid(grid, id, bounds) {
75
+ const cs = grid.cellSize;
76
+ const cx0 = _cellIndex(bounds.minX, cs);
77
+ const cx1 = _cellIndex(bounds.maxX, cs);
78
+ const cy0 = _cellIndex(bounds.minY, cs);
79
+ const cy1 = _cellIndex(bounds.maxY, cs);
80
+ grid.bounds.set(id, { minX: bounds.minX, minY: bounds.minY, maxX: bounds.maxX, maxY: bounds.maxY });
81
+ for (let cy = cy0; cy <= cy1; cy++) {
82
+ for (let cx = cx0; cx <= cx1; cx++) {
83
+ const key = _cellKey(cx, cy);
84
+ let cell = grid.cells.get(key);
85
+ if (cell === undefined) {
86
+ cell = { cx, cy, ids: new Set() };
87
+ grid.cells.set(key, cell);
88
+ }
89
+ cell.ids.add(id);
90
+ }
91
+ }
92
+ if (grid.empty) {
93
+ grid.minCellX = cx0;
94
+ grid.maxCellX = cx1;
95
+ grid.minCellY = cy0;
96
+ grid.maxCellY = cy1;
97
+ grid.empty = false;
98
+ }
99
+ else {
100
+ if (cx0 < grid.minCellX)
101
+ grid.minCellX = cx0;
102
+ if (cx1 > grid.maxCellX)
103
+ grid.maxCellX = cx1;
104
+ if (cy0 < grid.minCellY)
105
+ grid.minCellY = cy0;
106
+ if (cy1 > grid.maxCellY)
107
+ grid.maxCellY = cy1;
108
+ }
109
+ }
110
+ // Reports whether an AABB contains the point (`x`,`y`), reusing the geometry rectangle helper.
111
+ function _isSpatialAabbContainsPoint(aabb, x, y) {
112
+ _fillRectFromAabb(_scratchRectA, aabb);
113
+ return containsRectanglePointXY(_scratchRectA, x, y);
114
+ }
115
+ // Reports whether two AABBs overlap, reusing the geometry rectangle-overlap helper (edge-touching
116
+ // counts as disjoint, matching intersectsRectangle).
117
+ function _isSpatialAabbOverlapping(a, b) {
118
+ _fillRectFromAabb(_scratchRectA, a);
119
+ _fillRectFromAabb(_scratchRectB, b);
120
+ return intersectsRectangle(_scratchRectA, _scratchRectB);
121
+ }
122
+ // Slab test for a ray (origin `ox`,`oy`, direction `dx`,`dy`, `t >= 0`) against an axis-aligned box.
123
+ // Returns the entry parameter `t` (0 when the origin is already inside), or -1 when the ray misses
124
+ // the box or the box lies entirely behind the origin. Direction need not be normalized.
125
+ function _rayBoxEntryT(ox, oy, dx, dy, minX, minY, maxX, maxY) {
126
+ let tmin = -Infinity;
127
+ let tmax = Infinity;
128
+ if (dx !== 0) {
129
+ const inv = 1 / dx;
130
+ let t1 = (minX - ox) * inv;
131
+ let t2 = (maxX - ox) * inv;
132
+ if (t1 > t2) {
133
+ const t = t1;
134
+ t1 = t2;
135
+ t2 = t;
136
+ }
137
+ if (t1 > tmin)
138
+ tmin = t1;
139
+ if (t2 < tmax)
140
+ tmax = t2;
141
+ }
142
+ else if (ox < minX || ox > maxX) {
143
+ return -1;
144
+ }
145
+ if (dy !== 0) {
146
+ const inv = 1 / dy;
147
+ let t1 = (minY - oy) * inv;
148
+ let t2 = (maxY - oy) * inv;
149
+ if (t1 > t2) {
150
+ const t = t1;
151
+ t1 = t2;
152
+ t2 = t;
153
+ }
154
+ if (t1 > tmin)
155
+ tmin = t1;
156
+ if (t2 < tmax)
157
+ tmax = t2;
158
+ }
159
+ else if (oy < minY || oy > maxY) {
160
+ return -1;
161
+ }
162
+ if (tmax < tmin || tmax < 0)
163
+ return -1;
164
+ return tmin > 0 ? tmin : 0;
165
+ }
166
+ // Removes an object from every cell its stored AABB covered and drops now-empty cells. The occupied
167
+ // cell range is intentionally not shrunk (only reset when the grid fully empties). A no-op for an
168
+ // unknown id.
169
+ function _removeFromGrid(grid, id) {
170
+ const bounds = grid.bounds.get(id);
171
+ if (bounds === undefined)
172
+ return;
173
+ const cs = grid.cellSize;
174
+ const cx0 = _cellIndex(bounds.minX, cs);
175
+ const cx1 = _cellIndex(bounds.maxX, cs);
176
+ const cy0 = _cellIndex(bounds.minY, cs);
177
+ const cy1 = _cellIndex(bounds.maxY, cs);
178
+ for (let cy = cy0; cy <= cy1; cy++) {
179
+ for (let cx = cx0; cx <= cx1; cx++) {
180
+ const key = _cellKey(cx, cy);
181
+ const cell = grid.cells.get(key);
182
+ if (cell === undefined)
183
+ continue;
184
+ cell.ids.delete(id);
185
+ if (cell.ids.size === 0)
186
+ grid.cells.delete(key);
187
+ }
188
+ }
189
+ grid.bounds.delete(id);
190
+ if (grid.bounds.size === 0)
191
+ grid.empty = true;
192
+ }
193
+ // Enumerates candidate pairs. Within each cell every co-occupant pair is a candidate, but a pair may
194
+ // share several cells; to emit it exactly once, a pair is emitted only from its canonical cell — the
195
+ // top-left (min x, min y) cell of the two objects' overlapping cell ranges, which both objects are
196
+ // guaranteed to occupy. Ids are ordered a < b so the unordered pair is canonical. A pair is never
197
+ // (a,a). The pair is a broadphase candidate (shared cell locality); the caller confirms real overlap.
198
+ function _queryGridPairs(grid, out) {
199
+ out.length = 0;
200
+ const cs = grid.cellSize;
201
+ for (const cell of grid.cells.values()) {
202
+ const ids = cell.ids;
203
+ if (ids.size < 2)
204
+ continue;
205
+ const list = [...ids];
206
+ for (let i = 0; i < list.length; i++) {
207
+ for (let j = i + 1; j < list.length; j++) {
208
+ let a = list[i];
209
+ let b = list[j];
210
+ if (a > b) {
211
+ const t = a;
212
+ a = b;
213
+ b = t;
214
+ }
215
+ const ab = grid.bounds.get(a);
216
+ const bb = grid.bounds.get(b);
217
+ if (ab === undefined || bb === undefined)
218
+ continue;
219
+ const canonicalX = Math.max(_cellIndex(ab.minX, cs), _cellIndex(bb.minX, cs));
220
+ const canonicalY = Math.max(_cellIndex(ab.minY, cs), _cellIndex(bb.minY, cs));
221
+ if (cell.cx === canonicalX && cell.cy === canonicalY)
222
+ out.push({ a, b });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ // Gathers the ids in the cell containing the point, then confirms each against its real bounds. A
228
+ // single cell holds each id at most once, so no dedup pass is needed.
229
+ function _queryGridPoint(grid, x, y, out) {
230
+ out.length = 0;
231
+ const cs = grid.cellSize;
232
+ const cell = grid.cells.get(_cellKey(_cellIndex(x, cs), _cellIndex(y, cs)));
233
+ if (cell === undefined)
234
+ return;
235
+ for (const id of cell.ids) {
236
+ const bounds = grid.bounds.get(id);
237
+ if (bounds !== undefined && _isSpatialAabbContainsPoint(bounds, x, y))
238
+ out.push(id);
239
+ }
240
+ }
241
+ // Walks the cells the ray crosses (bounded to the occupied cell range) via an amanatides-woo DDA,
242
+ // gathering deduplicated ids, then confirms each against a real ray-vs-AABB slab test — so a cell
243
+ // co-occupant the ray does not actually strike is dropped. A zero-length direction degenerates to a
244
+ // point query at the origin. An empty grid returns nothing.
245
+ function _queryGridRay(grid, ox, oy, dx, dy, out) {
246
+ out.length = 0;
247
+ if (grid.empty)
248
+ return;
249
+ const cs = grid.cellSize;
250
+ const seen = grid.seen;
251
+ seen.clear();
252
+ if (dx === 0 && dy === 0) {
253
+ _queryGridPoint(grid, ox, oy, out);
254
+ return;
255
+ }
256
+ const boxMinX = grid.minCellX * cs;
257
+ const boxMinY = grid.minCellY * cs;
258
+ const boxMaxX = (grid.maxCellX + 1) * cs;
259
+ const boxMaxY = (grid.maxCellY + 1) * cs;
260
+ const tEnter = _rayBoxEntryT(ox, oy, dx, dy, boxMinX, boxMinY, boxMaxX, boxMaxY);
261
+ if (tEnter < 0)
262
+ return;
263
+ const startX = ox + tEnter * dx;
264
+ const startY = oy + tEnter * dy;
265
+ let cx = _cellIndex(startX, cs);
266
+ let cy = _cellIndex(startY, cs);
267
+ if (cx < grid.minCellX)
268
+ cx = grid.minCellX;
269
+ else if (cx > grid.maxCellX)
270
+ cx = grid.maxCellX;
271
+ if (cy < grid.minCellY)
272
+ cy = grid.minCellY;
273
+ else if (cy > grid.maxCellY)
274
+ cy = grid.maxCellY;
275
+ const stepX = dx > 0 ? 1 : dx < 0 ? -1 : 0;
276
+ const stepY = dy > 0 ? 1 : dy < 0 ? -1 : 0;
277
+ let tMaxX = Infinity;
278
+ let tDeltaX = Infinity;
279
+ if (stepX !== 0) {
280
+ const boundary = stepX > 0 ? (cx + 1) * cs : cx * cs;
281
+ tMaxX = (boundary - ox) / dx;
282
+ tDeltaX = cs / Math.abs(dx);
283
+ }
284
+ let tMaxY = Infinity;
285
+ let tDeltaY = Infinity;
286
+ if (stepY !== 0) {
287
+ const boundary = stepY > 0 ? (cy + 1) * cs : cy * cs;
288
+ tMaxY = (boundary - oy) / dy;
289
+ tDeltaY = cs / Math.abs(dy);
290
+ }
291
+ const maxSteps = grid.maxCellX - grid.minCellX + (grid.maxCellY - grid.minCellY) + 3;
292
+ for (let step = 0; step <= maxSteps; step++) {
293
+ if (cx < grid.minCellX || cx > grid.maxCellX || cy < grid.minCellY || cy > grid.maxCellY)
294
+ break;
295
+ const cell = grid.cells.get(_cellKey(cx, cy));
296
+ if (cell !== undefined) {
297
+ for (const id of cell.ids)
298
+ seen.add(id);
299
+ }
300
+ if (tMaxX < tMaxY) {
301
+ cx += stepX;
302
+ tMaxX += tDeltaX;
303
+ }
304
+ else {
305
+ cy += stepY;
306
+ tMaxY += tDeltaY;
307
+ }
308
+ }
309
+ for (const id of seen) {
310
+ const bounds = grid.bounds.get(id);
311
+ if (bounds !== undefined &&
312
+ _rayBoxEntryT(ox, oy, dx, dy, bounds.minX, bounds.minY, bounds.maxX, bounds.maxY) >= 0) {
313
+ out.push(id);
314
+ }
315
+ }
316
+ }
317
+ // Gathers the ids in every cell the region covers (deduplicated via the reused scratch set), then
318
+ // confirms each against the region's real bounds — so a false cell-mate whose bounds miss the region
319
+ // is dropped.
320
+ function _queryGridRegion(grid, region, out) {
321
+ out.length = 0;
322
+ const cs = grid.cellSize;
323
+ const seen = grid.seen;
324
+ seen.clear();
325
+ const cx0 = _cellIndex(region.minX, cs);
326
+ const cx1 = _cellIndex(region.maxX, cs);
327
+ const cy0 = _cellIndex(region.minY, cs);
328
+ const cy1 = _cellIndex(region.maxY, cs);
329
+ for (let cy = cy0; cy <= cy1; cy++) {
330
+ for (let cx = cx0; cx <= cx1; cx++) {
331
+ const cell = grid.cells.get(_cellKey(cx, cy));
332
+ if (cell === undefined)
333
+ continue;
334
+ for (const id of cell.ids) {
335
+ if (seen.has(id))
336
+ continue;
337
+ seen.add(id);
338
+ const bounds = grid.bounds.get(id);
339
+ if (bounds !== undefined && _isSpatialAabbOverlapping(bounds, region))
340
+ out.push(id);
341
+ }
342
+ }
343
+ }
344
+ }
345
+ // Reused scratch rectangles for the geometry overlap/containment helpers. Only ever read+written
346
+ // within a single non-nested helper call, so sharing them across the module allocates nothing per
347
+ // query without aliasing hazard.
348
+ const _scratchRectA = { x: 0, y: 0, width: 0, height: 0 };
349
+ const _scratchRectB = { x: 0, y: 0, width: 0, height: 0 };
350
+ //# sourceMappingURL=uniformGrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uniformGrid.js","sourceRoot":"","sources":["../src/uniformGrid.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/spatial",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/geometry": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "2D broadphase spatial index over object AABBs — candidate-pair, region, point, and ray queries behind a swappable index seam (uniform-grid default), feeding @flighthq/collision narrow-phase",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,184 @@
1
+ import type { SpatialIndex, SpatialObjectId, SpatialPair } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ clearSpatialIndex,
6
+ createSpatialIndex,
7
+ insertSpatialObject,
8
+ querySpatialPairs,
9
+ querySpatialPoint,
10
+ querySpatialRay,
11
+ querySpatialRegion,
12
+ removeSpatialObject,
13
+ updateSpatialObject,
14
+ } from './spatialIndex';
15
+ import { createUniformGridSpatialBackend } from './uniformGrid';
16
+
17
+ function makeIndex(cellSize = 10): SpatialIndex {
18
+ return createSpatialIndex(createUniformGridSpatialBackend(cellSize));
19
+ }
20
+
21
+ // Sorts pairs into canonical "a-b" strings so a result can be compared as a set regardless of order.
22
+ function pairKeys(pairs: readonly SpatialPair[]): string[] {
23
+ return pairs.map((p) => `${Math.min(p.a, p.b)}-${Math.max(p.a, p.b)}`).sort();
24
+ }
25
+
26
+ describe('clearSpatialIndex', () => {
27
+ it('empties every query after clearing', () => {
28
+ const index = makeIndex();
29
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
30
+ insertSpatialObject(index, 2, { minX: 2, minY: 2, maxX: 6, maxY: 6 });
31
+
32
+ clearSpatialIndex(index);
33
+
34
+ const pairs: SpatialPair[] = [];
35
+ querySpatialPairs(index, pairs);
36
+ expect(pairs).toHaveLength(0);
37
+
38
+ const region: SpatialObjectId[] = [];
39
+ querySpatialRegion(index, { minX: 0, minY: 0, maxX: 10, maxY: 10 }, region);
40
+ expect(region).toHaveLength(0);
41
+
42
+ const point: SpatialObjectId[] = [];
43
+ querySpatialPoint(index, 3, 3, point);
44
+ expect(point).toHaveLength(0);
45
+ });
46
+ });
47
+
48
+ describe('createSpatialIndex', () => {
49
+ it('defaults to a working uniform grid when given no backend', () => {
50
+ const index = createSpatialIndex();
51
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
52
+ insertSpatialObject(index, 2, { minX: 1, minY: 1, maxX: 5, maxY: 5 });
53
+
54
+ const pairs: SpatialPair[] = [];
55
+ querySpatialPairs(index, pairs);
56
+ expect(pairKeys(pairs)).toEqual(['1-2']);
57
+ });
58
+
59
+ it('uses an explicitly supplied backend', () => {
60
+ const index = createSpatialIndex(createUniformGridSpatialBackend(64));
61
+ insertSpatialObject(index, 7, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
62
+
63
+ const point: SpatialObjectId[] = [];
64
+ querySpatialPoint(index, 2, 2, point);
65
+ expect(point).toEqual([7]);
66
+ });
67
+ });
68
+
69
+ describe('insertSpatialObject', () => {
70
+ it('yields exactly the one overlapping pair from two overlapping objects and one far object', () => {
71
+ const index = makeIndex();
72
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
73
+ insertSpatialObject(index, 2, { minX: 2, minY: 2, maxX: 6, maxY: 6 });
74
+ insertSpatialObject(index, 3, { minX: 100, minY: 100, maxX: 104, maxY: 104 });
75
+
76
+ const pairs: SpatialPair[] = [];
77
+ querySpatialPairs(index, pairs);
78
+
79
+ expect(pairs).toHaveLength(1);
80
+ expect(pairKeys(pairs)).toEqual(['1-2']);
81
+ for (const pair of pairs) expect(pair.a).not.toBe(pair.b);
82
+ });
83
+ });
84
+
85
+ describe('querySpatialPairs', () => {
86
+ it('reuses and clears the out array across calls', () => {
87
+ const index = makeIndex();
88
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
89
+ insertSpatialObject(index, 2, { minX: 2, minY: 2, maxX: 6, maxY: 6 });
90
+
91
+ const out: SpatialPair[] = [];
92
+ querySpatialPairs(index, out);
93
+ expect(out).toHaveLength(1);
94
+
95
+ removeSpatialObject(index, 2);
96
+ querySpatialPairs(index, out);
97
+ expect(out).toHaveLength(0);
98
+ });
99
+ });
100
+
101
+ describe('querySpatialPoint', () => {
102
+ it('returns an object at an interior point and nothing at an empty point', () => {
103
+ const index = makeIndex();
104
+ insertSpatialObject(index, 1, { minX: 1, minY: 1, maxX: 3, maxY: 3 });
105
+ insertSpatialObject(index, 2, { minX: 8, minY: 8, maxX: 9, maxY: 9 });
106
+
107
+ const inside: SpatialObjectId[] = [];
108
+ querySpatialPoint(index, 2, 2, inside);
109
+ expect(inside).toEqual([1]);
110
+
111
+ const outside: SpatialObjectId[] = [];
112
+ querySpatialPoint(index, 6, 6, outside);
113
+ expect(outside).toHaveLength(0);
114
+ });
115
+ });
116
+
117
+ describe('querySpatialRay', () => {
118
+ it('returns an object the ray crosses and nothing for a ray that misses', () => {
119
+ const index = makeIndex();
120
+ insertSpatialObject(index, 1, { minX: 20, minY: 20, maxX: 24, maxY: 24 });
121
+
122
+ const hit: SpatialObjectId[] = [];
123
+ querySpatialRay(index, 0, 22, 1, 0, hit);
124
+ expect(hit).toEqual([1]);
125
+
126
+ const miss: SpatialObjectId[] = [];
127
+ querySpatialRay(index, 0, 0, 1, 0, miss);
128
+ expect(miss).toHaveLength(0);
129
+ });
130
+ });
131
+
132
+ describe('querySpatialRegion', () => {
133
+ it('includes overlapping bounds and excludes a cell-mate whose bounds miss the region', () => {
134
+ const index = makeIndex();
135
+ // Both objects share grid cell (0,0), but only object 1 actually overlaps the query region.
136
+ insertSpatialObject(index, 1, { minX: 1, minY: 1, maxX: 3, maxY: 3 });
137
+ insertSpatialObject(index, 2, { minX: 8, minY: 8, maxX: 9, maxY: 9 });
138
+
139
+ const out: SpatialObjectId[] = [];
140
+ querySpatialRegion(index, { minX: 0, minY: 0, maxX: 4, maxY: 4 }, out);
141
+
142
+ expect(out).toEqual([1]);
143
+ });
144
+ });
145
+
146
+ describe('removeSpatialObject', () => {
147
+ it('drops the object from pair, region, and point queries', () => {
148
+ const index = makeIndex();
149
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
150
+ insertSpatialObject(index, 2, { minX: 2, minY: 2, maxX: 6, maxY: 6 });
151
+
152
+ removeSpatialObject(index, 2);
153
+
154
+ const pairs: SpatialPair[] = [];
155
+ querySpatialPairs(index, pairs);
156
+ expect(pairs).toHaveLength(0);
157
+
158
+ const region: SpatialObjectId[] = [];
159
+ querySpatialRegion(index, { minX: 0, minY: 0, maxX: 6, maxY: 6 }, region);
160
+ expect(region).toEqual([1]);
161
+
162
+ const point: SpatialObjectId[] = [];
163
+ querySpatialPoint(index, 5, 5, point);
164
+ expect(point).toHaveLength(0);
165
+ });
166
+ });
167
+
168
+ describe('updateSpatialObject', () => {
169
+ it('removes a pair once the moved object leaves the shared region', () => {
170
+ const index = makeIndex();
171
+ insertSpatialObject(index, 1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
172
+ insertSpatialObject(index, 2, { minX: 2, minY: 2, maxX: 6, maxY: 6 });
173
+
174
+ const before: SpatialPair[] = [];
175
+ querySpatialPairs(index, before);
176
+ expect(before).toHaveLength(1);
177
+
178
+ updateSpatialObject(index, 2, { minX: 200, minY: 200, maxX: 204, maxY: 204 });
179
+
180
+ const after: SpatialPair[] = [];
181
+ querySpatialPairs(index, after);
182
+ expect(after).toHaveLength(0);
183
+ });
184
+ });
@@ -0,0 +1,97 @@
1
+ import type { SpatialAabb, SpatialObjectId, SpatialPair } from '@flighthq/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { createUniformGridSpatialBackend } from './uniformGrid';
5
+
6
+ // A plain AABB-overlap confirmation used to turn broadphase candidate pairs into confirmed pairs (the
7
+ // narrow-phase stand-in): exactly the check the caller would apply downstream.
8
+ function boundsOverlap(a: Readonly<SpatialAabb>, b: Readonly<SpatialAabb>): boolean {
9
+ return a.minX < b.maxX && a.maxX > b.minX && a.minY < b.maxY && a.maxY > b.minY;
10
+ }
11
+
12
+ function pairKeys(pairs: readonly SpatialPair[]): string[] {
13
+ return pairs.map((p) => `${Math.min(p.a, p.b)}-${Math.max(p.a, p.b)}`).sort();
14
+ }
15
+
16
+ describe('createUniformGridSpatialBackend', () => {
17
+ it('emits a pair spanning several shared cells exactly once', () => {
18
+ const grid = createUniformGridSpatialBackend(10);
19
+ // Both objects cover the same 3x3 block of cells (0..2, 0..2), so they co-occupy nine cells.
20
+ grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 25, maxY: 25 });
21
+ grid.insertSpatialObject(2, { minX: 5, minY: 5, maxX: 29, maxY: 29 });
22
+
23
+ const pairs: SpatialPair[] = [];
24
+ grid.querySpatialPairs(pairs);
25
+
26
+ expect(pairs).toHaveLength(1);
27
+ expect(pairKeys(pairs)).toEqual(['1-2']);
28
+ });
29
+
30
+ it('never pairs an object with itself', () => {
31
+ const grid = createUniformGridSpatialBackend(10);
32
+ grid.insertSpatialObject(1, { minX: 0, minY: 0, maxX: 4, maxY: 4 });
33
+
34
+ const pairs: SpatialPair[] = [];
35
+ grid.querySpatialPairs(pairs);
36
+ expect(pairs).toHaveLength(0);
37
+ });
38
+
39
+ it('returns nothing from every query on an empty grid', () => {
40
+ const grid = createUniformGridSpatialBackend(10);
41
+
42
+ const pairs: SpatialPair[] = [];
43
+ grid.querySpatialPairs(pairs);
44
+ expect(pairs).toHaveLength(0);
45
+
46
+ const region: SpatialObjectId[] = [];
47
+ grid.querySpatialRegion({ minX: -50, minY: -50, maxX: 50, maxY: 50 }, region);
48
+ expect(region).toHaveLength(0);
49
+
50
+ const point: SpatialObjectId[] = [];
51
+ grid.querySpatialPoint(0, 0, point);
52
+ expect(point).toHaveLength(0);
53
+
54
+ const ray: SpatialObjectId[] = [];
55
+ grid.querySpatialRay(0, 0, 1, 1, ray);
56
+ expect(ray).toHaveLength(0);
57
+ });
58
+
59
+ it('indexes objects at negative coordinates', () => {
60
+ const grid = createUniformGridSpatialBackend(10);
61
+ grid.insertSpatialObject(1, { minX: -8, minY: -8, maxX: -4, maxY: -4 });
62
+ grid.insertSpatialObject(2, { minX: -6, minY: -6, maxX: -2, maxY: -2 });
63
+
64
+ const pairs: SpatialPair[] = [];
65
+ grid.querySpatialPairs(pairs);
66
+ expect(pairKeys(pairs)).toEqual(['1-2']);
67
+
68
+ const point: SpatialObjectId[] = [];
69
+ grid.querySpatialPoint(-5, -5, point);
70
+ expect(point.sort()).toEqual([1, 2]);
71
+ });
72
+
73
+ it('gives the same confirmed pairs across different cell sizes', () => {
74
+ const objects: { id: SpatialObjectId; bounds: SpatialAabb }[] = [
75
+ { id: 1, bounds: { minX: 0, minY: 0, maxX: 10, maxY: 10 } },
76
+ { id: 2, bounds: { minX: 5, minY: 5, maxX: 15, maxY: 15 } },
77
+ { id: 3, bounds: { minX: 12, minY: 12, maxX: 20, maxY: 20 } },
78
+ { id: 4, bounds: { minX: 100, minY: 100, maxX: 110, maxY: 110 } },
79
+ ];
80
+ const boundsOf = new Map(objects.map((o) => [o.id, o.bounds]));
81
+
82
+ function confirmedPairs(cellSize: number): string[] {
83
+ const grid = createUniformGridSpatialBackend(cellSize);
84
+ for (const o of objects) grid.insertSpatialObject(o.id, o.bounds);
85
+ const candidates: SpatialPair[] = [];
86
+ grid.querySpatialPairs(candidates);
87
+ const confirmed = candidates.filter((p) => boundsOverlap(boundsOf.get(p.a)!, boundsOf.get(p.b)!));
88
+ return pairKeys(confirmed);
89
+ }
90
+
91
+ const fine = confirmedPairs(10);
92
+ const coarse = confirmedPairs(64);
93
+
94
+ expect(fine).toEqual(['1-2', '2-3']);
95
+ expect(coarse).toEqual(fine);
96
+ });
97
+ });