@woosh/meep-engine 2.153.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 (96) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  3. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  4. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  5. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  6. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  7. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  8. package/src/engine/physics/PLAN.md +4 -4
  9. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  10. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  11. package/src/engine/physics/body/BodyStorage.js +452 -450
  12. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  13. package/src/engine/physics/body/SolverBodyState.js +6 -5
  14. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  15. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  16. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  17. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  18. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  19. package/src/engine/physics/computeInterceptPoint.js +8 -3
  20. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  21. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  22. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  23. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  24. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  25. package/src/engine/physics/ecs/BodyKind.js +25 -24
  26. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  27. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  29. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  30. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  32. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  33. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  34. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  35. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  36. package/src/engine/physics/ecs/is_sensor.js +27 -0
  37. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  38. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  39. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  40. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  41. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  42. package/src/engine/physics/gjk/gjk.js +3 -52
  43. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  44. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  45. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  46. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  47. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  48. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  49. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  50. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/mpr.js +362 -344
  52. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  53. package/src/engine/physics/island/IslandBuilder.js +431 -428
  54. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  55. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  56. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  57. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  58. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  59. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  60. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  61. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  62. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  63. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  64. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  65. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  66. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  68. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  69. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  71. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  72. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  74. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  75. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  76. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  77. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  78. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  79. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  80. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  81. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  82. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  83. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  84. package/src/engine/physics/queries/overlap_shape.js +16 -17
  85. package/src/engine/physics/queries/raycast.d.ts +5 -0
  86. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  87. package/src/engine/physics/queries/raycast.js +16 -8
  88. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  89. package/src/engine/physics/queries/shape_cast.js +13 -7
  90. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  91. package/src/engine/physics/solver/solve_contacts.js +8 -11
  92. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  93. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  94. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  95. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  96. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1,428 +1,431 @@
1
- import { ceilPowerOfTwo } from "../../../core/binary/operations/ceilPowerOfTwo.js";
2
- import { body_id_index } from "../body/BodyStorage.js";
3
- import { BodyKind } from "../ecs/BodyKind.js";
4
- import { ColliderFlags } from "../ecs/ColliderFlags.js";
5
- import { JOINT_WORLD } from "../ecs/Joint.js";
6
- import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
7
- import { uf_find, uf_init, uf_union } from "./union_find.js";
8
-
9
- /**
10
- * Partitions this frame's awake dynamic bodies + touched non-sensor contacts
11
- * into connected components ("islands") via union-find. The solver iterates
12
- * each island independently (so impulses converge inside an island without
13
- * waiting for global passes) and the sleep test will eventually use island
14
- * granularity for atomic whole-island sleep.
15
- *
16
- * Static and Kinematic bodies are not merged into islands they act as
17
- * constraint anchors. A 1000-block stack on a static floor is therefore one
18
- * island of 1000 dynamic bodies, not 1001 the floor anchors the island but
19
- * does not enlarge it. Multiple separate piles all resting on the same static
20
- * floor remain *separate* islands so each can sleep/wake independently.
21
- *
22
- * Sensor contacts are skipped entirely: they don't transmit constraint
23
- * forces, so two bodies linked only by a sensor pair are not in the same
24
- * structural island.
25
- *
26
- * Determinism contract:
27
- * - Union-find uses union-by-min-index + path halving, so the canonical
28
- * root of any component is the smallest body index of any of its members.
29
- * - Islands are emitted sorted ascending by root index.
30
- * - Bodies within an island are emitted sorted ascending by body index.
31
- * - Contacts within an island are emitted sorted ascending by manifold slot id.
32
- *
33
- * Output layout is CSR-like `body_offsets[i]..body_offsets[i+1]` indexes
34
- * into `body_data` for island `i`, similarly for contacts.
35
- *
36
- * @author Alex Goldring
37
- * @copyright Company Named Limited (c) 2026
38
- */
39
- export class IslandBuilder {
40
-
41
- constructor() {
42
- /**
43
- * Union-find parent table, indexed by body index (NOT packed id).
44
- * Sized to `storage.high_water_mark` at `build` time; reallocated by
45
- * doubling when the body pool grows past current capacity.
46
- * @type {Uint32Array}
47
- */
48
- this.parent = new Uint32Array(16);
49
-
50
- /**
51
- * Per-body island id. `island_of_body[idx] === -1` means the body
52
- * is not in any island this frame (static, kinematic, or unallocated).
53
- * @type {Int32Array}
54
- */
55
- this.island_of_body = new Int32Array(16);
56
-
57
- /**
58
- * Number of islands this frame.
59
- * @type {number}
60
- */
61
- this.island_count = 0;
62
-
63
- /**
64
- * CSR offsets into `body_data`. Length is `island_count + 1`. Bodies
65
- * for island `i` live in `body_data[body_offsets[i]..body_offsets[i+1])`.
66
- * @type {Uint32Array}
67
- */
68
- this.body_offsets = new Uint32Array(2);
69
-
70
- /**
71
- * Flattened body indices, grouped by island, sorted ascending within
72
- * each island.
73
- * @type {Uint32Array}
74
- */
75
- this.body_data = new Uint32Array(16);
76
-
77
- /**
78
- * Total number of body entries across all islands (= sum of island sizes).
79
- * @type {number}
80
- */
81
- this.body_total = 0;
82
-
83
- /**
84
- * CSR offsets into `contact_data`. Length is `island_count + 1`.
85
- * Manifold slot ids for island `i` live in
86
- * `contact_data[contact_offsets[i]..contact_offsets[i+1])`.
87
- * @type {Uint32Array}
88
- */
89
- this.contact_offsets = new Uint32Array(2);
90
-
91
- /**
92
- * Flattened manifold slot ids, grouped by island, sorted ascending
93
- * within each island.
94
- * @type {Uint32Array}
95
- */
96
- this.contact_data = new Uint32Array(16);
97
-
98
- /**
99
- * Total number of contact entries across all islands.
100
- * @type {number}
101
- */
102
- this.contact_total = 0;
103
-
104
- /**
105
- * Scratch: root body index → island id, or -1 if unassigned.
106
- * Sized in lockstep with `parent`.
107
- * @private
108
- * @type {Int32Array}
109
- */
110
- this.__root_to_island = new Int32Array(16);
111
-
112
- /**
113
- * Scratch: list of distinct root indices observed this frame, used
114
- * to sort them ascending before assigning island ids.
115
- * @private
116
- * @type {Uint32Array}
117
- */
118
- this.__scratch_roots = new Uint32Array(16);
119
-
120
- /**
121
- * Scratch write-cursors, one per island.
122
- * @private
123
- * @type {Uint32Array}
124
- */
125
- this.__cursors = new Uint32Array(2);
126
- }
127
-
128
- /**
129
- * @param {RigidBody} rb
130
- * @param {Array} collider_list collider list for the body (or undefined)
131
- * @returns {boolean}
132
- * @private
133
- */
134
- static __body_is_sensor(rb, collider_list) {
135
- if ((rb.flags & RigidBodyFlags.IsSensor) !== 0) return true;
136
- if (collider_list !== undefined && collider_list.length > 0) {
137
- // v1 approximation: any collider on the body marked as sensor
138
- // turns the whole body's contacts into sensor contacts for the
139
- // purposes of island building. Matches the solver's
140
- // pair_is_sensor check on the primary collider.
141
- if ((collider_list[0].collider.flags & ColliderFlags.IsSensor) !== 0) return true;
142
- }
143
- return false;
144
- }
145
-
146
- /**
147
- * (Re)build islands from `manifolds`' currently-touched non-sensor slots
148
- * and the bodies in `storage`'s awake list.
149
- *
150
- * @param {BodyStorage} storage
151
- * @param {ManifoldStore} manifolds
152
- * @param {RigidBody[]} bodies sparse, indexed by body index
153
- * @param {Array[]} body_collider_lists sparse, indexed by body index
154
- * @param {Joint[]} joints live joints (sparse). Jointed dynamic-dynamic
155
- * bodies are unioned into the same island so a chain / ragdoll sleeps
156
- * and wakes as a unit. Callers with no joints pass an empty array.
157
- */
158
- build(storage, manifolds, bodies, body_collider_lists, joints) {
159
- const hwm = storage.high_water_mark;
160
- this.__ensure_body_capacity(hwm);
161
-
162
- const parent = this.parent;
163
- uf_init(parent, hwm);
164
-
165
- // --- Pass 1: union dynamic-dynamic pairs from touched non-sensor manifolds.
166
- const live_count = manifolds.count;
167
- for (let i = 0; i < live_count; i++) {
168
- const slot = manifolds.live_at(i);
169
- if (!manifolds.is_touched(slot)) continue;
170
- const idxA = body_id_index(manifolds.bodyA(slot));
171
- const idxB = body_id_index(manifolds.bodyB(slot));
172
- const rbA = bodies[idxA];
173
- const rbB = bodies[idxB];
174
- if (rbA === undefined || rbB === undefined) continue;
175
- if (IslandBuilder.__body_is_sensor(rbA, body_collider_lists[idxA])) continue;
176
- if (IslandBuilder.__body_is_sensor(rbB, body_collider_lists[idxB])) continue;
177
- if (rbA.kind === BodyKind.Dynamic && rbB.kind === BodyKind.Dynamic) {
178
- uf_union(parent, idxA, idxB);
179
- }
180
- }
181
-
182
- // --- Pass 1b: union dynamic-dynamic bodies connected by a joint, so a
183
- // chain / ragdoll forms one island (and so sleeps/wakes as a unit).
184
- // Joint-to-world and joint-to-static/kinematic anchor the island
185
- // without enlarging it — same rule as a static contact. Stale joint
186
- // references (body unlinked / slot reused) are filtered by the
187
- // generation-checked `is_valid`. (Empty when the caller has no
188
- // joints the loop is then a no-op.)
189
- const jn = joints.length;
190
- for (let i = 0; i < jn; i++) {
191
- const joint = joints[i];
192
- if (joint === undefined || joint === null) continue;
193
- if (joint._bodyIdB === JOINT_WORLD) continue;
194
- if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
195
- const idxA = body_id_index(joint._bodyIdA);
196
- const idxB = body_id_index(joint._bodyIdB);
197
- const rbA = bodies[idxA];
198
- const rbB = bodies[idxB];
199
- if (rbA === undefined || rbB === undefined) continue;
200
- if (rbA.kind === BodyKind.Dynamic && rbB.kind === BodyKind.Dynamic) {
201
- uf_union(parent, idxA, idxB);
202
- }
203
- }
204
-
205
- // --- Pass 2: collect distinct roots over awake dynamic bodies.
206
- const island_of_body = this.island_of_body;
207
- // Cheap reset: only the indices we may write are below hwm.
208
- for (let i = 0; i < hwm; i++) island_of_body[i] = -1;
209
-
210
- const root_to_island = this.__root_to_island;
211
- for (let i = 0; i < hwm; i++) root_to_island[i] = -1;
212
-
213
- const awake_count = storage.awake_count;
214
- const scratch_roots = this.__scratch_roots;
215
- let root_count = 0;
216
- for (let ai = 0; ai < awake_count; ai++) {
217
- const idx = storage.awake_at(ai);
218
- const rb = bodies[idx];
219
- if (rb === undefined) continue;
220
- if (rb.kind !== BodyKind.Dynamic) continue;
221
- const r = uf_find(parent, idx);
222
- if (root_to_island[r] === -1) {
223
- scratch_roots[root_count++] = r;
224
- // Tag as collected — actual id assigned after sort.
225
- root_to_island[r] = -2;
226
- }
227
- }
228
-
229
- // Sort distinct roots ascending so island id 0 has the smallest root.
230
- if (root_count > 1) {
231
- scratch_roots.subarray(0, root_count).sort();
232
- }
233
- for (let i = 0; i < root_count; i++) {
234
- root_to_island[scratch_roots[i]] = i;
235
- }
236
- this.island_count = root_count;
237
-
238
- // Assign island id to every awake dynamic body.
239
- for (let ai = 0; ai < awake_count; ai++) {
240
- const idx = storage.awake_at(ai);
241
- const rb = bodies[idx];
242
- if (rb === undefined) continue;
243
- if (rb.kind !== BodyKind.Dynamic) continue;
244
- const r = uf_find(parent, idx);
245
- island_of_body[idx] = root_to_island[r];
246
- }
247
-
248
- this.__build_body_csr(storage, bodies, awake_count, root_count);
249
- this.__build_contact_csr(manifolds, bodies, body_collider_lists, root_count);
250
- }
251
-
252
- /**
253
- * Fill `body_offsets` + `body_data` with awake dynamic bodies grouped
254
- * by island, sorted ascending within each island.
255
- * @private
256
- */
257
- __build_body_csr(storage, bodies, awake_count, island_count) {
258
- this.__ensure_island_count_capacity(island_count);
259
- const offsets = this.body_offsets;
260
- for (let i = 0; i <= island_count; i++) offsets[i] = 0;
261
-
262
- // Count pass: offsets[i + 1] starts as the number of bodies in island i.
263
- let total = 0;
264
- for (let ai = 0; ai < awake_count; ai++) {
265
- const idx = storage.awake_at(ai);
266
- const rb = bodies[idx];
267
- if (rb === undefined) continue;
268
- if (rb.kind !== BodyKind.Dynamic) continue;
269
- const isl = this.island_of_body[idx];
270
- if (isl < 0) continue;
271
- offsets[isl + 1]++;
272
- total++;
273
- }
274
- // Prefix sum → start offsets.
275
- for (let i = 0; i < island_count; i++) {
276
- offsets[i + 1] += offsets[i];
277
- }
278
- this.body_total = total;
279
-
280
- this.__ensure_body_data_capacity(total);
281
-
282
- const cursors = this.__cursors;
283
- for (let i = 0; i < island_count; i++) cursors[i] = offsets[i];
284
-
285
- const data = this.body_data;
286
- for (let ai = 0; ai < awake_count; ai++) {
287
- const idx = storage.awake_at(ai);
288
- const rb = bodies[idx];
289
- if (rb === undefined) continue;
290
- if (rb.kind !== BodyKind.Dynamic) continue;
291
- const isl = this.island_of_body[idx];
292
- if (isl < 0) continue;
293
- data[cursors[isl]++] = idx;
294
- }
295
-
296
- // Sort each island's body slice ascending.
297
- for (let i = 0; i < island_count; i++) {
298
- const start = offsets[i];
299
- const end = offsets[i + 1];
300
- if (end - start > 1) {
301
- data.subarray(start, end).sort();
302
- }
303
- }
304
- }
305
-
306
- /**
307
- * Fill `contact_offsets` + `contact_data` with touched non-sensor manifold
308
- * slot ids grouped by island. A contact belongs to its dynamic
309
- * participant's island (if both sides are dynamic they share an island
310
- * by construction). Static-vs-static contacts are skipped.
311
- * @private
312
- */
313
- __build_contact_csr(manifolds, bodies, body_collider_lists, island_count) {
314
- const offsets = this.contact_offsets;
315
- for (let i = 0; i <= island_count; i++) offsets[i] = 0;
316
-
317
- // Count pass.
318
- const live_count = manifolds.count;
319
- let total = 0;
320
- for (let i = 0; i < live_count; i++) {
321
- const slot = manifolds.live_at(i);
322
- const isl = this.__island_of_slot(manifolds, slot, bodies, body_collider_lists);
323
- if (isl < 0) continue;
324
- offsets[isl + 1]++;
325
- total++;
326
- }
327
- for (let i = 0; i < island_count; i++) {
328
- offsets[i + 1] += offsets[i];
329
- }
330
- this.contact_total = total;
331
-
332
- this.__ensure_contact_data_capacity(total);
333
-
334
- const cursors = this.__cursors;
335
- for (let i = 0; i < island_count; i++) cursors[i] = offsets[i];
336
-
337
- const data = this.contact_data;
338
- for (let i = 0; i < live_count; i++) {
339
- const slot = manifolds.live_at(i);
340
- const isl = this.__island_of_slot(manifolds, slot, bodies, body_collider_lists);
341
- if (isl < 0) continue;
342
- data[cursors[isl]++] = slot;
343
- }
344
-
345
- // Sort each island's contact slice ascending by slot id.
346
- for (let i = 0; i < island_count; i++) {
347
- const start = offsets[i];
348
- const end = offsets[i + 1];
349
- if (end - start > 1) {
350
- data.subarray(start, end).sort();
351
- }
352
- }
353
- }
354
-
355
- /**
356
- * Resolve which island a manifold slot belongs to.
357
- *
358
- * Returns `-1` if the manifold is not touched this frame, is a sensor
359
- * pair, or both bodies are non-dynamic (static-static, which the solver
360
- * wouldn't act on anyway).
361
- *
362
- * @private
363
- */
364
- __island_of_slot(manifolds, slot, bodies, body_collider_lists) {
365
- if (!manifolds.is_touched(slot)) return -1;
366
- const idxA = body_id_index(manifolds.bodyA(slot));
367
- const idxB = body_id_index(manifolds.bodyB(slot));
368
- const rbA = bodies[idxA];
369
- const rbB = bodies[idxB];
370
- if (rbA === undefined || rbB === undefined) return -1;
371
- if (IslandBuilder.__body_is_sensor(rbA, body_collider_lists[idxA])) return -1;
372
- if (IslandBuilder.__body_is_sensor(rbB, body_collider_lists[idxB])) return -1;
373
- // Pick the dynamic side's island. If both are dynamic they share
374
- // one by construction (pass-1 union).
375
- if (rbA.kind === BodyKind.Dynamic) return this.island_of_body[idxA];
376
- if (rbB.kind === BodyKind.Dynamic) return this.island_of_body[idxB];
377
- return -1;
378
- }
379
-
380
- // --- Resize helpers ------------------------------------------------------
381
-
382
- /**
383
- * @private
384
- * @param {number} n
385
- */
386
- __ensure_body_capacity(n) {
387
- if (this.parent.length < n) {
388
- const cap = ceilPowerOfTwo(n);
389
- this.parent = new Uint32Array(cap);
390
- this.island_of_body = new Int32Array(cap);
391
- this.__root_to_island = new Int32Array(cap);
392
- this.__scratch_roots = new Uint32Array(cap);
393
- }
394
- }
395
-
396
- /**
397
- * @private
398
- * @param {number} n
399
- */
400
- __ensure_island_count_capacity(n) {
401
- if (this.body_offsets.length < n + 1) {
402
- const cap = ceilPowerOfTwo(n + 1);
403
- this.body_offsets = new Uint32Array(cap);
404
- this.contact_offsets = new Uint32Array(cap);
405
- this.__cursors = new Uint32Array(cap);
406
- }
407
- }
408
-
409
- /**
410
- * @private
411
- * @param {number} n
412
- */
413
- __ensure_body_data_capacity(n) {
414
- if (this.body_data.length < n) {
415
- this.body_data = new Uint32Array(ceilPowerOfTwo(n));
416
- }
417
- }
418
-
419
- /**
420
- * @private
421
- * @param {number} n
422
- */
423
- __ensure_contact_data_capacity(n) {
424
- if (this.contact_data.length < n) {
425
- this.contact_data = new Uint32Array(ceilPowerOfTwo(n));
426
- }
427
- }
428
- }
1
+ import { ceilPowerOfTwo } from "../../../core/binary/operations/ceilPowerOfTwo.js";
2
+ import { body_id_index } from "../body/BodyStorage.js";
3
+ import { BodyKind } from "../ecs/BodyKind.js";
4
+ import { is_sensor } from "../ecs/is_sensor.js";
5
+ import { JOINT_WORLD } from "../ecs/Joint.js";
6
+ import { uf_find, uf_init, uf_union } from "./union_find.js";
7
+
8
+ /**
9
+ * Partitions this frame's awake dynamic bodies + touched non-sensor contacts
10
+ * into connected components ("islands") via union-find. The solver iterates
11
+ * each island independently (so impulses converge inside an island without
12
+ * waiting for global passes) and the sleep test will eventually use island
13
+ * granularity for atomic whole-island sleep.
14
+ *
15
+ * Static and Kinematic bodies are not merged into islands — they act as
16
+ * constraint anchors. A 1000-block stack on a static floor is therefore one
17
+ * island of 1000 dynamic bodies, not 1001 — the floor anchors the island but
18
+ * does not enlarge it. Multiple separate piles all resting on the same static
19
+ * floor remain *separate* islands so each can sleep/wake independently.
20
+ *
21
+ * Sensor contacts are skipped entirely: they don't transmit constraint
22
+ * forces, so two bodies linked only by a sensor pair are not in the same
23
+ * structural island.
24
+ *
25
+ * Determinism contract:
26
+ * - Union-find uses union-by-min-index + path halving, so the canonical
27
+ * root of any component is the smallest body index of any of its members.
28
+ * - Islands are emitted sorted ascending by root index.
29
+ * - Bodies within an island are emitted sorted ascending by body index.
30
+ * - Contacts within an island are emitted sorted ascending by manifold slot id.
31
+ *
32
+ * Output layout is CSR-like — `body_offsets[i]..body_offsets[i+1]` indexes
33
+ * into `body_data` for island `i`, similarly for contacts.
34
+ *
35
+ * @author Alex Goldring
36
+ * @copyright Company Named Limited (c) 2026
37
+ */
38
+ export class IslandBuilder {
39
+
40
+ constructor() {
41
+ /**
42
+ * Union-find parent table, indexed by body index (NOT packed id).
43
+ * Sized to `storage.high_water_mark` at `build` time; reallocated by
44
+ * doubling when the body pool grows past current capacity.
45
+ * @type {Uint32Array}
46
+ */
47
+ this.parent = new Uint32Array(16);
48
+
49
+ /**
50
+ * Per-body island id. `island_of_body[idx] === -1` means the body
51
+ * is not in any island this frame (static, kinematic, or unallocated).
52
+ * @type {Int32Array}
53
+ */
54
+ this.island_of_body = new Int32Array(16);
55
+
56
+ /**
57
+ * Number of islands this frame.
58
+ * @type {number}
59
+ */
60
+ this.island_count = 0;
61
+
62
+ /**
63
+ * CSR offsets into `body_data`. Length is `island_count + 1`. Bodies
64
+ * for island `i` live in `body_data[body_offsets[i]..body_offsets[i+1])`.
65
+ * @type {Uint32Array}
66
+ */
67
+ this.body_offsets = new Uint32Array(2);
68
+
69
+ /**
70
+ * Flattened body indices, grouped by island, sorted ascending within
71
+ * each island.
72
+ * @type {Uint32Array}
73
+ */
74
+ this.body_data = new Uint32Array(16);
75
+
76
+ /**
77
+ * Total number of body entries across all islands (= sum of island sizes).
78
+ * @type {number}
79
+ */
80
+ this.body_total = 0;
81
+
82
+ /**
83
+ * CSR offsets into `contact_data`. Length is `island_count + 1`.
84
+ * Manifold slot ids for island `i` live in
85
+ * `contact_data[contact_offsets[i]..contact_offsets[i+1])`.
86
+ * @type {Uint32Array}
87
+ */
88
+ this.contact_offsets = new Uint32Array(2);
89
+
90
+ /**
91
+ * Flattened manifold slot ids, grouped by island, sorted ascending
92
+ * within each island.
93
+ * @type {Uint32Array}
94
+ */
95
+ this.contact_data = new Uint32Array(16);
96
+
97
+ /**
98
+ * Total number of contact entries across all islands.
99
+ * @type {number}
100
+ */
101
+ this.contact_total = 0;
102
+
103
+ /**
104
+ * Scratch: root body index → island id, or -1 if unassigned.
105
+ * Sized in lockstep with `parent`.
106
+ * @private
107
+ * @type {Int32Array}
108
+ */
109
+ this.__root_to_island = new Int32Array(16);
110
+
111
+ /**
112
+ * Scratch: list of distinct root indices observed this frame, used
113
+ * to sort them ascending before assigning island ids.
114
+ * @private
115
+ * @type {Uint32Array}
116
+ */
117
+ this.__scratch_roots = new Uint32Array(16);
118
+
119
+ /**
120
+ * Scratch write-cursors, one per island.
121
+ * @private
122
+ * @type {Uint32Array}
123
+ */
124
+ this.__cursors = new Uint32Array(2);
125
+ }
126
+
127
+ /**
128
+ * @param {RigidBody} rb
129
+ * @param {Array} collider_list collider list for the body (or undefined)
130
+ * @returns {boolean}
131
+ * @private
132
+ */
133
+ static __body_is_sensor(rb, collider_list) {
134
+ // v1 approximation: the body's primary (first) collider stands in for
135
+ // the whole body, matching the solver's pair_is_sensor / raycast checks.
136
+ const collider = (collider_list !== undefined && collider_list.length > 0)
137
+ ? collider_list[0].collider
138
+ : null;
139
+ return is_sensor(rb, collider);
140
+ }
141
+
142
+ /**
143
+ * (Re)build islands from `manifolds`' currently-touched non-sensor slots
144
+ * and the bodies in `storage`'s awake list.
145
+ *
146
+ * @param {BodyStorage} storage
147
+ * @param {ManifoldStore} manifolds
148
+ * @param {RigidBody[]} bodies sparse, indexed by body index
149
+ * @param {Array[]} body_collider_lists sparse, indexed by body index
150
+ * @param {Joint[]} joints live joints (sparse). Jointed dynamic-dynamic
151
+ * bodies are unioned into the same island so a chain / ragdoll sleeps
152
+ * and wakes as a unit. Callers with no joints pass an empty array.
153
+ */
154
+ build(storage, manifolds, bodies, body_collider_lists, joints) {
155
+ const hwm = storage.high_water_mark;
156
+ this.__ensure_body_capacity(hwm);
157
+
158
+ const parent = this.parent;
159
+ uf_init(parent, hwm);
160
+
161
+ // --- Pass 1: union dynamic-dynamic pairs from touched non-sensor manifolds.
162
+ const live_count = manifolds.count;
163
+ for (let i = 0; i < live_count; i++) {
164
+ const slot = manifolds.live_at(i);
165
+ if (!manifolds.is_touched(slot)) continue;
166
+ const idxA = body_id_index(manifolds.bodyA(slot));
167
+ const idxB = body_id_index(manifolds.bodyB(slot));
168
+ const rbA = bodies[idxA];
169
+ const rbB = bodies[idxB];
170
+ if (rbA === undefined || rbB === undefined) continue;
171
+ if (IslandBuilder.__body_is_sensor(rbA, body_collider_lists[idxA])) continue;
172
+ if (IslandBuilder.__body_is_sensor(rbB, body_collider_lists[idxB])) continue;
173
+ if (rbA.kind === BodyKind.Dynamic && rbB.kind === BodyKind.Dynamic) {
174
+ uf_union(parent, idxA, idxB);
175
+ }
176
+ }
177
+
178
+ // --- Pass 1b: union dynamic-dynamic bodies connected by a joint, so a
179
+ // chain / ragdoll forms one island (and so sleeps/wakes as a unit).
180
+ // Joint-to-world and joint-to-static/kinematic anchor the island
181
+ // without enlarging it — same rule as a static contact. Stale joint
182
+ // references (body unlinked / slot reused) are filtered by the
183
+ // generation-checked `is_valid`. (Empty when the caller has no
184
+ // joints the loop is then a no-op.)
185
+ const jn = joints.length;
186
+ for (let i = 0; i < jn; i++) {
187
+ const joint = joints[i];
188
+ if (joint === undefined || joint === null) continue;
189
+ if (joint._bodyIdB === JOINT_WORLD) continue;
190
+ if (!storage.is_valid(joint._bodyIdA) || !storage.is_valid(joint._bodyIdB)) continue;
191
+ const idxA = body_id_index(joint._bodyIdA);
192
+ const idxB = body_id_index(joint._bodyIdB);
193
+ const rbA = bodies[idxA];
194
+ const rbB = bodies[idxB];
195
+ if (rbA === undefined || rbB === undefined) continue;
196
+ if (rbA.kind === BodyKind.Dynamic && rbB.kind === BodyKind.Dynamic) {
197
+ uf_union(parent, idxA, idxB);
198
+ }
199
+ }
200
+
201
+ // --- Pass 2: collect distinct roots over awake dynamic bodies.
202
+ const island_of_body = this.island_of_body;
203
+ // Cheap reset: only the indices we may write are below hwm.
204
+ for (let i = 0; i < hwm; i++) island_of_body[i] = -1;
205
+
206
+ const root_to_island = this.__root_to_island;
207
+ for (let i = 0; i < hwm; i++) root_to_island[i] = -1;
208
+
209
+ const awake_count = storage.awake_count;
210
+ const scratch_roots = this.__scratch_roots;
211
+ let root_count = 0;
212
+ for (let ai = 0; ai < awake_count; ai++) {
213
+ const idx = storage.awake_at(ai);
214
+ const rb = bodies[idx];
215
+ if (rb === undefined) continue;
216
+ if (rb.kind !== BodyKind.Dynamic) continue;
217
+ const r = uf_find(parent, idx);
218
+ if (root_to_island[r] === -1) {
219
+ scratch_roots[root_count++] = r;
220
+ // Tag as collected — actual id assigned after sort.
221
+ root_to_island[r] = -2;
222
+ }
223
+ }
224
+
225
+ // Sort distinct roots ascending so island id 0 has the smallest root.
226
+ if (root_count > 1) {
227
+ scratch_roots.subarray(0, root_count).sort();
228
+ }
229
+ for (let i = 0; i < root_count; i++) {
230
+ root_to_island[scratch_roots[i]] = i;
231
+ }
232
+ this.island_count = root_count;
233
+
234
+ // Assign island id to every awake dynamic body.
235
+ for (let ai = 0; ai < awake_count; ai++) {
236
+ const idx = storage.awake_at(ai);
237
+ const rb = bodies[idx];
238
+ if (rb === undefined) continue;
239
+ if (rb.kind !== BodyKind.Dynamic) continue;
240
+ const r = uf_find(parent, idx);
241
+ island_of_body[idx] = root_to_island[r];
242
+ }
243
+
244
+ this.__build_body_csr(storage, bodies, awake_count, root_count);
245
+ this.__build_contact_csr(manifolds, bodies, body_collider_lists, root_count);
246
+ }
247
+
248
+ /**
249
+ * Fill `body_offsets` + `body_data` with awake dynamic bodies grouped
250
+ * by island, sorted ascending within each island.
251
+ * @private
252
+ */
253
+ __build_body_csr(storage, bodies, awake_count, island_count) {
254
+ this.__ensure_island_count_capacity(island_count);
255
+ const offsets = this.body_offsets;
256
+ for (let i = 0; i <= island_count; i++) offsets[i] = 0;
257
+
258
+ // Count pass: offsets[i + 1] starts as the number of bodies in island i.
259
+ let total = 0;
260
+ for (let ai = 0; ai < awake_count; ai++) {
261
+ const idx = storage.awake_at(ai);
262
+ const rb = bodies[idx];
263
+ if (rb === undefined) continue;
264
+ if (rb.kind !== BodyKind.Dynamic) continue;
265
+ const isl = this.island_of_body[idx];
266
+ if (isl < 0) continue;
267
+ offsets[isl + 1]++;
268
+ total++;
269
+ }
270
+ // Prefix sum start offsets.
271
+ for (let i = 0; i < island_count; i++) {
272
+ offsets[i + 1] += offsets[i];
273
+ }
274
+ this.body_total = total;
275
+
276
+ this.__ensure_body_data_capacity(total);
277
+
278
+ const cursors = this.__cursors;
279
+ for (let i = 0; i < island_count; i++) cursors[i] = offsets[i];
280
+
281
+ const data = this.body_data;
282
+ for (let ai = 0; ai < awake_count; ai++) {
283
+ const idx = storage.awake_at(ai);
284
+ const rb = bodies[idx];
285
+ if (rb === undefined) continue;
286
+ if (rb.kind !== BodyKind.Dynamic) continue;
287
+ const isl = this.island_of_body[idx];
288
+ if (isl < 0) continue;
289
+ data[cursors[isl]++] = idx;
290
+ }
291
+
292
+ // Sort each island's body slice ascending.
293
+ for (let i = 0; i < island_count; i++) {
294
+ const start = offsets[i];
295
+ const end = offsets[i + 1];
296
+ if (end - start > 1) {
297
+ data.subarray(start, end).sort();
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Fill `contact_offsets` + `contact_data` with touched non-sensor manifold
304
+ * slot ids grouped by island. A contact belongs to its dynamic
305
+ * participant's island (if both sides are dynamic they share an island
306
+ * by construction). Static-vs-static contacts are skipped.
307
+ * @private
308
+ */
309
+ __build_contact_csr(manifolds, bodies, body_collider_lists, island_count) {
310
+ const offsets = this.contact_offsets;
311
+ for (let i = 0; i <= island_count; i++) offsets[i] = 0;
312
+
313
+ // Count pass.
314
+ const live_count = manifolds.count;
315
+ let total = 0;
316
+ for (let i = 0; i < live_count; i++) {
317
+ const slot = manifolds.live_at(i);
318
+ const isl = this.__island_of_slot(manifolds, slot, bodies, body_collider_lists);
319
+ if (isl < 0) continue;
320
+ offsets[isl + 1]++;
321
+ total++;
322
+ }
323
+ for (let i = 0; i < island_count; i++) {
324
+ offsets[i + 1] += offsets[i];
325
+ }
326
+ this.contact_total = total;
327
+
328
+ this.__ensure_contact_data_capacity(total);
329
+
330
+ const cursors = this.__cursors;
331
+ for (let i = 0; i < island_count; i++) cursors[i] = offsets[i];
332
+
333
+ const data = this.contact_data;
334
+ for (let i = 0; i < live_count; i++) {
335
+ const slot = manifolds.live_at(i);
336
+ const isl = this.__island_of_slot(manifolds, slot, bodies, body_collider_lists);
337
+ if (isl < 0) continue;
338
+ data[cursors[isl]++] = slot;
339
+ }
340
+
341
+ // Sort each island's contact slice ascending by slot id.
342
+ for (let i = 0; i < island_count; i++) {
343
+ const start = offsets[i];
344
+ const end = offsets[i + 1];
345
+ if (end - start > 1) {
346
+ data.subarray(start, end).sort();
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Resolve which island a manifold slot belongs to.
353
+ *
354
+ * Returns `-1` if the manifold is not touched this frame, is a sensor
355
+ * pair, or both bodies are non-dynamic (static-static, which the solver
356
+ * wouldn't act on anyway).
357
+ *
358
+ * @private
359
+ */
360
+ __island_of_slot(manifolds, slot, bodies, body_collider_lists) {
361
+ if (!manifolds.is_touched(slot)) return -1;
362
+ const idxA = body_id_index(manifolds.bodyA(slot));
363
+ const idxB = body_id_index(manifolds.bodyB(slot));
364
+ const rbA = bodies[idxA];
365
+ const rbB = bodies[idxB];
366
+ if (rbA === undefined || rbB === undefined) return -1;
367
+ if (IslandBuilder.__body_is_sensor(rbA, body_collider_lists[idxA])) return -1;
368
+ if (IslandBuilder.__body_is_sensor(rbB, body_collider_lists[idxB])) return -1;
369
+ // Pick the dynamic side's island. If both are dynamic they share one by
370
+ // construction (pass-1 union). Guard `>= 0`: a dynamic body that is ASLEEP
371
+ // has island_of_body === -1 (islands cover only awake bodies). Returning
372
+ // that -1 would drop the contact from the island, so the live
373
+ // non-penetration constraint against the awake side would never be solved
374
+ // e.g. an awake body resting on a freshly-slept neighbour falls through.
375
+ // Falling through to side B keeps the awake dynamic side's island.
376
+ const ia = this.island_of_body[idxA];
377
+ if (rbA.kind === BodyKind.Dynamic && ia >= 0) return ia;
378
+ const ib = this.island_of_body[idxB];
379
+ if (rbB.kind === BodyKind.Dynamic && ib >= 0) return ib;
380
+ return -1;
381
+ }
382
+
383
+ // --- Resize helpers ------------------------------------------------------
384
+
385
+ /**
386
+ * @private
387
+ * @param {number} n
388
+ */
389
+ __ensure_body_capacity(n) {
390
+ if (this.parent.length < n) {
391
+ const cap = ceilPowerOfTwo(n);
392
+ this.parent = new Uint32Array(cap);
393
+ this.island_of_body = new Int32Array(cap);
394
+ this.__root_to_island = new Int32Array(cap);
395
+ this.__scratch_roots = new Uint32Array(cap);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * @private
401
+ * @param {number} n
402
+ */
403
+ __ensure_island_count_capacity(n) {
404
+ if (this.body_offsets.length < n + 1) {
405
+ const cap = ceilPowerOfTwo(n + 1);
406
+ this.body_offsets = new Uint32Array(cap);
407
+ this.contact_offsets = new Uint32Array(cap);
408
+ this.__cursors = new Uint32Array(cap);
409
+ }
410
+ }
411
+
412
+ /**
413
+ * @private
414
+ * @param {number} n
415
+ */
416
+ __ensure_body_data_capacity(n) {
417
+ if (this.body_data.length < n) {
418
+ this.body_data = new Uint32Array(ceilPowerOfTwo(n));
419
+ }
420
+ }
421
+
422
+ /**
423
+ * @private
424
+ * @param {number} n
425
+ */
426
+ __ensure_contact_data_capacity(n) {
427
+ if (this.contact_data.length < n) {
428
+ this.contact_data = new Uint32Array(ceilPowerOfTwo(n));
429
+ }
430
+ }
431
+ }