@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/dist/index.cjs ADDED
@@ -0,0 +1,709 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AABBPool: () => AABBPool,
24
+ AABB_STRIDE: () => AABB_STRIDE,
25
+ MAX_OBJECTS_PER_NODE: () => MAX_OBJECTS_PER_NODE,
26
+ NODE_AABB_OFFSET: () => NODE_AABB_OFFSET,
27
+ NODE_FIRST_CHILD_OFFSET: () => NODE_FIRST_CHILD_OFFSET,
28
+ NODE_OBJECTS_OFFSET: () => NODE_OBJECTS_OFFSET,
29
+ NODE_OBJECT_COUNT_OFFSET: () => NODE_OBJECT_COUNT_OFFSET,
30
+ NODE_PARENT_OFFSET: () => NODE_PARENT_OFFSET,
31
+ NODE_STRIDE: () => NODE_STRIDE,
32
+ ObjectPool: () => ObjectPool,
33
+ Octree: () => Octree,
34
+ OctreeNodePool: () => OctreeNodePool,
35
+ RAY_STRIDE: () => RAY_STRIDE,
36
+ RayPool: () => RayPool,
37
+ aabbExpand: () => aabbExpand,
38
+ aabbIntersects: () => aabbIntersects,
39
+ aabbMerge: () => aabbMerge,
40
+ createLidarProcessor: () => createLidarProcessor,
41
+ rayIntersectsAABB: () => rayIntersectsAABB,
42
+ vec3Cross: () => vec3Cross,
43
+ vec3Distance: () => vec3Distance,
44
+ vec3DistanceSq: () => vec3DistanceSq,
45
+ vec3Dot: () => vec3Dot
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/aabb.ts
50
+ var AABB_STRIDE = 6;
51
+ var AABBPool = class _AABBPool {
52
+ buffer;
53
+ count = 0;
54
+ constructor(capacity, sharedBuffer) {
55
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * AABB_STRIDE) : new Float32Array(capacity * AABB_STRIDE);
56
+ }
57
+ /**
58
+ * Create an AABBPool backed by a new `SharedArrayBuffer`.
59
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
60
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
61
+ */
62
+ static createShared(capacity) {
63
+ const sab = new SharedArrayBuffer(capacity * AABB_STRIDE * Float32Array.BYTES_PER_ELEMENT);
64
+ return { pool: new _AABBPool(capacity, sab), sab };
65
+ }
66
+ /** Allocate a new AABB slot and return its index. */
67
+ allocate() {
68
+ const index = this.count;
69
+ this.count += 1;
70
+ return index;
71
+ }
72
+ /** Set the bounds of an AABB at the given index. */
73
+ set(index, minX, minY, minZ, maxX, maxY, maxZ) {
74
+ const offset = index * AABB_STRIDE;
75
+ this.buffer[offset] = minX;
76
+ this.buffer[offset + 1] = minY;
77
+ this.buffer[offset + 2] = minZ;
78
+ this.buffer[offset + 3] = maxX;
79
+ this.buffer[offset + 4] = maxY;
80
+ this.buffer[offset + 5] = maxZ;
81
+ }
82
+ /** Read a single component value from the buffer. */
83
+ get(index, component) {
84
+ return this.buffer[index * AABB_STRIDE + component] ?? 0;
85
+ }
86
+ /** Returns the number of allocated AABBs. */
87
+ get size() {
88
+ return this.count;
89
+ }
90
+ /** Reset the pool (all allocations freed, no GC). */
91
+ reset() {
92
+ this.count = 0;
93
+ }
94
+ };
95
+ function aabbExpand(pool, a, b) {
96
+ const buf = pool.buffer;
97
+ const aOff = a * AABB_STRIDE;
98
+ const bOff = b * AABB_STRIDE;
99
+ buf[aOff] = Math.min(buf[aOff] ?? 0, buf[bOff] ?? 0);
100
+ buf[aOff + 1] = Math.min(buf[aOff + 1] ?? 0, buf[bOff + 1] ?? 0);
101
+ buf[aOff + 2] = Math.min(buf[aOff + 2] ?? 0, buf[bOff + 2] ?? 0);
102
+ buf[aOff + 3] = Math.max(buf[aOff + 3] ?? 0, buf[bOff + 3] ?? 0);
103
+ buf[aOff + 4] = Math.max(buf[aOff + 4] ?? 0, buf[bOff + 4] ?? 0);
104
+ buf[aOff + 5] = Math.max(buf[aOff + 5] ?? 0, buf[bOff + 5] ?? 0);
105
+ }
106
+ function aabbMerge(pool, dest, a, b) {
107
+ const buf = pool.buffer;
108
+ const aOff = a * AABB_STRIDE;
109
+ const bOff = b * AABB_STRIDE;
110
+ const dOff = dest * AABB_STRIDE;
111
+ buf[dOff] = Math.min(buf[aOff] ?? 0, buf[bOff] ?? 0);
112
+ buf[dOff + 1] = Math.min(buf[aOff + 1] ?? 0, buf[bOff + 1] ?? 0);
113
+ buf[dOff + 2] = Math.min(buf[aOff + 2] ?? 0, buf[bOff + 2] ?? 0);
114
+ buf[dOff + 3] = Math.max(buf[aOff + 3] ?? 0, buf[bOff + 3] ?? 0);
115
+ buf[dOff + 4] = Math.max(buf[aOff + 4] ?? 0, buf[bOff + 4] ?? 0);
116
+ buf[dOff + 5] = Math.max(buf[aOff + 5] ?? 0, buf[bOff + 5] ?? 0);
117
+ }
118
+ function aabbIntersects(pool, a, b) {
119
+ const aOff = a * AABB_STRIDE;
120
+ const bOff = b * AABB_STRIDE;
121
+ const buf = pool.buffer;
122
+ 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);
123
+ }
124
+
125
+ // src/ray.ts
126
+ var RAY_STRIDE = 6;
127
+ var RayPool = class _RayPool {
128
+ buffer;
129
+ count = 0;
130
+ constructor(capacity, sharedBuffer) {
131
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * RAY_STRIDE) : new Float32Array(capacity * RAY_STRIDE);
132
+ }
133
+ /**
134
+ * Create a RayPool backed by a new `SharedArrayBuffer`.
135
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
136
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
137
+ */
138
+ static createShared(capacity) {
139
+ const sab = new SharedArrayBuffer(capacity * RAY_STRIDE * Float32Array.BYTES_PER_ELEMENT);
140
+ return { pool: new _RayPool(capacity, sab), sab };
141
+ }
142
+ /** Allocate a new Ray slot and return its index. */
143
+ allocate() {
144
+ const index = this.count;
145
+ this.count += 1;
146
+ return index;
147
+ }
148
+ /** Set origin and direction of a ray at the given index. */
149
+ set(index, ox, oy, oz, dx, dy, dz) {
150
+ const offset = index * RAY_STRIDE;
151
+ this.buffer[offset] = ox;
152
+ this.buffer[offset + 1] = oy;
153
+ this.buffer[offset + 2] = oz;
154
+ this.buffer[offset + 3] = dx;
155
+ this.buffer[offset + 4] = dy;
156
+ this.buffer[offset + 5] = dz;
157
+ }
158
+ /** Read a single component value from the buffer. */
159
+ get(index, component) {
160
+ return this.buffer[index * RAY_STRIDE + component] ?? 0;
161
+ }
162
+ /** Returns the number of allocated Rays. */
163
+ get size() {
164
+ return this.count;
165
+ }
166
+ /** Reset the pool (all allocations freed, no GC). */
167
+ reset() {
168
+ this.count = 0;
169
+ }
170
+ };
171
+ function rayIntersectsAABB(rayBuf, rayOffset, aabbBuf, aabbOffset) {
172
+ const ox = rayBuf[rayOffset] ?? 0;
173
+ const oy = rayBuf[rayOffset + 1] ?? 0;
174
+ const oz = rayBuf[rayOffset + 2] ?? 0;
175
+ const idx = 1 / (rayBuf[rayOffset + 3] ?? 0);
176
+ const idy = 1 / (rayBuf[rayOffset + 4] ?? 0);
177
+ const idz = 1 / (rayBuf[rayOffset + 5] ?? 0);
178
+ const minX = aabbBuf[aabbOffset] ?? 0;
179
+ const minY = aabbBuf[aabbOffset + 1] ?? 0;
180
+ const minZ = aabbBuf[aabbOffset + 2] ?? 0;
181
+ const maxX = aabbBuf[aabbOffset + 3] ?? 0;
182
+ const maxY = aabbBuf[aabbOffset + 4] ?? 0;
183
+ const maxZ = aabbBuf[aabbOffset + 5] ?? 0;
184
+ const t1x = (minX - ox) * idx;
185
+ const t2x = (maxX - ox) * idx;
186
+ const t1y = (minY - oy) * idy;
187
+ const t2y = (maxY - oy) * idy;
188
+ const t1z = (minZ - oz) * idz;
189
+ const t2z = (maxZ - oz) * idz;
190
+ const tmin = Math.max(Math.min(t1x, t2x), Math.min(t1y, t2y), Math.min(t1z, t2z));
191
+ const tmax = Math.min(Math.max(t1x, t2x), Math.max(t1y, t2y), Math.max(t1z, t2z));
192
+ if (tmax < 0 || !(tmin <= tmax)) return -1;
193
+ return tmin >= 0 ? tmin : tmax;
194
+ }
195
+
196
+ // src/object-pool.ts
197
+ var ObjectPool = class {
198
+ freeList;
199
+ top;
200
+ /** Total number of slots in the pool. */
201
+ capacity;
202
+ constructor(capacity) {
203
+ this.capacity = capacity;
204
+ this.freeList = new Int32Array(capacity);
205
+ for (let i = 0; i < capacity; i++) {
206
+ this.freeList[i] = capacity - 1 - i;
207
+ }
208
+ this.top = capacity;
209
+ }
210
+ /** Number of indices currently available for acquisition. */
211
+ get available() {
212
+ return this.top;
213
+ }
214
+ /**
215
+ * Acquire a free index from the pool.
216
+ * Returns the index, or `null` when the pool is exhausted.
217
+ */
218
+ acquire() {
219
+ if (this.top === 0) {
220
+ return null;
221
+ }
222
+ this.top -= 1;
223
+ return this.freeList[this.top];
224
+ }
225
+ /**
226
+ * Release a previously acquired index back to the pool.
227
+ * Throws if the index is out of range or the pool is already full.
228
+ */
229
+ release(index) {
230
+ if (index < 0 || index >= this.capacity) {
231
+ throw new RangeError(`ObjectPool.release: index ${index} is out of range [0, ${this.capacity})`);
232
+ }
233
+ if (this.top >= this.capacity) {
234
+ throw new RangeError("ObjectPool.release: pool is already at full capacity");
235
+ }
236
+ this.freeList[this.top] = index;
237
+ this.top += 1;
238
+ }
239
+ };
240
+
241
+ // src/flat-math.ts
242
+ function vec3Dot(buf, i, j) {
243
+ return (buf[i] ?? 0) * (buf[j] ?? 0) + (buf[i + 1] ?? 0) * (buf[j + 1] ?? 0) + (buf[i + 2] ?? 0) * (buf[j + 2] ?? 0);
244
+ }
245
+ function vec3DistanceSq(buf, i, j) {
246
+ const dx = (buf[i] ?? 0) - (buf[j] ?? 0);
247
+ const dy = (buf[i + 1] ?? 0) - (buf[j + 1] ?? 0);
248
+ const dz = (buf[i + 2] ?? 0) - (buf[j + 2] ?? 0);
249
+ return dx * dx + dy * dy + dz * dz;
250
+ }
251
+ function vec3Distance(buf, i, j) {
252
+ return Math.sqrt(vec3DistanceSq(buf, i, j));
253
+ }
254
+ function vec3Cross(out, outOffset, buf, i, j) {
255
+ const ax = buf[i] ?? 0;
256
+ const ay = buf[i + 1] ?? 0;
257
+ const az = buf[i + 2] ?? 0;
258
+ const bx = buf[j] ?? 0;
259
+ const by = buf[j + 1] ?? 0;
260
+ const bz = buf[j + 2] ?? 0;
261
+ out[outOffset] = ay * bz - az * by;
262
+ out[outOffset + 1] = az * bx - ax * bz;
263
+ out[outOffset + 2] = ax * by - ay * bx;
264
+ }
265
+
266
+ // src/octree-node.ts
267
+ var NODE_AABB_OFFSET = 0;
268
+ var NODE_FIRST_CHILD_OFFSET = 6;
269
+ var NODE_PARENT_OFFSET = 7;
270
+ var NODE_OBJECT_COUNT_OFFSET = 8;
271
+ var NODE_OBJECTS_OFFSET = 9;
272
+ var MAX_OBJECTS_PER_NODE = 8;
273
+ var NODE_STRIDE = NODE_OBJECTS_OFFSET + MAX_OBJECTS_PER_NODE;
274
+ var OctreeNodePool = class _OctreeNodePool {
275
+ buffer;
276
+ count = 0;
277
+ constructor(capacity, sharedBuffer) {
278
+ this.buffer = sharedBuffer ? new Float32Array(sharedBuffer, 0, capacity * NODE_STRIDE) : new Float32Array(capacity * NODE_STRIDE);
279
+ }
280
+ /**
281
+ * Create an OctreeNodePool backed by a new `SharedArrayBuffer`.
282
+ * Both the pool and the underlying `SharedArrayBuffer` are returned so the
283
+ * caller can transfer the buffer to a `Worker` via `postMessage`.
284
+ */
285
+ static createShared(capacity) {
286
+ const sab = new SharedArrayBuffer(capacity * NODE_STRIDE * Float32Array.BYTES_PER_ELEMENT);
287
+ return { pool: new _OctreeNodePool(capacity, sab), sab };
288
+ }
289
+ /** Allocate a new node slot, initialise sentinel values, and return its index. */
290
+ allocate() {
291
+ const index = this.count;
292
+ const offset = index * NODE_STRIDE;
293
+ this.buffer[offset + NODE_FIRST_CHILD_OFFSET] = -1;
294
+ this.buffer[offset + NODE_PARENT_OFFSET] = -1;
295
+ this.buffer[offset + NODE_OBJECT_COUNT_OFFSET] = 0;
296
+ this.count += 1;
297
+ return index;
298
+ }
299
+ /** Returns the number of allocated nodes. */
300
+ get size() {
301
+ return this.count;
302
+ }
303
+ /** Reset the pool (all allocations freed, no GC). */
304
+ reset() {
305
+ this.count = 0;
306
+ }
307
+ // ── AABB ──────────────────────────────────────────────────────────────────
308
+ /** Set the AABB bounds for the node at the given index. */
309
+ setAABB(index, minX, minY, minZ, maxX, maxY, maxZ) {
310
+ const off = index * NODE_STRIDE + NODE_AABB_OFFSET;
311
+ this.buffer[off] = minX;
312
+ this.buffer[off + 1] = minY;
313
+ this.buffer[off + 2] = minZ;
314
+ this.buffer[off + 3] = maxX;
315
+ this.buffer[off + 4] = maxY;
316
+ this.buffer[off + 5] = maxZ;
317
+ }
318
+ /** Read a single AABB component (0–5) from the node at the given index. */
319
+ getAABB(index, component) {
320
+ return this.buffer[index * NODE_STRIDE + NODE_AABB_OFFSET + component] ?? 0;
321
+ }
322
+ // ── First-child link ──────────────────────────────────────────────────────
323
+ /** Set the first-child index for the node (-1 = leaf). */
324
+ setFirstChild(index, childIndex) {
325
+ this.buffer[index * NODE_STRIDE + NODE_FIRST_CHILD_OFFSET] = childIndex;
326
+ }
327
+ /** Get the first-child index for the node (-1 = leaf). */
328
+ getFirstChild(index) {
329
+ return this.buffer[index * NODE_STRIDE + NODE_FIRST_CHILD_OFFSET] ?? -1;
330
+ }
331
+ // ── Parent link ───────────────────────────────────────────────────────────
332
+ /** Set the parent index for the node (-1 = root). */
333
+ setParent(index, parentIndex) {
334
+ this.buffer[index * NODE_STRIDE + NODE_PARENT_OFFSET] = parentIndex;
335
+ }
336
+ /** Get the parent index for the node (-1 = root). */
337
+ getParent(index) {
338
+ return this.buffer[index * NODE_STRIDE + NODE_PARENT_OFFSET] ?? -1;
339
+ }
340
+ // ── Object pointers ───────────────────────────────────────────────────────
341
+ /** Return the number of objects stored in the node at the given index. */
342
+ getObjectCount(index) {
343
+ return this.buffer[index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET] ?? 0;
344
+ }
345
+ /**
346
+ * Append an object index to the node's object list.
347
+ * Throws a RangeError when MAX_OBJECTS_PER_NODE is exceeded.
348
+ */
349
+ addObject(index, objectIndex) {
350
+ const countOff = index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET;
351
+ const count = this.buffer[countOff] ?? 0;
352
+ if (count >= MAX_OBJECTS_PER_NODE) {
353
+ throw new RangeError(
354
+ `OctreeNodePool.addObject: node ${index} already contains MAX_OBJECTS_PER_NODE (${MAX_OBJECTS_PER_NODE}) objects`
355
+ );
356
+ }
357
+ this.buffer[index * NODE_STRIDE + NODE_OBJECTS_OFFSET + count] = objectIndex;
358
+ this.buffer[countOff] = count + 1;
359
+ }
360
+ /** Return the object index stored at slot `slot` in the given node. */
361
+ getObject(index, slot) {
362
+ return this.buffer[index * NODE_STRIDE + NODE_OBJECTS_OFFSET + slot] ?? 0;
363
+ }
364
+ /** Reset the object count of the given node to zero (does not zero the slots). */
365
+ clearObjects(index) {
366
+ this.buffer[index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET] = 0;
367
+ }
368
+ /**
369
+ * Remove a single object index from the node's object list.
370
+ * Uses swap-with-last to avoid shifting. Returns true if found and removed.
371
+ */
372
+ removeObject(index, objectIndex) {
373
+ const countOff = index * NODE_STRIDE + NODE_OBJECT_COUNT_OFFSET;
374
+ const count = this.buffer[countOff] ?? 0;
375
+ const baseOff = index * NODE_STRIDE + NODE_OBJECTS_OFFSET;
376
+ for (let i = 0; i < count; i++) {
377
+ if (this.buffer[baseOff + i] === objectIndex) {
378
+ this.buffer[baseOff + i] = this.buffer[baseOff + count - 1] ?? 0;
379
+ this.buffer[countOff] = count - 1;
380
+ return true;
381
+ }
382
+ }
383
+ return false;
384
+ }
385
+ };
386
+
387
+ // src/octree.ts
388
+ function aabbOverlapsBox(buf, bufOffset, qMinX, qMinY, qMinZ, qMaxX, qMaxY, qMaxZ) {
389
+ return buf[bufOffset] <= qMaxX && buf[bufOffset + 3] >= qMinX && buf[bufOffset + 1] <= qMaxY && buf[bufOffset + 4] >= qMinY && buf[bufOffset + 2] <= qMaxZ && buf[bufOffset + 5] >= qMinZ;
390
+ }
391
+ var Octree = class {
392
+ nodePool;
393
+ aabbPool;
394
+ root;
395
+ /** Tracks which node each object (by AABBPool index) is currently stored in. */
396
+ objectNodeMap = /* @__PURE__ */ new Map();
397
+ /** Pre-allocated traversal stack reused across raycast calls to avoid GC pressure. */
398
+ _stack = [];
399
+ constructor(nodePool, aabbPool) {
400
+ this.nodePool = nodePool;
401
+ this.aabbPool = aabbPool;
402
+ this.root = nodePool.allocate();
403
+ }
404
+ /** The index of the root node in the underlying OctreeNodePool. */
405
+ get rootIndex() {
406
+ return this.root;
407
+ }
408
+ /** Set the world-space AABB that the root node covers. */
409
+ setBounds(minX, minY, minZ, maxX, maxY, maxZ) {
410
+ this.nodePool.setAABB(this.root, minX, minY, minZ, maxX, maxY, maxZ);
411
+ }
412
+ /** Insert the AABB at `objectIndex` (in the AABBPool) into the tree. */
413
+ insert(objectIndex) {
414
+ this.insertIntoNode(this.root, objectIndex);
415
+ }
416
+ /**
417
+ * Update the AABB at `objectIndex` with new bounds and reposition it in the
418
+ * tree without rebuilding. If the new bounds still fit in the current node
419
+ * the object stays there; otherwise it is removed and re-inserted from the
420
+ * lowest ancestor whose bounds fully contain the new AABB.
421
+ */
422
+ update(objectIndex, newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ) {
423
+ const np = this.nodePool;
424
+ const ap = this.aabbPool;
425
+ ap.set(objectIndex, newMinX, newMinY, newMinZ, newMaxX, newMaxY, newMaxZ);
426
+ const currentNode = this.objectNodeMap.get(objectIndex);
427
+ if (currentNode === void 0) return;
428
+ if (this.fitsInNode(objectIndex, currentNode)) return;
429
+ np.removeObject(currentNode, objectIndex);
430
+ this.objectNodeMap.delete(objectIndex);
431
+ let ancestorNode = np.getParent(currentNode);
432
+ while (ancestorNode !== -1 && !this.fitsInNode(objectIndex, ancestorNode)) {
433
+ ancestorNode = np.getParent(ancestorNode);
434
+ }
435
+ if (ancestorNode === -1) ancestorNode = this.root;
436
+ this.insertIntoNode(ancestorNode, objectIndex);
437
+ }
438
+ /**
439
+ * Cast a ray through the octree and return the closest intersecting object.
440
+ *
441
+ * Uses an iterative stack traversal (pre-allocated, no recursion) to avoid
442
+ * GC pressure. Only descends into child nodes whose AABBs are intersected by
443
+ * the ray, providing efficient pruning of non-intersecting subtrees.
444
+ *
445
+ * @param rayBuf Float32Array containing the ray data [ox,oy,oz,dx,dy,dz].
446
+ * @param rayOffset Element offset (in floats) within `rayBuf` to the ray's origin.
447
+ * @returns The closest `{ objectIndex, t }` hit, or `null` if nothing is hit.
448
+ */
449
+ raycast(rayBuf, rayOffset) {
450
+ const np = this.nodePool;
451
+ const npBuf = np.buffer;
452
+ const apBuf = this.aabbPool.buffer;
453
+ if (rayIntersectsAABB(rayBuf, rayOffset, npBuf, this.root * NODE_STRIDE + NODE_AABB_OFFSET) < 0) {
454
+ return null;
455
+ }
456
+ this._stack.length = 0;
457
+ this._stack.push(this.root);
458
+ let closestT = Infinity;
459
+ let closestIndex = -1;
460
+ while (this._stack.length > 0) {
461
+ const nodeIdx = this._stack.pop();
462
+ const objCount = np.getObjectCount(nodeIdx);
463
+ for (let i = 0; i < objCount; i++) {
464
+ const objIdx = np.getObject(nodeIdx, i);
465
+ const t = rayIntersectsAABB(rayBuf, rayOffset, apBuf, objIdx * AABB_STRIDE);
466
+ if (t >= 0 && t < closestT) {
467
+ closestT = t;
468
+ closestIndex = objIdx;
469
+ }
470
+ }
471
+ const firstChild = np.getFirstChild(nodeIdx);
472
+ if (firstChild !== -1) {
473
+ for (let i = 0; i < 8; i++) {
474
+ const childIdx = firstChild + i;
475
+ if (rayIntersectsAABB(rayBuf, rayOffset, npBuf, childIdx * NODE_STRIDE + NODE_AABB_OFFSET) >= 0) {
476
+ this._stack.push(childIdx);
477
+ }
478
+ }
479
+ }
480
+ }
481
+ if (closestIndex === -1) return null;
482
+ return { objectIndex: closestIndex, t: closestT };
483
+ }
484
+ /**
485
+ * Query the octree for all objects whose AABB overlaps the given axis-aligned
486
+ * box region and return their indices (in the AABBPool).
487
+ *
488
+ * Uses the same pre-allocated iterative stack as `raycast` to avoid GC
489
+ * pressure. Descends only into child nodes whose AABBs overlap the query box,
490
+ * pruning non-intersecting subtrees.
491
+ *
492
+ * @param minX Minimum X of the query box.
493
+ * @param minY Minimum Y of the query box.
494
+ * @param minZ Minimum Z of the query box.
495
+ * @param maxX Maximum X of the query box.
496
+ * @param maxY Maximum Y of the query box.
497
+ * @param maxZ Maximum Z of the query box.
498
+ * @returns Array of AABBPool indices for every object that overlaps the box.
499
+ */
500
+ queryBox(minX, minY, minZ, maxX, maxY, maxZ) {
501
+ const np = this.nodePool;
502
+ const npBuf = np.buffer;
503
+ const apBuf = this.aabbPool.buffer;
504
+ const results = [];
505
+ if (!aabbOverlapsBox(npBuf, this.root * NODE_STRIDE + NODE_AABB_OFFSET, minX, minY, minZ, maxX, maxY, maxZ)) {
506
+ return results;
507
+ }
508
+ this._stack.length = 0;
509
+ this._stack.push(this.root);
510
+ while (this._stack.length > 0) {
511
+ const nodeIdx = this._stack.pop();
512
+ const objCount = np.getObjectCount(nodeIdx);
513
+ for (let i = 0; i < objCount; i++) {
514
+ const objIdx = np.getObject(nodeIdx, i);
515
+ if (aabbOverlapsBox(apBuf, objIdx * AABB_STRIDE, minX, minY, minZ, maxX, maxY, maxZ)) {
516
+ results.push(objIdx);
517
+ }
518
+ }
519
+ const firstChild = np.getFirstChild(nodeIdx);
520
+ if (firstChild !== -1) {
521
+ for (let i = 0; i < 8; i++) {
522
+ const childIdx = firstChild + i;
523
+ if (aabbOverlapsBox(npBuf, childIdx * NODE_STRIDE + NODE_AABB_OFFSET, minX, minY, minZ, maxX, maxY, maxZ)) {
524
+ this._stack.push(childIdx);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ return results;
530
+ }
531
+ // ── Private helpers ──────────────────────────────────────────────────────
532
+ insertIntoNode(nodeIdx, objectIndex) {
533
+ const np = this.nodePool;
534
+ const firstChild = np.getFirstChild(nodeIdx);
535
+ if (firstChild !== -1) {
536
+ for (let i = 0; i < 8; i++) {
537
+ if (this.fitsInNode(objectIndex, firstChild + i)) {
538
+ this.insertIntoNode(firstChild + i, objectIndex);
539
+ return;
540
+ }
541
+ }
542
+ np.addObject(nodeIdx, objectIndex);
543
+ this.objectNodeMap.set(objectIndex, nodeIdx);
544
+ return;
545
+ }
546
+ const count = np.getObjectCount(nodeIdx);
547
+ if (count < MAX_OBJECTS_PER_NODE) {
548
+ np.addObject(nodeIdx, objectIndex);
549
+ this.objectNodeMap.set(objectIndex, nodeIdx);
550
+ } else {
551
+ this.subdivide(nodeIdx);
552
+ this.insertIntoNode(nodeIdx, objectIndex);
553
+ }
554
+ }
555
+ /** Split a leaf node into 8 children and redistribute its objects. */
556
+ subdivide(nodeIdx) {
557
+ const np = this.nodePool;
558
+ const minX = np.getAABB(nodeIdx, 0);
559
+ const minY = np.getAABB(nodeIdx, 1);
560
+ const minZ = np.getAABB(nodeIdx, 2);
561
+ const maxX = np.getAABB(nodeIdx, 3);
562
+ const maxY = np.getAABB(nodeIdx, 4);
563
+ const maxZ = np.getAABB(nodeIdx, 5);
564
+ const midX = (minX + maxX) / 2;
565
+ const midY = (minY + maxY) / 2;
566
+ const midZ = (minZ + maxZ) / 2;
567
+ const firstChild = np.allocate();
568
+ for (let i = 1; i < 8; i++) {
569
+ np.allocate();
570
+ }
571
+ np.setFirstChild(nodeIdx, firstChild);
572
+ for (let i = 0; i < 8; i++) {
573
+ const childIdx = firstChild + i;
574
+ const cMinX = (i & 1) === 0 ? minX : midX;
575
+ const cMaxX = (i & 1) === 0 ? midX : maxX;
576
+ const cMinY = (i & 2) === 0 ? minY : midY;
577
+ const cMaxY = (i & 2) === 0 ? midY : maxY;
578
+ const cMinZ = (i & 4) === 0 ? minZ : midZ;
579
+ const cMaxZ = (i & 4) === 0 ? midZ : maxZ;
580
+ np.setAABB(childIdx, cMinX, cMinY, cMinZ, cMaxX, cMaxY, cMaxZ);
581
+ np.setParent(childIdx, nodeIdx);
582
+ }
583
+ const count = np.getObjectCount(nodeIdx);
584
+ const saved = [];
585
+ for (let i = 0; i < count; i++) {
586
+ saved.push(np.getObject(nodeIdx, i));
587
+ }
588
+ np.clearObjects(nodeIdx);
589
+ for (const obj of saved) {
590
+ this.objectNodeMap.delete(obj);
591
+ this.insertIntoNode(nodeIdx, obj);
592
+ }
593
+ }
594
+ /** Returns true when the AABB at `objectIndex` is fully contained by `nodeIdx`. */
595
+ fitsInNode(objectIndex, nodeIdx) {
596
+ const np = this.nodePool;
597
+ const ap = this.aabbPool;
598
+ 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);
599
+ }
600
+ };
601
+
602
+ // src/lidar-worker.ts
603
+ function createLidarProcessor() {
604
+ let octree = null;
605
+ let aabbPool = null;
606
+ let raysBuf = null;
607
+ let resultsBuf = null;
608
+ let totalRayCount = 0;
609
+ let insertedObjectCount = 0;
610
+ return {
611
+ /** Initialise shared buffers and create the Octree. */
612
+ init(msg) {
613
+ const nodePool = new OctreeNodePool(msg.nodeCapacity, msg.nodesSab);
614
+ aabbPool = new AABBPool(msg.objectCapacity, msg.aabbsSab);
615
+ raysBuf = new Float32Array(msg.raysSab);
616
+ resultsBuf = new Float32Array(msg.resultsSab);
617
+ totalRayCount = msg.rayCount;
618
+ insertedObjectCount = 0;
619
+ octree = new Octree(nodePool, aabbPool);
620
+ octree.setBounds(
621
+ msg.worldMinX,
622
+ msg.worldMinY,
623
+ msg.worldMinZ,
624
+ msg.worldMaxX,
625
+ msg.worldMaxY,
626
+ msg.worldMaxZ
627
+ );
628
+ return { type: "ready" };
629
+ },
630
+ /**
631
+ * Read current object AABB data from the shared buffer, update the Octree,
632
+ * cast every sweep ray, and write results to the results buffer.
633
+ */
634
+ sweep(msg) {
635
+ if (octree === null || aabbPool === null || raysBuf === null || resultsBuf === null) {
636
+ throw new Error("LidarProcessor: sweep called before init");
637
+ }
638
+ const objectCount = msg.objectCount;
639
+ while (aabbPool.size < objectCount) {
640
+ aabbPool.allocate();
641
+ }
642
+ for (let i = 0; i < objectCount; i++) {
643
+ const minX = aabbPool.get(i, 0);
644
+ const minY = aabbPool.get(i, 1);
645
+ const minZ = aabbPool.get(i, 2);
646
+ const maxX = aabbPool.get(i, 3);
647
+ const maxY = aabbPool.get(i, 4);
648
+ const maxZ = aabbPool.get(i, 5);
649
+ if (i < insertedObjectCount) {
650
+ octree.update(i, minX, minY, minZ, maxX, maxY, maxZ);
651
+ } else {
652
+ octree.insert(i);
653
+ insertedObjectCount++;
654
+ }
655
+ }
656
+ for (let r = 0; r < totalRayCount; r++) {
657
+ const hit = octree.raycast(raysBuf, r * RAY_STRIDE);
658
+ if (hit !== null) {
659
+ resultsBuf[r * 2] = hit.objectIndex;
660
+ resultsBuf[r * 2 + 1] = hit.t;
661
+ } else {
662
+ resultsBuf[r * 2] = -1;
663
+ resultsBuf[r * 2 + 1] = -1;
664
+ }
665
+ }
666
+ return { type: "done", rayCount: totalRayCount };
667
+ }
668
+ };
669
+ }
670
+ var globalScope = globalThis;
671
+ if (typeof globalScope["postMessage"] === "function" && typeof globalScope["onmessage"] !== "undefined") {
672
+ const workerSelf = globalThis;
673
+ const processor = createLidarProcessor();
674
+ workerSelf.onmessage = (event) => {
675
+ const msg = event.data;
676
+ if (msg.type === "init") {
677
+ workerSelf.postMessage(processor.init(msg));
678
+ } else if (msg.type === "sweep") {
679
+ workerSelf.postMessage(processor.sweep(msg));
680
+ }
681
+ };
682
+ }
683
+ // Annotate the CommonJS export names for ESM import in node:
684
+ 0 && (module.exports = {
685
+ AABBPool,
686
+ AABB_STRIDE,
687
+ MAX_OBJECTS_PER_NODE,
688
+ NODE_AABB_OFFSET,
689
+ NODE_FIRST_CHILD_OFFSET,
690
+ NODE_OBJECTS_OFFSET,
691
+ NODE_OBJECT_COUNT_OFFSET,
692
+ NODE_PARENT_OFFSET,
693
+ NODE_STRIDE,
694
+ ObjectPool,
695
+ Octree,
696
+ OctreeNodePool,
697
+ RAY_STRIDE,
698
+ RayPool,
699
+ aabbExpand,
700
+ aabbIntersects,
701
+ aabbMerge,
702
+ createLidarProcessor,
703
+ rayIntersectsAABB,
704
+ vec3Cross,
705
+ vec3Distance,
706
+ vec3DistanceSq,
707
+ vec3Dot
708
+ });
709
+ //# sourceMappingURL=index.cjs.map