@spatial-engine/core 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francisco Rueda Esquivel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,586 @@
1
+ // src/octree-node.ts
2
+ var NODE_AABB_OFFSET = 0;
3
+ var NODE_FIRST_CHILD_OFFSET = 6;
4
+ var NODE_PARENT_OFFSET = 7;
5
+ var NODE_OBJECT_COUNT_OFFSET = 8;
6
+ var NODE_OBJECTS_OFFSET = 9;
7
+ var MAX_OBJECTS_PER_NODE = 8;
8
+ var NODE_STRIDE = NODE_OBJECTS_OFFSET + MAX_OBJECTS_PER_NODE;
9
+ var OctreeNodePool = class _OctreeNodePool {
10
+ buffer;
11
+ count = 0;
12
+ constructor(capacity, sharedBuffer) {
13
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * NODE_STRIDE) : new Float32Array(capacity * NODE_STRIDE);
14
+ }
15
+ /**
16
+ * Create an OctreeNodePool backed by a new `SharedArrayBuffer`.
17
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
18
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
19
+ */
20
+ static createShared(capacity) {
21
+ const sab = new SharedArrayBuffer(capacity * NODE_STRIDE * Float32Array.BYTES_PER_ELEMENT);
22
+ return { pool: new _OctreeNodePool(capacity, sab), sab };
23
+ }
24
+ /** Allocate a new node slot, initialise sentinel values, and return its index. */
25
+ allocate() {
26
+ const index = this.count;
27
+ const offset = index * NODE_STRIDE;
28
+ this.buffer[offset + NODE_FIRST_CHILD_OFFSET] = -1;
29
+ this.buffer[offset + NODE_PARENT_OFFSET] = -1;
30
+ this.buffer[offset + NODE_OBJECT_COUNT_OFFSET] = 0;
31
+ this.count += 1;
32
+ return index;
33
+ }
34
+ /** Returns the number of allocated nodes. */
35
+ get size() {
36
+ return this.count;
37
+ }
38
+ /** Reset the pool (all allocations freed, no GC). */
39
+ reset() {
40
+ this.count = 0;
41
+ }
42
+ // ── AABB ──────────────────────────────────────────────────────────────────
43
+ /** Set the AABB bounds for the node at the given index. */
44
+ setAABB(index, minX, minY, minZ, maxX, maxY, maxZ) {
45
+ const off = index * NODE_STRIDE + NODE_AABB_OFFSET;
46
+ this.buffer[off] = minX;
47
+ this.buffer[off + 1] = minY;
48
+ this.buffer[off + 2] = minZ;
49
+ this.buffer[off + 3] = maxX;
50
+ this.buffer[off + 4] = maxY;
51
+ this.buffer[off + 5] = maxZ;
52
+ }
53
+ /** Read a single AABB component (0–5) from the node at the given index. */
54
+ getAABB(index, component) {
55
+ return this.buffer[index * NODE_STRIDE + NODE_AABB_OFFSET + component] ?? 0;
56
+ }
57
+ // ── First-child link ──────────────────────────────────────────────────────
58
+ /** Set the first-child index for the node (-1 = leaf). */
59
+ setFirstChild(index, childIndex) {
60
+ this.buffer[index * NODE_STRIDE + NODE_FIRST_CHILD_OFFSET] = childIndex;
61
+ }
62
+ /** Get the first-child index for the node (-1 = leaf). */
63
+ getFirstChild(index) {
64
+ return this.buffer[index * NODE_STRIDE + NODE_FIRST_CHILD_OFFSET] ?? -1;
65
+ }
66
+ // ── Parent link ───────────────────────────────────────────────────────────
67
+ /** Set the parent index for the node (-1 = root). */
68
+ setParent(index, parentIndex) {
69
+ this.buffer[index * NODE_STRIDE + NODE_PARENT_OFFSET] = parentIndex;
70
+ }
71
+ /** Get the parent index for the node (-1 = root). */
72
+ getParent(index) {
73
+ return this.buffer[index * NODE_STRIDE + NODE_PARENT_OFFSET] ?? -1;
74
+ }
75
+ // ── Object pointers ───────────────────────────────────────────────────────
76
+ /** Return the number of objects stored in the node at the given index. */
77
+ getObjectCount(index) {
78
+ return this.buffer[index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET] ?? 0;
79
+ }
80
+ /**
81
+ * Append an object index to the node's object list.
82
+ * Throws a RangeError when MAX_OBJECTS_PER_NODE is exceeded.
83
+ */
84
+ addObject(index, objectIndex) {
85
+ const countOff = index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET;
86
+ const count = this.buffer[countOff] ?? 0;
87
+ if (count >= MAX_OBJECTS_PER_NODE) {
88
+ throw new RangeError(
89
+ `OctreeNodePool.addObject: node ${index} already contains MAX_OBJECTS_PER_NODE (${MAX_OBJECTS_PER_NODE}) objects`
90
+ );
91
+ }
92
+ this.buffer[index * NODE_STRIDE + NODE_OBJECTS_OFFSET + count] = objectIndex;
93
+ this.buffer[countOff] = count + 1;
94
+ }
95
+ /** Return the object index stored at slot `slot` in the given node. */
96
+ getObject(index, slot) {
97
+ return this.buffer[index * NODE_STRIDE + NODE_OBJECTS_OFFSET + slot] ?? 0;
98
+ }
99
+ /** Reset the object count of the given node to zero (does not zero the slots). */
100
+ clearObjects(index) {
101
+ this.buffer[index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET] = 0;
102
+ }
103
+ /**
104
+ * Remove a single object index from the node's object list.
105
+ * Uses swap-with-last to avoid shifting. Returns true if found and removed.
106
+ */
107
+ removeObject(index, objectIndex) {
108
+ const countOff = index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET;
109
+ const count = this.buffer[countOff] ?? 0;
110
+ const baseOff = index * NODE_STRIDE + NODE_OBJECTS_OFFSET;
111
+ for (let i = 0; i < count; i++) {
112
+ if (this.buffer[baseOff + i] === objectIndex) {
113
+ this.buffer[baseOff + i] = this.buffer[baseOff + count - 1] ?? 0;
114
+ this.buffer[countOff] = count - 1;
115
+ return true;
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ };
121
+
122
+ // src/aabb.ts
123
+ var AABB_STRIDE = 6;
124
+ var AABBPool = class _AABBPool {
125
+ buffer;
126
+ count = 0;
127
+ constructor(capacity, sharedBuffer) {
128
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * AABB_STRIDE) : new Float32Array(capacity * AABB_STRIDE);
129
+ }
130
+ /**
131
+ * Create an AABBPool backed by a new `SharedArrayBuffer`.
132
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
133
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
134
+ */
135
+ static createShared(capacity) {
136
+ const sab = new SharedArrayBuffer(capacity * AABB_STRIDE * Float32Array.BYTES_PER_ELEMENT);
137
+ return { pool: new _AABBPool(capacity, sab), sab };
138
+ }
139
+ /** Allocate a new AABB slot and return its index. */
140
+ allocate() {
141
+ const index = this.count;
142
+ this.count += 1;
143
+ return index;
144
+ }
145
+ /** Set the bounds of an AABB at the given index. */
146
+ set(index, minX, minY, minZ, maxX, maxY, maxZ) {
147
+ const offset = index * AABB_STRIDE;
148
+ this.buffer[offset] = minX;
149
+ this.buffer[offset + 1] = minY;
150
+ this.buffer[offset + 2] = minZ;
151
+ this.buffer[offset + 3] = maxX;
152
+ this.buffer[offset + 4] = maxY;
153
+ this.buffer[offset + 5] = maxZ;
154
+ }
155
+ /** Read a single component value from the buffer. */
156
+ get(index, component) {
157
+ return this.buffer[index * AABB_STRIDE + component] ?? 0;
158
+ }
159
+ /** Returns the number of allocated AABBs. */
160
+ get size() {
161
+ return this.count;
162
+ }
163
+ /** Reset the pool (all allocations freed, no GC). */
164
+ reset() {
165
+ this.count = 0;
166
+ }
167
+ };
168
+ function aabbExpand(pool, a, b) {
169
+ const buf = pool.buffer;
170
+ const aOff = a * AABB_STRIDE;
171
+ const bOff = b * AABB_STRIDE;
172
+ buf[aOff] = Math.min(buf[aOff] ?? 0, buf[bOff] ?? 0);
173
+ buf[aOff + 1] = Math.min(buf[aOff + 1] ?? 0, buf[bOff + 1] ?? 0);
174
+ buf[aOff + 2] = Math.min(buf[aOff + 2] ?? 0, buf[bOff + 2] ?? 0);
175
+ buf[aOff + 3] = Math.max(buf[aOff + 3] ?? 0, buf[bOff + 3] ?? 0);
176
+ buf[aOff + 4] = Math.max(buf[aOff + 4] ?? 0, buf[bOff + 4] ?? 0);
177
+ buf[aOff + 5] = Math.max(buf[aOff + 5] ?? 0, buf[bOff + 5] ?? 0);
178
+ }
179
+ function aabbMerge(pool, dest, a, b) {
180
+ const buf = pool.buffer;
181
+ const aOff = a * AABB_STRIDE;
182
+ const bOff = b * AABB_STRIDE;
183
+ const dOff = dest * AABB_STRIDE;
184
+ buf[dOff] = Math.min(buf[aOff] ?? 0, buf[bOff] ?? 0);
185
+ buf[dOff + 1] = Math.min(buf[aOff + 1] ?? 0, buf[bOff + 1] ?? 0);
186
+ buf[dOff + 2] = Math.min(buf[aOff + 2] ?? 0, buf[bOff + 2] ?? 0);
187
+ buf[dOff + 3] = Math.max(buf[aOff + 3] ?? 0, buf[bOff + 3] ?? 0);
188
+ buf[dOff + 4] = Math.max(buf[aOff + 4] ?? 0, buf[bOff + 4] ?? 0);
189
+ buf[dOff + 5] = Math.max(buf[aOff + 5] ?? 0, buf[bOff + 5] ?? 0);
190
+ }
191
+ function aabbIntersects(pool, a, b) {
192
+ const aOff = a * AABB_STRIDE;
193
+ const bOff = b * AABB_STRIDE;
194
+ const buf = pool.buffer;
195
+ return (buf[aOff] ?? 0) <= (buf[bOff + 3] ?? 0) && (buf[aOff + 3] ?? 0) >= (buf[bOff] ?? 0) && (buf[aOff + 1] ?? 0) <= (buf[bOff + 4] ?? 0) && (buf[aOff + 4] ?? 0) >= (buf[bOff + 1] ?? 0) && (buf[aOff + 2] ?? 0) <= (buf[bOff + 5] ?? 0) && (buf[aOff + 5] ?? 0) >= (buf[bOff + 2] ?? 0);
196
+ }
197
+
198
+ // src/ray.ts
199
+ var RAY_STRIDE = 6;
200
+ var RayPool = class _RayPool {
201
+ buffer;
202
+ count = 0;
203
+ constructor(capacity, sharedBuffer) {
204
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * RAY_STRIDE) : new Float32Array(capacity * RAY_STRIDE);
205
+ }
206
+ /**
207
+ * Create a RayPool backed by a new `SharedArrayBuffer`.
208
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
209
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
210
+ */
211
+ static createShared(capacity) {
212
+ const sab = new SharedArrayBuffer(capacity * RAY_STRIDE * Float32Array.BYTES_PER_ELEMENT);
213
+ return { pool: new _RayPool(capacity, sab), sab };
214
+ }
215
+ /** Allocate a new Ray slot and return its index. */
216
+ allocate() {
217
+ const index = this.count;
218
+ this.count += 1;
219
+ return index;
220
+ }
221
+ /** Set origin and direction of a ray at the given index. */
222
+ set(index, ox, oy, oz, dx, dy, dz) {
223
+ const offset = index * RAY_STRIDE;
224
+ this.buffer[offset] = ox;
225
+ this.buffer[offset + 1] = oy;
226
+ this.buffer[offset + 2] = oz;
227
+ this.buffer[offset + 3] = dx;
228
+ this.buffer[offset + 4] = dy;
229
+ this.buffer[offset + 5] = dz;
230
+ }
231
+ /** Read a single component value from the buffer. */
232
+ get(index, component) {
233
+ return this.buffer[index * RAY_STRIDE + component] ?? 0;
234
+ }
235
+ /** Returns the number of allocated Rays. */
236
+ get size() {
237
+ return this.count;
238
+ }
239
+ /** Reset the pool (all allocations freed, no GC). */
240
+ reset() {
241
+ this.count = 0;
242
+ }
243
+ };
244
+ function rayIntersectsAABB(rayBuf, rayOffset, aabbBuf, aabbOffset) {
245
+ const ox = rayBuf[rayOffset] ?? 0;
246
+ const oy = rayBuf[rayOffset + 1] ?? 0;
247
+ const oz = rayBuf[rayOffset + 2] ?? 0;
248
+ const idx = 1 / (rayBuf[rayOffset + 3] ?? 0);
249
+ const idy = 1 / (rayBuf[rayOffset + 4] ?? 0);
250
+ const idz = 1 / (rayBuf[rayOffset + 5] ?? 0);
251
+ const minX = aabbBuf[aabbOffset] ?? 0;
252
+ const minY = aabbBuf[aabbOffset + 1] ?? 0;
253
+ const minZ = aabbBuf[aabbOffset + 2] ?? 0;
254
+ const maxX = aabbBuf[aabbOffset + 3] ?? 0;
255
+ const maxY = aabbBuf[aabbOffset + 4] ?? 0;
256
+ const maxZ = aabbBuf[aabbOffset + 5] ?? 0;
257
+ const t1x = (minX - ox) * idx;
258
+ const t2x = (maxX - ox) * idx;
259
+ const t1y = (minY - oy) * idy;
260
+ const t2y = (maxY - oy) * idy;
261
+ const t1z = (minZ - oz) * idz;
262
+ const t2z = (maxZ - oz) * idz;
263
+ const tmin = Math.max(Math.min(t1x, t2x), Math.min(t1y, t2y), Math.min(t1z, t2z));
264
+ const tmax = Math.min(Math.max(t1x, t2x), Math.max(t1y, t2y), Math.max(t1z, t2z));
265
+ if (tmax < 0 || !(tmin <= tmax)) return -1;
266
+ return tmin >= 0 ? tmin : tmax;
267
+ }
268
+
269
+ // src/octree.ts
270
+ function aabbOverlapsBox(buf, bufOffset, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ) {
271
+ return buf[bufOffset] <= qMaxX && buf[bufOffset + 3] >= qMinX && buf[bufOffset + 1] <= qMaxY && buf[bufOffset + 4] >= qMinY && buf[bufOffset + 2] <= qMaxZ && buf[bufOffset + 5] >= qMinZ;
272
+ }
273
+ var Octree = class {
274
+ nodePool;
275
+ aabbPool;
276
+ root;
277
+ /** Tracks which node each object (by AABBPool index) is currently stored in. */
278
+ objectNodeMap = /* @__PURE__ */ new Map();
279
+ /** Pre-allocated traversal stack reused across raycast calls to avoid GC pressure. */
280
+ _stack = [];
281
+ constructor(nodePool, aabbPool) {
282
+ this.nodePool = nodePool;
283
+ this.aabbPool = aabbPool;
284
+ this.root = nodePool.allocate();
285
+ }
286
+ /** The index of the root node in the underlying OctreeNodePool. */
287
+ get rootIndex() {
288
+ return this.root;
289
+ }
290
+ /** Set the world-space AABB that the root node covers. */
291
+ setBounds(minX, minY, minZ, maxX, maxY, maxZ) {
292
+ this.nodePool.setAABB(this.root, minX, minY, minZ, maxX, maxY, maxZ);
293
+ }
294
+ /** Insert the AABB at `objectIndex` (in the AABBPool) into the tree. */
295
+ insert(objectIndex) {
296
+ this.insertIntoNode(this.root, objectIndex);
297
+ }
298
+ /**
299
+ * Update the AABB at `objectIndex` with new bounds and reposition it in the
300
+ * tree without rebuilding. If the new bounds still fit in the current node
301
+ * the object stays there; otherwise it is removed and re-inserted from the
302
+ * lowest ancestor whose bounds fully contain the new AABB.
303
+ */
304
+ update(objectIndex, newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ) {
305
+ const np = this.nodePool;
306
+ const ap = this.aabbPool;
307
+ ap.set(objectIndex, newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ);
308
+ const currentNode = this.objectNodeMap.get(objectIndex);
309
+ if (currentNode === void 0) return;
310
+ if (this.fitsInNode(objectIndex, currentNode)) return;
311
+ np.removeObject(currentNode, objectIndex);
312
+ this.objectNodeMap.delete(objectIndex);
313
+ let ancestorNode = np.getParent(currentNode);
314
+ while (ancestorNode !== -1 && !this.fitsInNode(objectIndex, ancestorNode)) {
315
+ ancestorNode = np.getParent(ancestorNode);
316
+ }
317
+ if (ancestorNode === -1) ancestorNode = this.root;
318
+ this.insertIntoNode(ancestorNode, objectIndex);
319
+ }
320
+ /**
321
+ * Cast a ray through the octree and return the closest intersecting object.
322
+ *
323
+ * Uses an iterative stack traversal (pre-allocated, no recursion) to avoid
324
+ * GC pressure. Only descends into child nodes whose AABBs are intersected by
325
+ * the ray, providing efficient pruning of non-intersecting subtrees.
326
+ *
327
+ * @param rayBuf Float32Array containing the ray data [ox,oy,oz,dx,dy,dz].
328
+ * @param rayOffset Element offset (in floats) within `rayBuf` to the ray's origin.
329
+ * @returns The closest `{ objectIndex, t }` hit, or `null` if nothing is hit.
330
+ */
331
+ raycast(rayBuf, rayOffset) {
332
+ const np = this.nodePool;
333
+ const npBuf = np.buffer;
334
+ const apBuf = this.aabbPool.buffer;
335
+ if (rayIntersectsAABB(rayBuf, rayOffset, npBuf, this.root * NODE_STRIDE + NODE_AABB_OFFSET) < 0) {
336
+ return null;
337
+ }
338
+ this._stack.length = 0;
339
+ this._stack.push(this.root);
340
+ let closestT = Infinity;
341
+ let closestIndex = -1;
342
+ while (this._stack.length > 0) {
343
+ const nodeIdx = this._stack.pop();
344
+ const objCount = np.getObjectCount(nodeIdx);
345
+ for (let i = 0; i < objCount; i++) {
346
+ const objIdx = np.getObject(nodeIdx, i);
347
+ const t = rayIntersectsAABB(rayBuf, rayOffset, apBuf, objIdx * AABB_STRIDE);
348
+ if (t >= 0 && t < closestT) {
349
+ closestT = t;
350
+ closestIndex = objIdx;
351
+ }
352
+ }
353
+ const firstChild = np.getFirstChild(nodeIdx);
354
+ if (firstChild !== -1) {
355
+ for (let i = 0; i < 8; i++) {
356
+ const childIdx = firstChild + i;
357
+ if (rayIntersectsAABB(rayBuf, rayOffset, npBuf, childIdx * NODE_STRIDE + NODE_AABB_OFFSET) >= 0) {
358
+ this._stack.push(childIdx);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ if (closestIndex === -1) return null;
364
+ return { objectIndex: closestIndex, t: closestT };
365
+ }
366
+ /**
367
+ * Query the octree for all objects whose AABB overlaps the given axis-aligned
368
+ * box region and return their indices (in the AABBPool).
369
+ *
370
+ * Uses the same pre-allocated iterative stack as `raycast` to avoid GC
371
+ * pressure. Descends only into child nodes whose AABBs overlap the query box,
372
+ * pruning non-intersecting subtrees.
373
+ *
374
+ * @param minX Minimum X of the query box.
375
+ * @param minY Minimum Y of the query box.
376
+ * @param minZ Minimum Z of the query box.
377
+ * @param maxX Maximum X of the query box.
378
+ * @param maxY Maximum Y of the query box.
379
+ * @param maxZ Maximum Z of the query box.
380
+ * @returns Array of AABBPool indices for every object that overlaps the box.
381
+ */
382
+ queryBox(minX, minY, minZ, maxX, maxY, maxZ) {
383
+ const np = this.nodePool;
384
+ const npBuf = np.buffer;
385
+ const apBuf = this.aabbPool.buffer;
386
+ const results = [];
387
+ if (!aabbOverlapsBox(npBuf, this.root * NODE_STRIDE + NODE_AABB_OFFSET, minX, minY, minZ, maxX, maxY, maxZ)) {
388
+ return results;
389
+ }
390
+ this._stack.length = 0;
391
+ this._stack.push(this.root);
392
+ while (this._stack.length > 0) {
393
+ const nodeIdx = this._stack.pop();
394
+ const objCount = np.getObjectCount(nodeIdx);
395
+ for (let i = 0; i < objCount; i++) {
396
+ const objIdx = np.getObject(nodeIdx, i);
397
+ if (aabbOverlapsBox(apBuf, objIdx * AABB_STRIDE, minX, minY, minZ, maxX, maxY, maxZ)) {
398
+ results.push(objIdx);
399
+ }
400
+ }
401
+ const firstChild = np.getFirstChild(nodeIdx);
402
+ if (firstChild !== -1) {
403
+ for (let i = 0; i < 8; i++) {
404
+ const childIdx = firstChild + i;
405
+ if (aabbOverlapsBox(npBuf, childIdx * NODE_STRIDE + NODE_AABB_OFFSET, minX, minY, minZ, maxX, maxY, maxZ)) {
406
+ this._stack.push(childIdx);
407
+ }
408
+ }
409
+ }
410
+ }
411
+ return results;
412
+ }
413
+ // ── Private helpers ──────────────────────────────────────────────────────
414
+ insertIntoNode(nodeIdx, objectIndex) {
415
+ const np = this.nodePool;
416
+ const firstChild = np.getFirstChild(nodeIdx);
417
+ if (firstChild !== -1) {
418
+ for (let i = 0; i < 8; i++) {
419
+ if (this.fitsInNode(objectIndex, firstChild + i)) {
420
+ this.insertIntoNode(firstChild + i, objectIndex);
421
+ return;
422
+ }
423
+ }
424
+ np.addObject(nodeIdx, objectIndex);
425
+ this.objectNodeMap.set(objectIndex, nodeIdx);
426
+ return;
427
+ }
428
+ const count = np.getObjectCount(nodeIdx);
429
+ if (count < MAX_OBJECTS_PER_NODE) {
430
+ np.addObject(nodeIdx, objectIndex);
431
+ this.objectNodeMap.set(objectIndex, nodeIdx);
432
+ } else {
433
+ this.subdivide(nodeIdx);
434
+ this.insertIntoNode(nodeIdx, objectIndex);
435
+ }
436
+ }
437
+ /** Split a leaf node into 8 children and redistribute its objects. */
438
+ subdivide(nodeIdx) {
439
+ const np = this.nodePool;
440
+ const minX = np.getAABB(nodeIdx, 0);
441
+ const minY = np.getAABB(nodeIdx, 1);
442
+ const minZ = np.getAABB(nodeIdx, 2);
443
+ const maxX = np.getAABB(nodeIdx, 3);
444
+ const maxY = np.getAABB(nodeIdx, 4);
445
+ const maxZ = np.getAABB(nodeIdx, 5);
446
+ const midX = (minX + maxX) / 2;
447
+ const midY = (minY + maxY) / 2;
448
+ const midZ = (minZ + maxZ) / 2;
449
+ const firstChild = np.allocate();
450
+ for (let i = 1; i < 8; i++) {
451
+ np.allocate();
452
+ }
453
+ np.setFirstChild(nodeIdx, firstChild);
454
+ for (let i = 0; i < 8; i++) {
455
+ const childIdx = firstChild + i;
456
+ const cMinX = (i & 1) === 0 ? minX : midX;
457
+ const cMaxX = (i & 1) === 0 ? midX : maxX;
458
+ const cMinY = (i & 2) === 0 ? minY : midY;
459
+ const cMaxY = (i & 2) === 0 ? midY : maxY;
460
+ const cMinZ = (i & 4) === 0 ? minZ : midZ;
461
+ const cMaxZ = (i & 4) === 0 ? midZ : maxZ;
462
+ np.setAABB(childIdx, cMinX, cMinY, cMinZ, cMaxX, cMaxY, cMaxZ);
463
+ np.setParent(childIdx, nodeIdx);
464
+ }
465
+ const count = np.getObjectCount(nodeIdx);
466
+ const saved = [];
467
+ for (let i = 0; i < count; i++) {
468
+ saved.push(np.getObject(nodeIdx, i));
469
+ }
470
+ np.clearObjects(nodeIdx);
471
+ for (const obj of saved) {
472
+ this.objectNodeMap.delete(obj);
473
+ this.insertIntoNode(nodeIdx, obj);
474
+ }
475
+ }
476
+ /** Returns true when the AABB at `objectIndex` is fully contained by `nodeIdx`. */
477
+ fitsInNode(objectIndex, nodeIdx) {
478
+ const np = this.nodePool;
479
+ const ap = this.aabbPool;
480
+ return ap.get(objectIndex, 0) >= np.getAABB(nodeIdx, 0) && ap.get(objectIndex, 1) >= np.getAABB(nodeIdx, 1) && ap.get(objectIndex, 2) >= np.getAABB(nodeIdx, 2) && ap.get(objectIndex, 3) <= np.getAABB(nodeIdx, 3) && ap.get(objectIndex, 4) <= np.getAABB(nodeIdx, 4) && ap.get(objectIndex, 5) <= np.getAABB(nodeIdx, 5);
481
+ }
482
+ };
483
+
484
+ // src/lidar-worker.ts
485
+ function createLidarProcessor() {
486
+ let octree = null;
487
+ let aabbPool = null;
488
+ let raysBuf = null;
489
+ let resultsBuf = null;
490
+ let totalRayCount = 0;
491
+ let insertedObjectCount = 0;
492
+ return {
493
+ /** Initialise shared buffers and create the Octree. */
494
+ init(msg) {
495
+ const nodePool = new OctreeNodePool(msg.nodeCapacity, msg.nodesSab);
496
+ aabbPool = new AABBPool(msg.objectCapacity, msg.aabbsSab);
497
+ raysBuf = new Float32Array(msg.raysSab);
498
+ resultsBuf = new Float32Array(msg.resultsSab);
499
+ totalRayCount = msg.rayCount;
500
+ insertedObjectCount = 0;
501
+ octree = new Octree(nodePool, aabbPool);
502
+ octree.setBounds(
503
+ msg.worldMinX,
504
+ msg.worldMinY,
505
+ msg.worldMinZ,
506
+ msg.worldMaxX,
507
+ msg.worldMaxY,
508
+ msg.worldMaxZ
509
+ );
510
+ return { type: "ready" };
511
+ },
512
+ /**
513
+ * Read current object AABB data from the shared buffer, update the Octree,
514
+ * cast every sweep ray, and write results to the results buffer.
515
+ */
516
+ sweep(msg) {
517
+ if (octree === null || aabbPool === null || raysBuf === null || resultsBuf === null) {
518
+ throw new Error("LidarProcessor: sweep called before init");
519
+ }
520
+ const objectCount = msg.objectCount;
521
+ while (aabbPool.size < objectCount) {
522
+ aabbPool.allocate();
523
+ }
524
+ for (let i = 0; i < objectCount; i++) {
525
+ const minX = aabbPool.get(i, 0);
526
+ const minY = aabbPool.get(i, 1);
527
+ const minZ = aabbPool.get(i, 2);
528
+ const maxX = aabbPool.get(i, 3);
529
+ const maxY = aabbPool.get(i, 4);
530
+ const maxZ = aabbPool.get(i, 5);
531
+ if (i < insertedObjectCount) {
532
+ octree.update(i, minX, minY, minZ, maxX, maxY, maxZ);
533
+ } else {
534
+ octree.insert(i);
535
+ insertedObjectCount++;
536
+ }
537
+ }
538
+ for (let r = 0; r < totalRayCount; r++) {
539
+ const hit = octree.raycast(raysBuf, r * RAY_STRIDE);
540
+ if (hit !== null) {
541
+ resultsBuf[r * 2] = hit.objectIndex;
542
+ resultsBuf[r * 2 + 1] = hit.t;
543
+ } else {
544
+ resultsBuf[r * 2] = -1;
545
+ resultsBuf[r * 2 + 1] = -1;
546
+ }
547
+ }
548
+ return { type: "done", rayCount: totalRayCount };
549
+ }
550
+ };
551
+ }
552
+ var globalScope = globalThis;
553
+ if (typeof globalScope["postMessage"] === "function" && typeof globalScope["onmessage"] !== "undefined") {
554
+ const workerSelf = globalThis;
555
+ const processor = createLidarProcessor();
556
+ workerSelf.onmessage = (event) => {
557
+ const msg = event.data;
558
+ if (msg.type === "init") {
559
+ workerSelf.postMessage(processor.init(msg));
560
+ } else if (msg.type === "sweep") {
561
+ workerSelf.postMessage(processor.sweep(msg));
562
+ }
563
+ };
564
+ }
565
+
566
+ export {
567
+ AABB_STRIDE,
568
+ AABBPool,
569
+ aabbExpand,
570
+ aabbMerge,
571
+ aabbIntersects,
572
+ RAY_STRIDE,
573
+ RayPool,
574
+ rayIntersectsAABB,
575
+ NODE_AABB_OFFSET,
576
+ NODE_FIRST_CHILD_OFFSET,
577
+ NODE_PARENT_OFFSET,
578
+ NODE_OBJECT_COUNT_OFFSET,
579
+ NODE_OBJECTS_OFFSET,
580
+ MAX_OBJECTS_PER_NODE,
581
+ NODE_STRIDE,
582
+ OctreeNodePool,
583
+ Octree,
584
+ createLidarProcessor
585
+ };
586
+ //# sourceMappingURL=chunk-JF4RVZK3.js.map