@woosh/meep-engine 2.152.0 → 2.154.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.
Files changed (99) hide show
  1. package/package.json +1 -1
  2. package/src/core/color/Color.d.ts +26 -6
  3. package/src/core/color/Color.d.ts.map +1 -1
  4. package/src/core/color/Color.js +38 -6
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  6. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  7. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  9. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  10. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  11. package/src/engine/physics/PLAN.md +4 -4
  12. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  13. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  14. package/src/engine/physics/body/BodyStorage.js +452 -450
  15. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  16. package/src/engine/physics/body/SolverBodyState.js +6 -5
  17. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  18. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  19. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  20. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  21. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  22. package/src/engine/physics/computeInterceptPoint.js +8 -3
  23. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  24. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  25. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  26. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  27. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/BodyKind.js +25 -24
  29. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  30. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  32. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  33. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  34. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  35. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  37. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  38. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/is_sensor.js +27 -0
  40. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  41. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  42. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  43. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  44. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  45. package/src/engine/physics/gjk/gjk.js +3 -52
  46. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  47. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  48. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  49. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  50. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  52. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  53. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  54. package/src/engine/physics/gjk/mpr.js +362 -344
  55. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  56. package/src/engine/physics/island/IslandBuilder.js +431 -428
  57. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  58. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  59. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  60. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  61. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  62. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  63. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  64. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  65. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  66. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  68. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  69. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  71. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  72. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  74. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  75. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  76. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  77. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  78. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  79. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  80. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  81. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  82. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  83. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  84. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  86. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  87. package/src/engine/physics/queries/overlap_shape.js +16 -17
  88. package/src/engine/physics/queries/raycast.d.ts +5 -0
  89. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  90. package/src/engine/physics/queries/raycast.js +16 -8
  91. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  92. package/src/engine/physics/queries/shape_cast.js +13 -7
  93. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  94. package/src/engine/physics/solver/solve_contacts.js +8 -11
  95. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  96. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  97. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  98. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  99. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1,450 +1,452 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { BodyKind } from "../ecs/BodyKind.js";
3
-
4
- const DEFAULT_INITIAL_CAPACITY = 16;
5
- const MIN_CAPACITY = 4;
6
-
7
- /**
8
- * Mask for the 8-bit generation field encoded in a packed body id.
9
- * @type {number}
10
- */
11
- const GENERATION_MASK = 0xFF;
12
-
13
- /**
14
- * Bit width of the generation field. The remaining bits are the body index;
15
- * 24-bit indices give us 16M live bodies which is comfortably above the design
16
- * target of "millions".
17
- * @type {number}
18
- */
19
- const GENERATION_BITS = 8;
20
-
21
- /**
22
- * Sentinel returned by {@link BodyStorage#awake_position_of} and friends when a
23
- * body is not in the awake set.
24
- * @type {number}
25
- */
26
- export const BODY_INDEX_ABSENT = -1;
27
-
28
- /**
29
- * Pack a body index and generation into a single integer handle.
30
- *
31
- * @param {number} index
32
- * @param {number} generation
33
- * @returns {number}
34
- */
35
- export function pack_body_id(index, generation) {
36
- return (index << GENERATION_BITS) | (generation & GENERATION_MASK);
37
- }
38
-
39
- /**
40
- *
41
- * @param {number} packed
42
- * @returns {number}
43
- */
44
- export function body_id_index(packed) {
45
- return packed >>> GENERATION_BITS;
46
- }
47
-
48
- /**
49
- *
50
- * @param {number} packed
51
- * @returns {number}
52
- */
53
- export function body_id_generation(packed) {
54
- return packed & GENERATION_MASK;
55
- }
56
-
57
- /**
58
- * Structure-of-arrays pool for rigid bodies.
59
- *
60
- * Owns:
61
- * - per-body identity (entity, generation, kind, flags),
62
- * - a dense list of awake body indices (the simulation hot iteration target),
63
- * - a min-heap of free body indices so reuse order is deterministic regardless
64
- * of how interleaved allocate / free calls have been.
65
- *
66
- * The pool grows by doubling when the high-water mark reaches capacity; arrays
67
- * are replaced wholesale on grow, so callers must not retain references to the
68
- * raw typed arrays across allocate-after-grow boundaries.
69
- *
70
- * Determinism: allocate always reuses the lowest free index. Two pools given the
71
- * same sequence of allocate/free calls observe identical body indices.
72
- *
73
- * @author Alex Goldring
74
- * @copyright Company Named Limited (c) 2026
75
- */
76
- export class BodyStorage {
77
-
78
- /**
79
- *
80
- * @param {number} [initial_capacity]
81
- */
82
- constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
83
- assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');
84
-
85
- const cap = Math.max(MIN_CAPACITY, initial_capacity);
86
-
87
- this.__capacity = cap;
88
- // High-water mark of slot indices ever issued. Slots in [0, count) are
89
- // either currently allocated or sitting on the free heap.
90
- this.__count = 0;
91
-
92
- this.__entities = new Int32Array(cap);
93
- this.__generations = new Uint8Array(cap);
94
- this.__kinds = new Uint8Array(cap);
95
- this.__flags = new Uint32Array(cap);
96
-
97
- // 1 = allocated, 0 = free. Cheaper than scanning the free heap.
98
- this.__alive = new Uint8Array(cap);
99
-
100
- // Awake set: dense list of awake body indices + reverse map.
101
- this.__awake_list = new Uint32Array(cap);
102
- this.__awake_pos = new Int32Array(cap);
103
- this.__awake_count = 0;
104
-
105
- // Min-heap of free body indices for deterministic reuse.
106
- this.__free_heap = new Uint32Array(cap);
107
- this.__free_count = 0;
108
-
109
- // Entity → body-index map (one body per entity). Keeps {@link
110
- // index_of_entity} O(1) instead of an O(N) scan over the slot table on
111
- // every collider attach / detach and joint link. Maintained on
112
- // allocate / free; never grows with the typed arrays (a plain Map).
113
- this.__entity_to_index = new Map();
114
-
115
- // Initialise reverse map to BODY_INDEX_ABSENT.
116
- this.__awake_pos.fill(BODY_INDEX_ABSENT);
117
- }
118
-
119
- /**
120
- * Currently allocated body count (live, regardless of awake/sleeping).
121
- * @returns {number}
122
- */
123
- get size() {
124
- return this.__count - this.__free_count;
125
- }
126
-
127
- /**
128
- * Total slot capacity. Always a power of two by construction.
129
- * @returns {number}
130
- */
131
- get capacity() {
132
- return this.__capacity;
133
- }
134
-
135
- /**
136
- * Number of awake bodies.
137
- * @returns {number}
138
- */
139
- get awake_count() {
140
- return this.__awake_count;
141
- }
142
-
143
- /**
144
- * High-water mark of slot indices ever issued. Useful when callers maintain
145
- * per-slot side-tables.
146
- * @returns {number}
147
- */
148
- get high_water_mark() {
149
- return this.__count;
150
- }
151
-
152
- /**
153
- * Allocate a body slot for `entity`. The new body starts in the awake set
154
- * with default kind {@link BodyKind.Dynamic} and zero flags; the caller
155
- * may override these via {@link set_kind} / {@link set_flags}.
156
- *
157
- * @param {number} entity
158
- * @returns {number} packed body id
159
- */
160
- allocate(entity) {
161
- let index;
162
-
163
- if (this.__free_count > 0) {
164
- index = this.__heap_pop();
165
- } else {
166
- if (this.__count === this.__capacity) {
167
- this.__grow();
168
- }
169
- index = this.__count++;
170
- }
171
-
172
- this.__entities[index] = entity;
173
- this.__kinds[index] = BodyKind.Dynamic;
174
- this.__flags[index] = 0;
175
- this.__alive[index] = 1;
176
- this.__entity_to_index.set(entity, index);
177
-
178
- // Insert into awake set.
179
- const awake_pos = this.__awake_count++;
180
- this.__awake_list[awake_pos] = index;
181
- this.__awake_pos[index] = awake_pos;
182
-
183
- return pack_body_id(index, this.__generations[index]);
184
- }
185
-
186
- /**
187
- * Release a body slot previously returned by {@link allocate}. Bumps the
188
- * generation so any old packed id becomes stale.
189
- *
190
- * @param {number} packed_body_id
191
- */
192
- free(packed_body_id) {
193
- const index = body_id_index(packed_body_id);
194
-
195
- assert.equal(this.is_valid(packed_body_id), true, 'free() called on stale or unknown body id');
196
-
197
- // Remove from awake set if present.
198
- const awake_pos = this.__awake_pos[index];
199
- if (awake_pos !== BODY_INDEX_ABSENT) {
200
- this.__awake_remove_at(awake_pos);
201
- this.__awake_pos[index] = BODY_INDEX_ABSENT;
202
- }
203
-
204
- this.__alive[index] = 0;
205
-
206
- // Drop the entity → index mapping (the slot still holds the old entity
207
- // value until reallocation, so delete by it now while it's valid).
208
- this.__entity_to_index.delete(this.__entities[index]);
209
-
210
- // Bump generation; wraps mod 256.
211
- this.__generations[index] = (this.__generations[index] + 1) & GENERATION_MASK;
212
-
213
- this.__heap_push(index);
214
- }
215
-
216
- /**
217
- *
218
- * @param {number} packed_body_id
219
- * @returns {boolean}
220
- */
221
- is_valid(packed_body_id) {
222
- const index = body_id_index(packed_body_id);
223
- if (index < 0 || index >= this.__count) {
224
- return false;
225
- }
226
- if (this.__alive[index] !== 1) {
227
- return false;
228
- }
229
- return this.__generations[index] === body_id_generation(packed_body_id);
230
- }
231
-
232
- /**
233
- * @param {number} index body index (NOT a packed id)
234
- * @returns {number} entity for the body, or -1 if the slot is free.
235
- */
236
- entity_at(index) {
237
- if (this.__alive[index] !== 1) {
238
- return -1;
239
- }
240
- return this.__entities[index];
241
- }
242
-
243
- /**
244
- * Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
245
- * it. O(1) reverse of {@link entity_at} — the lookup callers use on the
246
- * link / attach / joint paths instead of scanning the slot table.
247
- * @param {number} entity
248
- * @returns {number}
249
- */
250
- index_of_entity(entity) {
251
- const idx = this.__entity_to_index.get(entity);
252
- return idx === undefined ? BODY_INDEX_ABSENT : idx;
253
- }
254
-
255
- /**
256
- * @param {number} index
257
- * @returns {number}
258
- */
259
- generation_at(index) {
260
- return this.__generations[index];
261
- }
262
-
263
- /**
264
- * @param {number} index
265
- * @returns {BodyKind|number}
266
- */
267
- kind_at(index) {
268
- return this.__kinds[index];
269
- }
270
-
271
- /**
272
- * @param {number} index
273
- * @param {BodyKind|number} kind
274
- */
275
- set_kind(index, kind) {
276
- this.__kinds[index] = kind;
277
- }
278
-
279
- /**
280
- * @param {number} index
281
- * @returns {number}
282
- */
283
- flags_at(index) {
284
- return this.__flags[index];
285
- }
286
-
287
- /**
288
- * @param {number} index
289
- * @param {number} flags
290
- */
291
- set_flags(index, flags) {
292
- this.__flags[index] = flags;
293
- }
294
-
295
- /**
296
- * Index of `index` in the awake list, or {@link BODY_INDEX_ABSENT} if asleep.
297
- * @param {number} index
298
- * @returns {number}
299
- */
300
- awake_position_of(index) {
301
- return this.__awake_pos[index];
302
- }
303
-
304
- /**
305
- *
306
- * @param {number} index
307
- * @returns {boolean}
308
- */
309
- is_awake(index) {
310
- return this.__awake_pos[index] !== BODY_INDEX_ABSENT;
311
- }
312
-
313
- /**
314
- * Read the body index at position `i` in the dense awake list (0-based).
315
- * @param {number} i
316
- * @returns {number}
317
- */
318
- awake_at(i) {
319
- return this.__awake_list[i];
320
- }
321
-
322
- /**
323
- * Add `index` to the awake set if not already present.
324
- * @param {number} index
325
- */
326
- mark_awake(index) {
327
- if (this.__awake_pos[index] !== BODY_INDEX_ABSENT) {
328
- return;
329
- }
330
- const pos = this.__awake_count++;
331
- this.__awake_list[pos] = index;
332
- this.__awake_pos[index] = pos;
333
- }
334
-
335
- /**
336
- * Remove `index` from the awake set if present.
337
- * @param {number} index
338
- */
339
- mark_sleeping(index) {
340
- const pos = this.__awake_pos[index];
341
- if (pos === BODY_INDEX_ABSENT) {
342
- return;
343
- }
344
- this.__awake_remove_at(pos);
345
- this.__awake_pos[index] = BODY_INDEX_ABSENT;
346
- }
347
-
348
- /**
349
- * Swap-with-last removal at position `pos` in the dense awake list.
350
- * @private
351
- * @param {number} pos
352
- */
353
- __awake_remove_at(pos) {
354
- const last_pos = --this.__awake_count;
355
- if (pos !== last_pos) {
356
- const swapped_index = this.__awake_list[last_pos];
357
- this.__awake_list[pos] = swapped_index;
358
- this.__awake_pos[swapped_index] = pos;
359
- }
360
- }
361
-
362
- /**
363
- * @private
364
- */
365
- __grow() {
366
- const new_cap = this.__capacity << 1;
367
- this.__capacity = new_cap;
368
-
369
- const grow_int32 = (a) => {
370
- const next = new Int32Array(new_cap);
371
- next.set(a);
372
- return next;
373
- };
374
- const grow_uint8 = (a) => {
375
- const next = new Uint8Array(new_cap);
376
- next.set(a);
377
- return next;
378
- };
379
- const grow_uint32 = (a) => {
380
- const next = new Uint32Array(new_cap);
381
- next.set(a);
382
- return next;
383
- };
384
-
385
- this.__entities = grow_int32(this.__entities);
386
- this.__generations = grow_uint8(this.__generations);
387
- this.__kinds = grow_uint8(this.__kinds);
388
- this.__flags = grow_uint32(this.__flags);
389
- this.__alive = grow_uint8(this.__alive);
390
- this.__awake_list = grow_uint32(this.__awake_list);
391
- this.__free_heap = grow_uint32(this.__free_heap);
392
-
393
- // Awake position has signed sentinel — needs fill on the grown portion.
394
- const next_pos = new Int32Array(new_cap);
395
- next_pos.set(this.__awake_pos);
396
- next_pos.fill(BODY_INDEX_ABSENT, this.__awake_pos.length);
397
- this.__awake_pos = next_pos;
398
- }
399
-
400
- // --- Min-heap of free indices --------------------------------------------
401
-
402
- /**
403
- * @private
404
- * @param {number} index
405
- */
406
- __heap_push(index) {
407
- const heap = this.__free_heap;
408
- let i = this.__free_count++;
409
- heap[i] = index;
410
-
411
- while (i > 0) {
412
- const parent = (i - 1) >>> 1;
413
- if (heap[parent] > heap[i]) {
414
- const tmp = heap[parent];
415
- heap[parent] = heap[i];
416
- heap[i] = tmp;
417
- i = parent;
418
- } else {
419
- break;
420
- }
421
- }
422
- }
423
-
424
- /**
425
- * @private
426
- * @returns {number}
427
- */
428
- __heap_pop() {
429
- const heap = this.__free_heap;
430
- const result = heap[0];
431
- const new_count = --this.__free_count;
432
- if (new_count > 0) {
433
- heap[0] = heap[new_count];
434
- let i = 0;
435
- while (true) {
436
- const l = (i << 1) + 1;
437
- const r = l + 1;
438
- let smallest = i;
439
- if (l < new_count && heap[l] < heap[smallest]) smallest = l;
440
- if (r < new_count && heap[r] < heap[smallest]) smallest = r;
441
- if (smallest === i) break;
442
- const tmp = heap[i];
443
- heap[i] = heap[smallest];
444
- heap[smallest] = tmp;
445
- i = smallest;
446
- }
447
- }
448
- return result;
449
- }
450
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { BodyKind } from "../ecs/BodyKind.js";
3
+
4
+ const DEFAULT_INITIAL_CAPACITY = 16;
5
+ const MIN_CAPACITY = 4;
6
+
7
+ /**
8
+ * Mask for the 8-bit generation field encoded in a packed body id.
9
+ * @type {number}
10
+ */
11
+ const GENERATION_MASK = 0xFF;
12
+
13
+ /**
14
+ * Bit width of the generation field. The remaining bits are the body index;
15
+ * 24-bit indices give us 16M live bodies which is comfortably above the design
16
+ * target of "millions".
17
+ * @type {number}
18
+ */
19
+ const GENERATION_BITS = 8;
20
+
21
+ /**
22
+ * Sentinel returned by {@link BodyStorage#awake_position_of} and friends when a
23
+ * body is not in the awake set.
24
+ * @type {number}
25
+ */
26
+ export const BODY_INDEX_ABSENT = -1;
27
+
28
+ /**
29
+ * Pack a body index and generation into a single integer handle.
30
+ *
31
+ * @param {number} index
32
+ * @param {number} generation
33
+ * @returns {number}
34
+ */
35
+ export function pack_body_id(index, generation) {
36
+ return (index << GENERATION_BITS) | (generation & GENERATION_MASK);
37
+ }
38
+
39
+ /**
40
+ *
41
+ * @param {number} packed
42
+ * @returns {number}
43
+ */
44
+ export function body_id_index(packed) {
45
+ return packed >>> GENERATION_BITS;
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {number} packed
51
+ * @returns {number}
52
+ */
53
+ export function body_id_generation(packed) {
54
+ return packed & GENERATION_MASK;
55
+ }
56
+
57
+ /**
58
+ * Structure-of-arrays pool for rigid bodies.
59
+ *
60
+ * Owns:
61
+ * - per-body identity (entity, generation, kind, flags),
62
+ * - a dense list of awake body indices (the simulation hot iteration target),
63
+ * - a min-heap of free body indices so reuse order is deterministic regardless
64
+ * of how interleaved allocate / free calls have been.
65
+ *
66
+ * The pool grows by doubling when the high-water mark reaches capacity; arrays
67
+ * are replaced wholesale on grow, so callers must not retain references to the
68
+ * raw typed arrays across allocate-after-grow boundaries.
69
+ *
70
+ * Determinism: allocate always reuses the lowest free index. Two pools given the
71
+ * same sequence of allocate/free calls observe identical body indices.
72
+ *
73
+ * @author Alex Goldring
74
+ * @copyright Company Named Limited (c) 2026
75
+ */
76
+ export class BodyStorage {
77
+
78
+ /**
79
+ *
80
+ * @param {number} [initial_capacity]
81
+ */
82
+ constructor(initial_capacity = DEFAULT_INITIAL_CAPACITY) {
83
+ assert.isNonNegativeInteger(initial_capacity, 'initial_capacity');
84
+
85
+ const cap = Math.max(MIN_CAPACITY, initial_capacity);
86
+
87
+ this.__capacity = cap;
88
+ // High-water mark of slot indices ever issued. Slots in [0, count) are
89
+ // either currently allocated or sitting on the free heap.
90
+ this.__count = 0;
91
+
92
+ this.__entities = new Int32Array(cap);
93
+ this.__generations = new Uint8Array(cap);
94
+ this.__kinds = new Uint8Array(cap);
95
+ this.__flags = new Uint32Array(cap);
96
+
97
+ // 1 = allocated, 0 = free. Cheaper than scanning the free heap.
98
+ this.__alive = new Uint8Array(cap);
99
+
100
+ // Awake set: dense list of awake body indices + reverse map.
101
+ this.__awake_list = new Uint32Array(cap);
102
+ this.__awake_pos = new Int32Array(cap);
103
+ this.__awake_count = 0;
104
+
105
+ // Min-heap of free body indices for deterministic reuse.
106
+ this.__free_heap = new Uint32Array(cap);
107
+ this.__free_count = 0;
108
+
109
+ // Entity → body-index map (one body per entity). Keeps {@link
110
+ // index_of_entity} O(1) instead of an O(N) scan over the slot table on
111
+ // every collider attach / detach and joint link. Maintained on
112
+ // allocate / free; never grows with the typed arrays (a plain Map).
113
+ this.__entity_to_index = new Map();
114
+
115
+ // Initialise reverse map to BODY_INDEX_ABSENT.
116
+ this.__awake_pos.fill(BODY_INDEX_ABSENT);
117
+ }
118
+
119
+ /**
120
+ * Currently allocated body count (live, regardless of awake/sleeping).
121
+ * @returns {number}
122
+ */
123
+ get size() {
124
+ return this.__count - this.__free_count;
125
+ }
126
+
127
+ /**
128
+ * Total slot capacity. Starts at the (power-of-two) default and grows by
129
+ * doubling; a caller-supplied non-power-of-two initial capacity is honoured
130
+ * as-is, so this is not guaranteed to be a power of two.
131
+ * @returns {number}
132
+ */
133
+ get capacity() {
134
+ return this.__capacity;
135
+ }
136
+
137
+ /**
138
+ * Number of awake bodies.
139
+ * @returns {number}
140
+ */
141
+ get awake_count() {
142
+ return this.__awake_count;
143
+ }
144
+
145
+ /**
146
+ * High-water mark of slot indices ever issued. Useful when callers maintain
147
+ * per-slot side-tables.
148
+ * @returns {number}
149
+ */
150
+ get high_water_mark() {
151
+ return this.__count;
152
+ }
153
+
154
+ /**
155
+ * Allocate a body slot for `entity`. The new body starts in the awake set
156
+ * with default kind {@link BodyKind.Dynamic} and zero flags; the caller
157
+ * may override these via {@link set_kind} / {@link set_flags}.
158
+ *
159
+ * @param {number} entity
160
+ * @returns {number} packed body id
161
+ */
162
+ allocate(entity) {
163
+ let index;
164
+
165
+ if (this.__free_count > 0) {
166
+ index = this.__heap_pop();
167
+ } else {
168
+ if (this.__count === this.__capacity) {
169
+ this.__grow();
170
+ }
171
+ index = this.__count++;
172
+ }
173
+
174
+ this.__entities[index] = entity;
175
+ this.__kinds[index] = BodyKind.Dynamic;
176
+ this.__flags[index] = 0;
177
+ this.__alive[index] = 1;
178
+ this.__entity_to_index.set(entity, index);
179
+
180
+ // Insert into awake set.
181
+ const awake_pos = this.__awake_count++;
182
+ this.__awake_list[awake_pos] = index;
183
+ this.__awake_pos[index] = awake_pos;
184
+
185
+ return pack_body_id(index, this.__generations[index]);
186
+ }
187
+
188
+ /**
189
+ * Release a body slot previously returned by {@link allocate}. Bumps the
190
+ * generation so any old packed id becomes stale.
191
+ *
192
+ * @param {number} packed_body_id
193
+ */
194
+ free(packed_body_id) {
195
+ const index = body_id_index(packed_body_id);
196
+
197
+ assert.equal(this.is_valid(packed_body_id), true, 'free() called on stale or unknown body id');
198
+
199
+ // Remove from awake set if present.
200
+ const awake_pos = this.__awake_pos[index];
201
+ if (awake_pos !== BODY_INDEX_ABSENT) {
202
+ this.__awake_remove_at(awake_pos);
203
+ this.__awake_pos[index] = BODY_INDEX_ABSENT;
204
+ }
205
+
206
+ this.__alive[index] = 0;
207
+
208
+ // Drop the entity → index mapping (the slot still holds the old entity
209
+ // value until reallocation, so delete by it now while it's valid).
210
+ this.__entity_to_index.delete(this.__entities[index]);
211
+
212
+ // Bump generation; wraps mod 256.
213
+ this.__generations[index] = (this.__generations[index] + 1) & GENERATION_MASK;
214
+
215
+ this.__heap_push(index);
216
+ }
217
+
218
+ /**
219
+ *
220
+ * @param {number} packed_body_id
221
+ * @returns {boolean}
222
+ */
223
+ is_valid(packed_body_id) {
224
+ const index = body_id_index(packed_body_id);
225
+ if (index < 0 || index >= this.__count) {
226
+ return false;
227
+ }
228
+ if (this.__alive[index] !== 1) {
229
+ return false;
230
+ }
231
+ return this.__generations[index] === body_id_generation(packed_body_id);
232
+ }
233
+
234
+ /**
235
+ * @param {number} index body index (NOT a packed id)
236
+ * @returns {number} entity for the body, or -1 if the slot is free.
237
+ */
238
+ entity_at(index) {
239
+ if (this.__alive[index] !== 1) {
240
+ return -1;
241
+ }
242
+ return this.__entities[index];
243
+ }
244
+
245
+ /**
246
+ * Body index for `entity`, or {@link BODY_INDEX_ABSENT} if no live body owns
247
+ * it. O(1) reverse of {@link entity_at} — the lookup callers use on the
248
+ * link / attach / joint paths instead of scanning the slot table.
249
+ * @param {number} entity
250
+ * @returns {number}
251
+ */
252
+ index_of_entity(entity) {
253
+ const idx = this.__entity_to_index.get(entity);
254
+ return idx === undefined ? BODY_INDEX_ABSENT : idx;
255
+ }
256
+
257
+ /**
258
+ * @param {number} index
259
+ * @returns {number}
260
+ */
261
+ generation_at(index) {
262
+ return this.__generations[index];
263
+ }
264
+
265
+ /**
266
+ * @param {number} index
267
+ * @returns {BodyKind|number}
268
+ */
269
+ kind_at(index) {
270
+ return this.__kinds[index];
271
+ }
272
+
273
+ /**
274
+ * @param {number} index
275
+ * @param {BodyKind|number} kind
276
+ */
277
+ set_kind(index, kind) {
278
+ this.__kinds[index] = kind;
279
+ }
280
+
281
+ /**
282
+ * @param {number} index
283
+ * @returns {number}
284
+ */
285
+ flags_at(index) {
286
+ return this.__flags[index];
287
+ }
288
+
289
+ /**
290
+ * @param {number} index
291
+ * @param {number} flags
292
+ */
293
+ set_flags(index, flags) {
294
+ this.__flags[index] = flags;
295
+ }
296
+
297
+ /**
298
+ * Index of `index` in the awake list, or {@link BODY_INDEX_ABSENT} if asleep.
299
+ * @param {number} index
300
+ * @returns {number}
301
+ */
302
+ awake_position_of(index) {
303
+ return this.__awake_pos[index];
304
+ }
305
+
306
+ /**
307
+ *
308
+ * @param {number} index
309
+ * @returns {boolean}
310
+ */
311
+ is_awake(index) {
312
+ return this.__awake_pos[index] !== BODY_INDEX_ABSENT;
313
+ }
314
+
315
+ /**
316
+ * Read the body index at position `i` in the dense awake list (0-based).
317
+ * @param {number} i
318
+ * @returns {number}
319
+ */
320
+ awake_at(i) {
321
+ return this.__awake_list[i];
322
+ }
323
+
324
+ /**
325
+ * Add `index` to the awake set if not already present.
326
+ * @param {number} index
327
+ */
328
+ mark_awake(index) {
329
+ if (this.__awake_pos[index] !== BODY_INDEX_ABSENT) {
330
+ return;
331
+ }
332
+ const pos = this.__awake_count++;
333
+ this.__awake_list[pos] = index;
334
+ this.__awake_pos[index] = pos;
335
+ }
336
+
337
+ /**
338
+ * Remove `index` from the awake set if present.
339
+ * @param {number} index
340
+ */
341
+ mark_sleeping(index) {
342
+ const pos = this.__awake_pos[index];
343
+ if (pos === BODY_INDEX_ABSENT) {
344
+ return;
345
+ }
346
+ this.__awake_remove_at(pos);
347
+ this.__awake_pos[index] = BODY_INDEX_ABSENT;
348
+ }
349
+
350
+ /**
351
+ * Swap-with-last removal at position `pos` in the dense awake list.
352
+ * @private
353
+ * @param {number} pos
354
+ */
355
+ __awake_remove_at(pos) {
356
+ const last_pos = --this.__awake_count;
357
+ if (pos !== last_pos) {
358
+ const swapped_index = this.__awake_list[last_pos];
359
+ this.__awake_list[pos] = swapped_index;
360
+ this.__awake_pos[swapped_index] = pos;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * @private
366
+ */
367
+ __grow() {
368
+ const new_cap = this.__capacity << 1;
369
+ this.__capacity = new_cap;
370
+
371
+ const grow_int32 = (a) => {
372
+ const next = new Int32Array(new_cap);
373
+ next.set(a);
374
+ return next;
375
+ };
376
+ const grow_uint8 = (a) => {
377
+ const next = new Uint8Array(new_cap);
378
+ next.set(a);
379
+ return next;
380
+ };
381
+ const grow_uint32 = (a) => {
382
+ const next = new Uint32Array(new_cap);
383
+ next.set(a);
384
+ return next;
385
+ };
386
+
387
+ this.__entities = grow_int32(this.__entities);
388
+ this.__generations = grow_uint8(this.__generations);
389
+ this.__kinds = grow_uint8(this.__kinds);
390
+ this.__flags = grow_uint32(this.__flags);
391
+ this.__alive = grow_uint8(this.__alive);
392
+ this.__awake_list = grow_uint32(this.__awake_list);
393
+ this.__free_heap = grow_uint32(this.__free_heap);
394
+
395
+ // Awake position has signed sentinel — needs fill on the grown portion.
396
+ const next_pos = new Int32Array(new_cap);
397
+ next_pos.set(this.__awake_pos);
398
+ next_pos.fill(BODY_INDEX_ABSENT, this.__awake_pos.length);
399
+ this.__awake_pos = next_pos;
400
+ }
401
+
402
+ // --- Min-heap of free indices --------------------------------------------
403
+
404
+ /**
405
+ * @private
406
+ * @param {number} index
407
+ */
408
+ __heap_push(index) {
409
+ const heap = this.__free_heap;
410
+ let i = this.__free_count++;
411
+ heap[i] = index;
412
+
413
+ while (i > 0) {
414
+ const parent = (i - 1) >>> 1;
415
+ if (heap[parent] > heap[i]) {
416
+ const tmp = heap[parent];
417
+ heap[parent] = heap[i];
418
+ heap[i] = tmp;
419
+ i = parent;
420
+ } else {
421
+ break;
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * @private
428
+ * @returns {number}
429
+ */
430
+ __heap_pop() {
431
+ const heap = this.__free_heap;
432
+ const result = heap[0];
433
+ const new_count = --this.__free_count;
434
+ if (new_count > 0) {
435
+ heap[0] = heap[new_count];
436
+ let i = 0;
437
+ while (true) {
438
+ const l = (i << 1) + 1;
439
+ const r = l + 1;
440
+ let smallest = i;
441
+ if (l < new_count && heap[l] < heap[smallest]) smallest = l;
442
+ if (r < new_count && heap[r] < heap[smallest]) smallest = r;
443
+ if (smallest === i) break;
444
+ const tmp = heap[i];
445
+ heap[i] = heap[smallest];
446
+ heap[smallest] = tmp;
447
+ i = smallest;
448
+ }
449
+ }
450
+ return result;
451
+ }
452
+ }