@woosh/meep-engine 2.140.0 → 2.141.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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +21 -0
  3. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -0
  4. package/src/core/geom/3d/quaternion/quat3_multiply.js +25 -0
  5. package/src/engine/physics/PLAN.md +152 -35
  6. package/src/engine/physics/REVIEW_002.md +151 -0
  7. package/src/engine/physics/constraint/DofMode.d.ts +28 -0
  8. package/src/engine/physics/constraint/DofMode.d.ts.map +1 -0
  9. package/src/engine/physics/constraint/DofMode.js +35 -0
  10. package/src/engine/physics/constraint/solve_constraints.d.ts +16 -0
  11. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -0
  12. package/src/engine/physics/constraint/solve_constraints.js +436 -0
  13. package/src/engine/physics/ecs/Joint.d.ts +179 -0
  14. package/src/engine/physics/ecs/Joint.d.ts.map +1 -0
  15. package/src/engine/physics/ecs/Joint.js +234 -0
  16. package/src/engine/physics/ecs/PhysicsSystem.d.ts +52 -0
  17. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  18. package/src/engine/physics/ecs/PhysicsSystem.js +126 -4
  19. package/src/engine/physics/fluid/FluidField.d.ts +14 -10
  20. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  21. package/src/engine/physics/fluid/FluidField.js +14 -10
  22. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  23. package/src/engine/physics/fluid/FluidSimulator.js +0 -1
  24. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts +17 -10
  25. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.d.ts.map +1 -1
  26. package/src/engine/physics/fluid/solver/v3_grid_compute_solid_neighbour_mask.js +18 -11
  27. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts +13 -10
  28. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.d.ts.map +1 -1
  29. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure.js +18 -13
  30. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts +4 -3
  31. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.d.ts.map +1 -1
  32. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js +15 -11
  33. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +24 -22
  34. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  35. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +26 -22
  36. package/src/engine/physics/island/IslandBuilder.d.ts +4 -1
  37. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  38. package/src/engine/physics/island/IslandBuilder.js +33 -16
  39. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  40. package/src/engine/physics/narrowphase/box_box_manifold.js +27 -1
  41. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +33 -0
  42. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  43. package/src/engine/physics/narrowphase/narrowphase_step.js +75 -0
  44. package/src/engine/physics/solver/solve_contacts.d.ts +28 -0
  45. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  46. package/src/engine/physics/solver/solve_contacts.js +169 -1
@@ -6,18 +6,25 @@ import { assert } from "../../../../core/assert.js";
6
6
  * {@link v3_grid_solve_pressure}, which uses the mask to skip boundary checks
7
7
  * and solid-neighbour checks entirely.
8
8
  *
9
- * Encoding (one byte per cell, low 6 bits):
9
+ * Encoding (one byte per cell):
10
10
  *
11
- * bit 0 (= 1) : -x neighbour is fluid
12
- * bit 1 (= 2) : +x neighbour is fluid
13
- * bit 2 (= 4) : -y neighbour is fluid
14
- * bit 3 (= 8) : +y neighbour is fluid
15
- * bit 4 (= 16) : -z neighbour is fluid
16
- * bit 5 (= 32) : +z neighbour is fluid
11
+ * bit 0 (= 1) : -x neighbour is fluid
12
+ * bit 1 (= 2) : +x neighbour is fluid
13
+ * bit 2 (= 4) : -y neighbour is fluid
14
+ * bit 3 (= 8) : +y neighbour is fluid
15
+ * bit 4 (= 16) : -z neighbour is fluid
16
+ * bit 5 (= 32) : +z neighbour is fluid
17
+ * bit 7 (= 128) : this cell is itself solid
17
18
  *
18
- * Solid cells themselves are encoded as `mask = 0` (not "their own neighbours
19
- * are fluid") so the SOR loop's `mask === 0 skip` branch covers both
20
- * "self-solid" and "fluid but no fluid neighbours" with one comparison.
19
+ * The low 6 bits are the fluid-neighbour set; bit 7 flags the cell as solid.
20
+ * A solid cell encodes as exactly `128` bit 7 set, every neighbour bit clear
21
+ * and an isolated fluid cell (no fluid neighbours) as `0`. Both therefore
22
+ * satisfy `(mask & 63) === 0`, so the SOR / PCG loops' "no fluid neighbours →
23
+ * skip" branch is a single `(mask & 63) === 0` comparison that covers
24
+ * "self-solid" and "fluid but no fluid neighbours" alike. The dedicated bit 7
25
+ * additionally lets {@link v3_grid_subtract_pressure_gradient} tell solid cells
26
+ * (zero the velocity) apart from isolated fluid (leave it) without the original
27
+ * `solid` array.
21
28
  *
22
29
  * O(N) — one byte-write per cell, up to six byte-reads per cell. The output
23
30
  * fully replaces whatever was in `mask` before; no need to zero-init.
@@ -49,7 +56,7 @@ export function v3_grid_compute_solid_neighbour_mask(mask, solid, res_x, res_y,
49
56
  for (let x = 0; x < res_x; x++) {
50
57
  const c = y_off + x;
51
58
  if (solid[c] !== 0) {
52
- mask[c] = 0;
59
+ mask[c] = 128; // bit 7 = this cell is solid; neighbour bits stay clear
53
60
  continue;
54
61
  }
55
62
  let m = 0;
@@ -11,17 +11,20 @@
11
11
  * a Uint8Array allocated and populated by {@link FluidField} via
12
12
  * {@link FluidField.recomputeSolidNeighbourMask}. The encoding is:
13
13
  *
14
- * bit 0 (= 1) : -x neighbour is fluid
15
- * bit 1 (= 2) : +x neighbour is fluid
16
- * bit 2 (= 4) : -y neighbour is fluid
17
- * bit 3 (= 8) : +y neighbour is fluid
18
- * bit 4 (= 16) : -z neighbour is fluid
19
- * bit 5 (= 32) : +z neighbour is fluid
14
+ * bit 0 (= 1) : -x neighbour is fluid
15
+ * bit 1 (= 2) : +x neighbour is fluid
16
+ * bit 2 (= 4) : -y neighbour is fluid
17
+ * bit 3 (= 8) : +y neighbour is fluid
18
+ * bit 4 (= 16) : -z neighbour is fluid
19
+ * bit 5 (= 32) : +z neighbour is fluid
20
+ * bit 7 (= 128) : this cell is itself solid (consumed by
21
+ * {@link v3_grid_subtract_pressure_gradient}, ignored here)
20
22
  *
21
- * Solid cells encode as `mask = 0`, so the inner loop's `mask === 0 ? skip` covers
22
- * both self-solid AND fluid-with-no-fluid-neighbours in one comparison. This
23
- * eliminates six boundary checks plus six solid-neighbour checks per cell per
24
- * iteration, replacing them with a single mask read and six bit-tests on a
23
+ * The low 6 bits are the fluid-neighbour set. A solid cell encodes as `128` and
24
+ * an isolated fluid cell as `0`, so the inner loop's `(mask & 63) === 0 ? skip`
25
+ * covers both self-solid AND fluid-with-no-fluid-neighbours in one comparison.
26
+ * This eliminates six boundary checks plus six solid-neighbour checks per cell
27
+ * per iteration, replacing them with a single mask read and six bit-tests on a
25
28
  * register-resident value. Boundary handling (cells at x=0, x=last, etc.) is
26
29
  * implicit — the recompute step writes 0 into the boundary-facing bit, so the
27
30
  * SOR loop never reads out-of-range memory.
@@ -1 +1 @@
1
- {"version":3,"file":"v3_grid_solve_pressure.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_solve_pressure.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,iDAdW,YAAY,eAAa,cAEzB,YAAY,eAAa,SAEzB,MAAM,SACN,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,kBAEN,UAAU,QA+DpB"}
1
+ {"version":3,"file":"v3_grid_solve_pressure.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_solve_pressure.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,iDAdW,YAAY,eAAa,cAEzB,YAAY,eAAa,SAEzB,MAAM,SACN,MAAM,SACN,MAAM,cACN,MAAM,SACN,MAAM,kBAEN,UAAU,QAiEpB"}
@@ -30,17 +30,20 @@ const POPCOUNT_6 = (function () {
30
30
  * a Uint8Array allocated and populated by {@link FluidField} via
31
31
  * {@link FluidField.recomputeSolidNeighbourMask}. The encoding is:
32
32
  *
33
- * bit 0 (= 1) : -x neighbour is fluid
34
- * bit 1 (= 2) : +x neighbour is fluid
35
- * bit 2 (= 4) : -y neighbour is fluid
36
- * bit 3 (= 8) : +y neighbour is fluid
37
- * bit 4 (= 16) : -z neighbour is fluid
38
- * bit 5 (= 32) : +z neighbour is fluid
33
+ * bit 0 (= 1) : -x neighbour is fluid
34
+ * bit 1 (= 2) : +x neighbour is fluid
35
+ * bit 2 (= 4) : -y neighbour is fluid
36
+ * bit 3 (= 8) : +y neighbour is fluid
37
+ * bit 4 (= 16) : -z neighbour is fluid
38
+ * bit 5 (= 32) : +z neighbour is fluid
39
+ * bit 7 (= 128) : this cell is itself solid (consumed by
40
+ * {@link v3_grid_subtract_pressure_gradient}, ignored here)
39
41
  *
40
- * Solid cells encode as `mask = 0`, so the inner loop's `mask === 0 ? skip` covers
41
- * both self-solid AND fluid-with-no-fluid-neighbours in one comparison. This
42
- * eliminates six boundary checks plus six solid-neighbour checks per cell per
43
- * iteration, replacing them with a single mask read and six bit-tests on a
42
+ * The low 6 bits are the fluid-neighbour set. A solid cell encodes as `128` and
43
+ * an isolated fluid cell as `0`, so the inner loop's `(mask & 63) === 0 ? skip`
44
+ * covers both self-solid AND fluid-with-no-fluid-neighbours in one comparison.
45
+ * This eliminates six boundary checks plus six solid-neighbour checks per cell
46
+ * per iteration, replacing them with a single mask read and six bit-tests on a
44
47
  * register-resident value. Boundary handling (cells at x=0, x=last, etc.) is
45
48
  * implicit — the recompute step writes 0 into the boundary-facing bit, so the
46
49
  * SOR loop never reads out-of-range memory.
@@ -101,9 +104,11 @@ export function v3_grid_solve_pressure(pressure, divergence, res_x, res_y, res_z
101
104
  const c = z_off + y_off + x;
102
105
 
103
106
  const mask = neighbour_mask[c];
104
- if (mask === 0) {
105
- // Solid cell OR isolated fluid with no fluid neighbours.
106
- // Either way, no degrees of freedom — skip.
107
+ if ((mask & 63) === 0) {
108
+ // No fluid neighbours: solid cell (bit 7 set) OR isolated
109
+ // fluid. Either way, no degrees of freedom — skip. Cells
110
+ // that reach past here have bit 7 clear, so `mask` indexes
111
+ // POPCOUNT_6 (sized 64) in range.
107
112
  continue;
108
113
  }
109
114
 
@@ -69,9 +69,10 @@
69
69
  * the hot path that prevents MIC-PCG from parallelizing onto a GPU without
70
70
  * substantial reformulation. On single-thread JS that's not a concern.
71
71
  *
72
- * Solid cells (mask = 0) are excluded from the system: their pressure stays at
73
- * whatever it was (typically 0), their entries in r, z, s, As stay 0, and they
74
- * contribute nothing to dot products.
72
+ * Cells with no fluid neighbours solid cells (mask bit 7 set) and isolated
73
+ * fluid (mask = 0), both identified by `(mask & 63) === 0` are excluded from
74
+ * the system: their pressure stays at whatever it was (typically 0), their
75
+ * entries in r, z, s, As stay 0, and they contribute nothing to dot products.
75
76
  *
76
77
  * @param {Float32Array|Float16Array} pressure Mutated in place. Pre-fill with 0
77
78
  * or with the previous step's solution to warm-start.
@@ -1 +1 @@
1
- {"version":3,"file":"v3_grid_solve_pressure_pcg.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AACH,qDAhBW,YAAY,eAAa,cAEzB,YAAY,eAAa,SACzB,MAAM,SACN,MAAM,SACN,MAAM,kBACN,MAAM,kBAEN,UAAU,aACV,YAAY,aACZ,YAAY,aACZ,YAAY,cACZ,YAAY,kBACZ,YAAY,QAuNtB"}
1
+ {"version":3,"file":"v3_grid_solve_pressure_pcg.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_solve_pressure_pcg.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AACH,qDAhBW,YAAY,eAAa,cAEzB,YAAY,eAAa,SACzB,MAAM,SACN,MAAM,SACN,MAAM,kBACN,MAAM,kBAEN,UAAU,aACV,YAAY,aACZ,YAAY,aACZ,YAAY,cACZ,YAAY,kBACZ,YAAY,QA0NtB"}
@@ -108,9 +108,10 @@ const MIC_SIGMA = 0.25;
108
108
  * the hot path that prevents MIC-PCG from parallelizing onto a GPU without
109
109
  * substantial reformulation. On single-thread JS that's not a concern.
110
110
  *
111
- * Solid cells (mask = 0) are excluded from the system: their pressure stays at
112
- * whatever it was (typically 0), their entries in r, z, s, As stay 0, and they
113
- * contribute nothing to dot products.
111
+ * Cells with no fluid neighbours solid cells (mask bit 7 set) and isolated
112
+ * fluid (mask = 0), both identified by `(mask & 63) === 0` are excluded from
113
+ * the system: their pressure stays at whatever it was (typically 0), their
114
+ * entries in r, z, s, As stay 0, and they contribute nothing to dot products.
114
115
  *
115
116
  * @param {Float32Array|Float16Array} pressure Mutated in place. Pre-fill with 0
116
117
  * or with the previous step's solution to warm-start.
@@ -173,9 +174,12 @@ export function v3_grid_solve_pressure_pcg(
173
174
  // (Standard fluids practice — see Bridson §5.5. The alternative is to pin
174
175
  // one cell to a Dirichlet value and exclude it from the system, but mean
175
176
  // subtraction has the same effect with simpler bookkeeping.)
177
+ // A cell is a degree of freedom iff it has ≥1 fluid neighbour, i.e. any of
178
+ // the low 6 bits set. Solid cells (bit 7) and isolated fluid (all-zero) are
179
+ // both excluded by masking off bit 7 with & 63.
176
180
  let fluid_count = 0;
177
181
  for (let c = 0; c < cells; c++) {
178
- if (neighbour_mask[c] !== 0) fluid_count++;
182
+ if ((neighbour_mask[c] & 63) !== 0) fluid_count++;
179
183
  }
180
184
  if (fluid_count === 0) {
181
185
  return;
@@ -193,7 +197,7 @@ export function v3_grid_solve_pressure_pcg(
193
197
  precon.fill(0);
194
198
  for (let c = 0; c < cells; c++) {
195
199
  const mask = neighbour_mask[c];
196
- if (mask === 0) {
200
+ if ((mask & 63) === 0) {
197
201
  continue;
198
202
  }
199
203
  const a_diag = POPCOUNT_6[mask];
@@ -246,7 +250,7 @@ export function v3_grid_solve_pressure_pcg(
246
250
 
247
251
  for (let c = 0; c < cells; c++) {
248
252
  const mask = neighbour_mask[c];
249
- if (mask === 0) {
253
+ if ((mask & 63) === 0) {
250
254
  r[c] = 0;
251
255
  continue;
252
256
  }
@@ -285,7 +289,7 @@ export function v3_grid_solve_pressure_pcg(
285
289
  let s_dot_As = 0;
286
290
  for (let c = 0; c < cells; c++) {
287
291
  const mask = neighbour_mask[c];
288
- if (mask === 0) {
292
+ if ((mask & 63) === 0) {
289
293
  As[c] = 0;
290
294
  continue;
291
295
  }
@@ -379,11 +383,11 @@ export function v3_grid_solve_pressure_pcg(
379
383
  function subtract_mean_fluid(v, mask_array, cells, fluid_count) {
380
384
  let sum = 0;
381
385
  for (let c = 0; c < cells; c++) {
382
- if (mask_array[c] !== 0) sum += v[c];
386
+ if ((mask_array[c] & 63) !== 0) sum += v[c];
383
387
  }
384
388
  const mean = sum / fluid_count;
385
389
  for (let c = 0; c < cells; c++) {
386
- if (mask_array[c] !== 0) v[c] -= mean;
390
+ if ((mask_array[c] & 63) !== 0) v[c] -= mean;
387
391
  }
388
392
  }
389
393
 
@@ -391,7 +395,7 @@ function apply_preconditioner(r, z, precon, mask_array, cells, rx, slice) {
391
395
  // Forward solve L · y = r. Store y in z (we won't read z[c'] for c' > c).
392
396
  for (let c = 0; c < cells; c++) {
393
397
  const mask = mask_array[c];
394
- if (mask === 0) {
398
+ if ((mask & 63) === 0) {
395
399
  z[c] = 0;
396
400
  continue;
397
401
  }
@@ -410,7 +414,7 @@ function apply_preconditioner(r, z, precon, mask_array, cells, rx, slice) {
410
414
  // indices, processed first in reverse).
411
415
  for (let c = cells - 1; c >= 0; c--) {
412
416
  const mask = mask_array[c];
413
- if (mask === 0) {
417
+ if ((mask & 63) === 0) {
414
418
  continue;
415
419
  }
416
420
  let t = z[c]; // = y[c] from forward pass
@@ -13,25 +13,28 @@
13
13
  * pressure (consistent with the Neumann condition used in the solve), which keeps the
14
14
  * flow tangent to the boundary.
15
15
  *
16
- * Which neighbours count as fluid is read straight from `neighbour_mask`, the same
17
- * pre-baked per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
18
- * {@link FluidField.recomputeSolidNeighbourMask}). A set bit means "that neighbour is
19
- * fluid (in-bounds AND non-solid)", so a clear bit reflects the cell's own pressure —
20
- * folding the boundary check and the solid-neighbour check into one register-resident
21
- * bit-test per face. Encoding:
22
- *
23
- * bit 0 (= 1) : -x neighbour is fluid
24
- * bit 1 (= 2) : +x neighbour is fluid
25
- * bit 2 (= 4) : -y neighbour is fluid
26
- * bit 3 (= 8) : +y neighbour is fluid
27
- * bit 4 (= 16) : -z neighbour is fluid
28
- * bit 5 (= 32) : +z neighbour is fluid
29
- *
30
- * Unlike the solve, the mask can't drive the self-cell branch: both a solid cell and
31
- * an isolated fluid cell (no fluid neighbours) encode as `mask = 0`, yet they need
32
- * opposite handling the solid must be zeroed for no-slip, while the isolated fluid's
33
- * gradient already nets to zero (every face reflects its own pressure) and must be
34
- * left alone. `solid` is what tells them apart, so it stays.
16
+ * Everything this needs is read straight from `neighbour_mask`, the same pre-baked
17
+ * per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
18
+ * {@link FluidField.recomputeSolidNeighbourMask}). Encoding:
19
+ *
20
+ * bit 0 (= 1) : -x neighbour is fluid
21
+ * bit 1 (= 2) : +x neighbour is fluid
22
+ * bit 2 (= 4) : -y neighbour is fluid
23
+ * bit 3 (= 8) : +y neighbour is fluid
24
+ * bit 4 (= 16) : -z neighbour is fluid
25
+ * bit 5 (= 32) : +z neighbour is fluid
26
+ * bit 7 (= 128) : this cell is itself solid
27
+ *
28
+ * A set neighbour bit means "that neighbour is fluid (in-bounds AND non-solid)", so a
29
+ * clear bit reflects the cell's own pressure — folding the boundary check and the
30
+ * solid-neighbour check into one register-resident bit-test per face.
31
+ *
32
+ * Bit 7 carries the self-solid flag that used to require the separate `solid` array.
33
+ * The solve can't use the low 6 bits for this (a solid cell and an isolated fluid cell
34
+ * both have zero fluid-neighbour bits) but needs opposite handling here — the solid
35
+ * must be zeroed for no-slip, while the isolated fluid's gradient already nets to zero
36
+ * (every face reflects its own pressure) and must be left alone. Bit 7 distinguishes
37
+ * them, so a single mask read drives both the zeroing and the gradient.
35
38
  *
36
39
  * @param {Float32Array} vel_x Mutated in place.
37
40
  * @param {Float32Array} vel_y Mutated in place.
@@ -40,9 +43,8 @@
40
43
  * @param {number} res_x
41
44
  * @param {number} res_y
42
45
  * @param {number} res_z
43
- * @param {Uint8Array} solid Required (zero-filled for a wall-free domain).
44
46
  * @param {Uint8Array} neighbour_mask Length ≥ res_x*res_y*res_z. Same buffer the
45
- * pressure solve uses; MUST be recomputed whenever `solid` changes.
47
+ * pressure solve uses; MUST be recomputed whenever the solid mask changes.
46
48
  */
47
- export function v3_grid_subtract_pressure_gradient(vel_x: Float32Array, vel_y: Float32Array, vel_z: Float32Array, pressure: Float32Array, res_x: number, res_y: number, res_z: number, solid: Uint8Array, neighbour_mask: Uint8Array): void;
49
+ export function v3_grid_subtract_pressure_gradient(vel_x: Float32Array, vel_y: Float32Array, vel_z: Float32Array, pressure: Float32Array, res_x: number, res_y: number, res_z: number, neighbour_mask: Uint8Array): void;
48
50
  //# sourceMappingURL=v3_grid_subtract_pressure_gradient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v3_grid_subtract_pressure_gradient.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,0DAXW,YAAY,SACZ,YAAY,SACZ,YAAY,YACZ,YAAY,SACZ,MAAM,SACN,MAAM,SACN,MAAM,SACN,UAAU,kBACV,UAAU,QA8CpB"}
1
+ {"version":3,"file":"v3_grid_subtract_pressure_gradient.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,0DAVW,YAAY,SACZ,YAAY,SACZ,YAAY,YACZ,YAAY,SACZ,MAAM,SACN,MAAM,SACN,MAAM,kBACN,UAAU,QAgDpB"}
@@ -15,25 +15,28 @@ import { assert } from "../../../../core/assert.js";
15
15
  * pressure (consistent with the Neumann condition used in the solve), which keeps the
16
16
  * flow tangent to the boundary.
17
17
  *
18
- * Which neighbours count as fluid is read straight from `neighbour_mask`, the same
19
- * pre-baked per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
20
- * {@link FluidField.recomputeSolidNeighbourMask}). A set bit means "that neighbour is
21
- * fluid (in-bounds AND non-solid)", so a clear bit reflects the cell's own pressure —
22
- * folding the boundary check and the solid-neighbour check into one register-resident
23
- * bit-test per face. Encoding:
18
+ * Everything this needs is read straight from `neighbour_mask`, the same pre-baked
19
+ * per-cell bitmask {@link v3_grid_solve_pressure} consumes (populated by
20
+ * {@link FluidField.recomputeSolidNeighbourMask}). Encoding:
24
21
  *
25
- * bit 0 (= 1) : -x neighbour is fluid
26
- * bit 1 (= 2) : +x neighbour is fluid
27
- * bit 2 (= 4) : -y neighbour is fluid
28
- * bit 3 (= 8) : +y neighbour is fluid
29
- * bit 4 (= 16) : -z neighbour is fluid
30
- * bit 5 (= 32) : +z neighbour is fluid
22
+ * bit 0 (= 1) : -x neighbour is fluid
23
+ * bit 1 (= 2) : +x neighbour is fluid
24
+ * bit 2 (= 4) : -y neighbour is fluid
25
+ * bit 3 (= 8) : +y neighbour is fluid
26
+ * bit 4 (= 16) : -z neighbour is fluid
27
+ * bit 5 (= 32) : +z neighbour is fluid
28
+ * bit 7 (= 128) : this cell is itself solid
31
29
  *
32
- * Unlike the solve, the mask can't drive the self-cell branch: both a solid cell and
33
- * an isolated fluid cell (no fluid neighbours) encode as `mask = 0`, yet they need
34
- * opposite handling — the solid must be zeroed for no-slip, while the isolated fluid's
35
- * gradient already nets to zero (every face reflects its own pressure) and must be
36
- * left alone. `solid` is what tells them apart, so it stays.
30
+ * A set neighbour bit means "that neighbour is fluid (in-bounds AND non-solid)", so a
31
+ * clear bit reflects the cell's own pressure folding the boundary check and the
32
+ * solid-neighbour check into one register-resident bit-test per face.
33
+ *
34
+ * Bit 7 carries the self-solid flag that used to require the separate `solid` array.
35
+ * The solve can't use the low 6 bits for this (a solid cell and an isolated fluid cell
36
+ * both have zero fluid-neighbour bits) but needs opposite handling here — the solid
37
+ * must be zeroed for no-slip, while the isolated fluid's gradient already nets to zero
38
+ * (every face reflects its own pressure) and must be left alone. Bit 7 distinguishes
39
+ * them, so a single mask read drives both the zeroing and the gradient.
37
40
  *
38
41
  * @param {Float32Array} vel_x Mutated in place.
39
42
  * @param {Float32Array} vel_y Mutated in place.
@@ -42,11 +45,10 @@ import { assert } from "../../../../core/assert.js";
42
45
  * @param {number} res_x
43
46
  * @param {number} res_y
44
47
  * @param {number} res_z
45
- * @param {Uint8Array} solid Required (zero-filled for a wall-free domain).
46
48
  * @param {Uint8Array} neighbour_mask Length ≥ res_x*res_y*res_z. Same buffer the
47
- * pressure solve uses; MUST be recomputed whenever `solid` changes.
49
+ * pressure solve uses; MUST be recomputed whenever the solid mask changes.
48
50
  */
49
- export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure, res_x, res_y, res_z, solid, neighbour_mask) {
51
+ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure, res_x, res_y, res_z, neighbour_mask) {
50
52
  const cell_count = res_x * res_y * res_z;
51
53
  assert.greaterThanOrEqual(vel_x.length, cell_count, "vel_x covers grid");
52
54
  assert.greaterThanOrEqual(vel_y.length, cell_count, "vel_y covers grid");
@@ -65,7 +67,10 @@ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure
65
67
  for (let x = 0; x < res_x; x++) {
66
68
  const c = z_off + y_off + x;
67
69
 
68
- if (solid[c] !== 0) {
70
+ const mask = neighbour_mask[c];
71
+
72
+ if (mask & 128) {
73
+ // Solid cell (bit 7): enforce no-slip.
69
74
  vel_x[c] = 0;
70
75
  vel_y[c] = 0;
71
76
  vel_z[c] = 0;
@@ -75,7 +80,6 @@ export function v3_grid_subtract_pressure_gradient(vel_x, vel_y, vel_z, pressure
75
80
  // A clear bit means the neighbour is out-of-bounds or solid; reflect
76
81
  // the cell's own pressure so that face contributes 0 to the gradient,
77
82
  // consistent with ∂p/∂n = 0 at the wall.
78
- const mask = neighbour_mask[c];
79
83
  const p_xm = (mask & 1) ? pressure[c - 1] : pressure[c];
80
84
  const p_xp = (mask & 2) ? pressure[c + 1] : pressure[c];
81
85
  const p_ym = (mask & 4) ? pressure[c - res_x] : pressure[c];
@@ -117,8 +117,11 @@ export class IslandBuilder {
117
117
  * @param {ManifoldStore} manifolds
118
118
  * @param {RigidBody[]} bodies sparse, indexed by body index
119
119
  * @param {Array[]} body_collider_lists sparse, indexed by body index
120
+ * @param {Joint[]} joints live joints (sparse). Jointed dynamic-dynamic
121
+ * bodies are unioned into the same island so a chain / ragdoll sleeps
122
+ * and wakes as a unit. Callers with no joints pass an empty array.
120
123
  */
121
- build(storage: BodyStorage, manifolds: ManifoldStore, bodies: RigidBody[], body_collider_lists: any[][]): void;
124
+ build(storage: BodyStorage, manifolds: ManifoldStore, bodies: RigidBody[], body_collider_lists: any[][], joints: Joint[]): void;
122
125
  /**
123
126
  * Fill `body_offsets` + `body_data` with awake dynamic bodies grouped
124
127
  * by island, sorted ascending within each island.
@@ -1 +1 @@
1
- {"version":3,"file":"IslandBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/island/IslandBuilder.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IAyFI;;;;;OAKG;IACH,gCAUC;IAtGG;;;;;OAKG;IACH,QAFU,WAAW,CAEY;IAEjC;;;;OAIG;IACH,gBAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,cAFU,MAAM,CAEK;IAErB;;;;OAIG;IACH,cAFU,WAAW,CAEiB;IAEtC;;;;OAIG;IACH,WAFU,WAAW,CAEe;IAEpC;;;OAGG;IACH,YAFU,MAAM,CAEG;IAEnB;;;;;OAKG;IACH,iBAFU,WAAW,CAEoB;IAEzC;;;;OAIG;IACH,cAFU,WAAW,CAEkB;IAEvC;;;OAGG;IACH,eAFU,MAAM,CAEM;IAEtB;;;;;OAKG;IACH,yBAA0C;IAE1C;;;;;OAKG;IACH,wBAA0C;IAE1C;;;;OAIG;IACH,kBAAmC;IAqBvC;;;;;;;;OAQG;IACH,8DAHW,WAAW,uBACX,OAAO,QAuEjB;IAED;;;;OAIG;IACH,yBA+CC;IAED;;;;;;OAMG;IACH,4BAwCC;IAED;;;;;;;;OAQG;IACH,yBAcC;IAID;;;OAGG;IACH,+BAQC;IAED;;;OAGG;IACH,uCAOC;IAED;;;OAGG;IACH,oCAIC;IAED;;;OAGG;IACH,uCAIC;CACJ"}
1
+ {"version":3,"file":"IslandBuilder.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/island/IslandBuilder.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH;IAyFI;;;;;OAKG;IACH,gCAUC;IAtGG;;;;;OAKG;IACH,QAFU,WAAW,CAEY;IAEjC;;;;OAIG;IACH,gBAFU,UAAU,CAEoB;IAExC;;;OAGG;IACH,cAFU,MAAM,CAEK;IAErB;;;;OAIG;IACH,cAFU,WAAW,CAEiB;IAEtC;;;;OAIG;IACH,WAFU,WAAW,CAEe;IAEpC;;;OAGG;IACH,YAFU,MAAM,CAEG;IAEnB;;;;;OAKG;IACH,iBAFU,WAAW,CAEoB;IAEzC;;;;OAIG;IACH,cAFU,WAAW,CAEkB;IAEvC;;;OAGG;IACH,eAFU,MAAM,CAEM;IAEtB;;;;;OAKG;IACH,yBAA0C;IAE1C;;;;;OAKG;IACH,wBAA0C;IAE1C;;;;OAIG;IACH,kBAAmC;IAqBvC;;;;;;;;;;;OAWG;IACH,8DANW,WAAW,uBACX,OAAO,UACP,OAAO,QAgGjB;IAED;;;;OAIG;IACH,yBA+CC;IAED;;;;;;OAMG;IACH,4BAwCC;IAED;;;;;;;;OAQG;IACH,yBAcC;IAID;;;OAGG;IACH,+BAQC;IAED;;;OAGG;IACH,uCAOC;IAED;;;OAGG;IACH,oCAIC;IAED;;;OAGG;IACH,uCAIC;CACJ"}
@@ -1,6 +1,8 @@
1
+ import { ceilPowerOfTwo } from "../../../core/binary/operations/ceilPowerOfTwo.js";
1
2
  import { body_id_index } from "../body/BodyStorage.js";
2
3
  import { BodyKind } from "../ecs/BodyKind.js";
3
4
  import { ColliderFlags } from "../ecs/ColliderFlags.js";
5
+ import { JOINT_WORLD } from "../ecs/Joint.js";
4
6
  import { RigidBodyFlags } from "../ecs/RigidBodyFlags.js";
5
7
  import { uf_find, uf_init, uf_union } from "./union_find.js";
6
8
 
@@ -149,8 +151,11 @@ export class IslandBuilder {
149
151
  * @param {ManifoldStore} manifolds
150
152
  * @param {RigidBody[]} bodies sparse, indexed by body index
151
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.
152
157
  */
153
- build(storage, manifolds, bodies, body_collider_lists) {
158
+ build(storage, manifolds, bodies, body_collider_lists, joints) {
154
159
  const hwm = storage.high_water_mark;
155
160
  this.__ensure_body_capacity(hwm);
156
161
 
@@ -174,6 +179,29 @@ export class IslandBuilder {
174
179
  }
175
180
  }
176
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
+
177
205
  // --- Pass 2: collect distinct roots over awake dynamic bodies.
178
206
  const island_of_body = this.island_of_body;
179
207
  // Cheap reset: only the indices we may write are below hwm.
@@ -357,7 +385,7 @@ export class IslandBuilder {
357
385
  */
358
386
  __ensure_body_capacity(n) {
359
387
  if (this.parent.length < n) {
360
- const cap = next_pow2(n);
388
+ const cap = ceilPowerOfTwo(n);
361
389
  this.parent = new Uint32Array(cap);
362
390
  this.island_of_body = new Int32Array(cap);
363
391
  this.__root_to_island = new Int32Array(cap);
@@ -371,7 +399,7 @@ export class IslandBuilder {
371
399
  */
372
400
  __ensure_island_count_capacity(n) {
373
401
  if (this.body_offsets.length < n + 1) {
374
- const cap = next_pow2(n + 1);
402
+ const cap = ceilPowerOfTwo(n + 1);
375
403
  this.body_offsets = new Uint32Array(cap);
376
404
  this.contact_offsets = new Uint32Array(cap);
377
405
  this.__cursors = new Uint32Array(cap);
@@ -384,7 +412,7 @@ export class IslandBuilder {
384
412
  */
385
413
  __ensure_body_data_capacity(n) {
386
414
  if (this.body_data.length < n) {
387
- this.body_data = new Uint32Array(next_pow2(n));
415
+ this.body_data = new Uint32Array(ceilPowerOfTwo(n));
388
416
  }
389
417
  }
390
418
 
@@ -394,18 +422,7 @@ export class IslandBuilder {
394
422
  */
395
423
  __ensure_contact_data_capacity(n) {
396
424
  if (this.contact_data.length < n) {
397
- this.contact_data = new Uint32Array(next_pow2(n));
425
+ this.contact_data = new Uint32Array(ceilPowerOfTwo(n));
398
426
  }
399
427
  }
400
428
  }
401
-
402
- /**
403
- * @param {number} n
404
- * @returns {number}
405
- */
406
- function next_pow2(n) {
407
- if (n <= 1) return 1;
408
- let p = 1;
409
- while (p < n) p <<= 1;
410
- return p;
411
- }
@@ -1 +1 @@
1
- {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AA+HA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CA0anB;AAvhBD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
1
+ {"version":3,"file":"box_box_manifold.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/box_box_manifold.js"],"names":[],"mappings":"AAqJA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,sCAvBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,GACJ,OAAO,CA8anB;AA3hBD;;;GAGG;AACH,iCAFU,MAAM,CAEoD"}
@@ -39,6 +39,28 @@ const MAX_CONTACTS = 4;
39
39
  const CONTACT_STRIDE = 7;
40
40
  const PARALLEL_EPS_SQR = 1e-8;
41
41
 
42
+ /**
43
+ * Reference-axis tie-break deadband. SAT picks the minimum-overlap axis, but
44
+ * for aligned boxes two axes (A's face normal and B's face normal along the
45
+ * contact direction) have *equal* overlap, so floating-point noise from a
46
+ * sub-degree wobble flips which one wins frame to frame. That flips the
47
+ * reference/incident assignment, which reorders the contact points, which
48
+ * flips the solver's Gauss-Seidel sweep order — injecting an alternating
49
+ * bias that makes symmetric stacks creep and never sleep.
50
+ *
51
+ * The deadband makes a later axis replace the current best only if it
52
+ * reduces the overlap by more than `best · TIE_REL + TIE_ABS`. Axes are
53
+ * tested A-faces, then B-faces, then edge-crosses, so ties resolve to the
54
+ * earlier axis (A's face, then a face over an edge) — a stable, consistent
55
+ * choice. Genuine minima (overlap smaller by more than the band) still win,
56
+ * so SAT correctness for non-aligned boxes is unchanged; only the
57
+ * noise-driven flip on near-perfect alignment is suppressed. Box2D's
58
+ * `b2CollidePolygons` uses the same relative+absolute hysteresis.
59
+ * @type {number}
60
+ */
61
+ const TIE_REL = 0.02;
62
+ const TIE_ABS = 1e-5;
63
+
42
64
  /**
43
65
  * Length of `out` required by {@link box_box_manifold}.
44
66
  * @type {number}
@@ -182,7 +204,11 @@ export function box_box_manifold(
182
204
  const dist = proj < 0 ? -proj : proj;
183
205
  const overlap = (rA + rB) - dist;
184
206
  if (overlap < 0) return true;
185
- if (overlap < best_overlap) {
207
+ // Deadband: the first axis always wins; a later axis replaces it only
208
+ // if it reduces the overlap past the hysteresis band. This biases ties
209
+ // toward earlier-tested axes (A faces > B faces > edges) so aligned
210
+ // boxes pick a stable reference instead of flip-flopping on noise.
211
+ if (best_source === -1 || overlap < best_overlap - (best_overlap * TIE_REL + TIE_ABS)) {
186
212
  best_overlap = overlap;
187
213
  const sign = proj > 0 ? -1 : 1; // normal points B→A
188
214
  best_nx = ux * sign;
@@ -14,4 +14,37 @@ export function narrowphase_step(pair_list: PairList, manifolds: ManifoldStore,
14
14
  collider: Collider;
15
15
  transform: Transform;
16
16
  }>>): void;
17
+ /**
18
+ * Re-detect contact GEOMETRY for one existing manifold slot at the bodies'
19
+ * current poses, updating the witness points / normal / depth of the slot's
20
+ * existing contacts in place. Does NOT change the contact count, the
21
+ * feature ids, or the accumulated impulses — it only refreshes geometry.
22
+ *
23
+ * This is the per-substep concave path (TGS): for a contact pair involving a
24
+ * concave body, the contact *feature* (which triangle is deepest, and its
25
+ * normal) genuinely changes as the body rocks, so the solver's cheap analytic
26
+ * refresh — which freezes the feature for the whole outer step — pumps energy
27
+ * in. Re-running the narrowphase geometry each substep gives a fresh,
28
+ * correct normal so the body settles instead of rocking. Convex pairs keep
29
+ * the analytic refresh and never call this (their feature is stable).
30
+ *
31
+ * Matching is by feature id (stable per-triangle for the decomposition path),
32
+ * so a contact's geometry tracks the same triangle across substeps. A contact
33
+ * whose triangle isn't found this substep keeps its previous geometry (a rare
34
+ * transient; the once-per-frame {@link narrowphase_step} re-establishes the
35
+ * contact set next outer step). Count never changes here, so the solver's
36
+ * per-contact scratch (sized once at prepare) stays aligned.
37
+ *
38
+ * @param {ManifoldStore} manifolds
39
+ * @param {number} slot
40
+ * @param {Array<{collider: Collider, transform: Transform}>} list_a
41
+ * @param {Array<{collider: Collider, transform: Transform}>} list_b
42
+ */
43
+ export function redetect_pair_geometry(manifolds: ManifoldStore, slot: number, list_a: Array<{
44
+ collider: Collider;
45
+ transform: Transform;
46
+ }>, list_b: Array<{
47
+ collider: Collider;
48
+ transform: Transform;
49
+ }>): void;
17
50
  //# sourceMappingURL=narrowphase_step.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA6uCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE"}
1
+ {"version":3,"file":"narrowphase_step.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/narrowphase_step.js"],"names":[],"mappings":"AA6uCA;;;;;;;;;;;GAWG;AACH,uFALW,MAAM,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,CAAC,QAyJlE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,uEAJW,MAAM,UACN,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,UACjD,MAAM;IAAC,QAAQ,WAAW;IAAC,SAAS,YAAW;CAAC,CAAC,QAiD3D"}