@woosh/meep-engine 2.157.0 → 2.158.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 (60) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/PosedShape3D.d.ts +17 -0
  3. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/PosedShape3D.js +50 -0
  5. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  6. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  7. package/src/engine/graphics/ecs/trail2d/Trail2DFlags.d.ts +1 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2DFlags.js +9 -1
  9. package/src/engine/physics/fluid/FluidField.d.ts +53 -9
  10. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  11. package/src/engine/physics/fluid/FluidField.js +684 -600
  12. package/src/engine/physics/fluid/FluidSimulator.d.ts +53 -38
  13. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  14. package/src/engine/physics/fluid/FluidSimulator.js +252 -178
  15. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +155 -26
  16. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts +72 -0
  17. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts.map +1 -0
  18. package/src/engine/physics/fluid/ecs/FluidObstacle.js +97 -0
  19. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +117 -0
  20. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts.map +1 -0
  21. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +348 -0
  22. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  23. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts +62 -12
  24. package/src/engine/physics/fluid/effector/GlobalFluidEffector.d.ts.map +1 -1
  25. package/src/engine/physics/fluid/effector/GlobalFluidEffector.js +135 -38
  26. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.d.ts.map +1 -1
  27. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +85 -38
  28. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts.map +1 -1
  29. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +104 -50
  30. package/src/engine/physics/fluid/prototype.js +25 -1
  31. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts +30 -0
  32. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.d.ts.map +1 -0
  33. package/src/engine/physics/fluid/solver/v3_grid_sample_scalar_masked.js +92 -0
  34. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.d.ts +42 -0
  35. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.d.ts.map +1 -0
  36. package/src/engine/physics/fluid/solver/v3_mac_advect_maccormack_velocity.js +319 -0
  37. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts +53 -0
  38. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.d.ts.map +1 -0
  39. package/src/engine/physics/fluid/solver/v3_mac_advect_scalar.js +236 -0
  40. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.d.ts +46 -0
  41. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.d.ts.map +1 -0
  42. package/src/engine/physics/fluid/solver/v3_mac_advect_sl_velocity.js +217 -0
  43. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.d.ts +40 -0
  44. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.d.ts.map +1 -0
  45. package/src/engine/physics/fluid/solver/v3_mac_apply_vorticity_confinement.js +165 -0
  46. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.d.ts +44 -0
  47. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.d.ts.map +1 -0
  48. package/src/engine/physics/fluid/solver/v3_mac_clip_trace.js +95 -0
  49. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.d.ts +38 -0
  50. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.d.ts.map +1 -0
  51. package/src/engine/physics/fluid/solver/v3_mac_compute_divergence.js +77 -0
  52. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.d.ts +52 -0
  53. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.d.ts.map +1 -0
  54. package/src/engine/physics/fluid/solver/v3_mac_compute_face_solid.js +131 -0
  55. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.d.ts +38 -0
  56. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.d.ts.map +1 -0
  57. package/src/engine/physics/fluid/solver/v3_mac_subtract_pressure_gradient.js +104 -0
  58. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.d.ts +0 -41
  59. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.d.ts.map +0 -1
  60. package/src/engine/physics/fluid/effector/AmbientWindFluidEffector.js +0 -124
@@ -7,23 +7,23 @@ public API of effectors + `FluidField` frozen (state shape may change).
7
7
 
8
8
  ## Quality gate baselines (quality.spec.js)
9
9
 
10
- | metric | stage 0 (baseline) | stage 1 | stage 2 | stage 3 | notes |
10
+ | metric | stage 0 (baseline) | stage 2 | stage 3+4 | **stage 5 (MAC)** | notes |
11
11
  |---|---|---|---|---|---|
12
- | projection linear residual, SOR | 1.21 | 1.21 | 0.67 | | RHS max\|div\| 14.5, 16³, 8 sweeps |
13
- | projection linear residual, PCG | 3.34e-2 | 3.34e-2 | 1.16e-2 | | iteration-capped, not noise |
14
- | cold post-div (operator floor) | 11.2 | 11.2 | 9.1 | | collocated floor the MAC metric |
15
- | warm post-div, SOR | 9.22 | 9.22 | **0.70** | **0.35** | open edges vent + reflection projects cleaner |
16
- | warm post-div, PCG | 1.84 | 1.84 | **0.70** | **0.35** | tolerance exit: same quality, fewer iters |
17
- | vortex KE retention (60 steps) | 0.498 | 0.498 | 0.495 | **0.527** | sealed-box scenario, default scheme |
18
- | vortex peak-ω retention | 0.606 | 0.606 | 0.624 | **0.725** | reflection keeps swirl alive |
19
- | wall penetration (seed 8) | 5.72 | 5.72 | 6.96 | 6.65 | collocated solid BC weakness the MAC metric |
20
- | scalar mass retention | 0.231 | 0.231 | **0.849** | 0.837 | real walls project consistently near boundary |
21
- | diffusion wall leak | 16.9 | **0** | 0 | 0 | fixed: no-flux on both wall sides |
22
- | sealed-box gravity max\|vy\| | 1.24 | 1.24 | 1.24 | **0.97** | semi-hydrostatic |
23
- | checkerboard residual (10 steps) | 0.593 | 0.593 | 0.627 | 0.587 | projection-invisible null mode |
24
- | damped gravity terminal \|v\| | — | — | 4.92 = analytic | 4.92 | bounded forcing (was unbounded growth) |
25
- | ambient wind target tracking | — | | 8.00/8 | 8.00/8 | AmbientWindFluidEffector holds target |
26
- | step() 32×8×32 (ms) | 1.16 | | 1.44 | 1.10* | *SL+refl after stage-3 sampler fusion; stage-4 still owes SOR work |
12
+ | projection linear residual, SOR | 1.21 | 0.67 | 0.67 | 0.92 | 8 sweeps @ 16³; RHS grew 14.5→17.8 with face differencing |
13
+ | projection linear residual, PCG | 3.34e-2 | 1.16e-2 | 1.16e-2 | 1.39e-2 | iteration/tolerance-capped |
14
+ | cold post-div | 11.2 | 9.1 | 9.1 | **0.91 / 1.4e-2** | SOR / PCG. Now EQUALS the linear residual: D∘G ≡ L, the operator floor is gone |
15
+ | warm post-div, SOR | 9.22 | 0.70 | 0.35 | **9.6e-3** | 1000× vs stage 0 |
16
+ | warm post-div, PCG | 1.84 | 0.70 | 0.35 | **3.1e-5** | |
17
+ | vortex KE retention (60 steps) | 0.498 | 0.495 | 0.527 | 0.530 | sealed box, default SL+reflection |
18
+ | vortex peak-ω retention | 0.606 | 0.624 | 0.725 | **0.736** | |
19
+ | wall penetration (seed 8) | 5.72 | 6.96 | 6.65 | **0 (exact)** | MAC pins wall-face normal velocity outright |
20
+ | scalar mass retention | 0.231 | 0.849 | 0.837 | 0.818 | residual loss = gather SL + traces through the shell |
21
+ | diffusion wall leak | 16.9 | 0 | 0 | 0 | stage-1 fix holds |
22
+ | sealed-box gravity max\|vy\| | 1.24 | 1.24 | 0.97 | **0.070** | near-exact hydrostatic balance |
23
+ | checkerboard residual (10 steps) | 0.593 | 0.627 | 0.587 | **0.058** | pure compressive mode; collocated was blind to it |
24
+ | damped gravity terminal \|v\| | — | 4.92 = analytic | 4.92 | 4.92 | bounded forcing (was unbounded growth) |
25
+ | atmosphere terminal velocity | — | 8.00/8 | 8.00/8 | (8.00, −5.00) = analytic | GlobalFluidEffector wind+force+drag holds `wind + force/drag` exactly |
26
+ | step() 32×8×32 / 32³ / 64³ (ms) | 1.16 / 4.38 / 54.0 | 1.44 / 5.11 / — | 1.25 / 3.91 / 42.7 | 2.56 / 7.39 / 68.5 | MAC ≈ 2× collocated: 3 lattices + cross-component carriers; levers listed under follow-ups |
27
27
 
28
28
  Perf reference (review probe, this machine): step() 32×8×32 = 1.16 ms,
29
29
  32³ = 4.4 ms, 64³ = 54 ms. Division→reciprocal-table in SOR measured 14%
@@ -58,9 +58,13 @@ SLOWER in V8 — do not do it.
58
58
  - `velocity_damping` knob on the simulator (exponential, default 0) — the
59
59
  solver's only energy sink under sustained forcing; gives constant forces
60
60
  a terminal velocity.
61
- - New `AmbientWindFluidEffector` — relaxes toward a target wind velocity
62
- instead of integrating constant acceleration (no unbounded accumulation;
63
- frame-rate independent).
61
+ - New ambient-wind drive — relaxes toward a target wind velocity instead of
62
+ integrating constant acceleration (no unbounded accumulation; frame-rate
63
+ independent). Shipped first as a separate `AmbientWindFluidEffector`,
64
+ later UNIFIED into `GlobalFluidEffector` as the `wind` + `drag` fields of
65
+ the atmosphere model `dv/dt = force + drag·(wind − v)` (exact-ODE
66
+ integrator, terminal velocity `wind + force/drag`) — one global effector
67
+ instead of two competing ones.
64
68
  - [x] **Stage 3 — transport quality.** (213 tests green.)
65
69
  - Fused 3-channel trilinear sampler (`scs3d_sample_linear3`); generalized
66
70
  SL velocity kernel with separate carrier; MacCormack velocity + scalar
@@ -86,13 +90,138 @@ SLOWER in V8 — do not do it.
86
90
  `advection_reflection = true`. Reflection alone is a clean win: stable,
87
91
  +20% step cost, warm steady-state divergence halves again (0.71 → 0.35),
88
92
  sealed-gravity settles better (1.24 → 0.97).
89
- - [ ] **Stage 4 — perf polish (bench-gated).**
90
- - SOR interior fast path (mask == 63 branchless) — keep only if measured win.
91
- - Optional vorticity-confinement knob (Fedkiw 2001), default off.
92
- - [ ] **Stage 5 MAC staggering decision.** Present quality deltas (operator
93
- floor, wall penetration, checkerboard) + consumer impact; **user decides**
94
- before any layout change (it alters how `velocity_*` arrays are interpreted
95
- by external readers).
93
+ - [x] **Stage 4 — perf polish (bench-gated).** (219 tests green; quality gate
94
+ bit-identical.)
95
+ - SOR interior fast path (`mask === 63` → branch-free constant-diagonal
96
+ body, no diag read): measured **37% faster** per solve at 64³, noise-level
97
+ at 32×8×32, bit-identical results. Adopted.
98
+ - Rejected by measurement (stage-0 probe): division reciprocal-table in
99
+ the SOR update was 14% SLOWER in V8. Recorded so nobody retries it.
100
+ - `vorticity_confinement` knob (Fedkiw 2001), default off — two O(N) passes
101
+ on existing scratch when enabled; pairs with `velocity_damping`.
102
+ - Final step() medians (this machine): 32×8×32 = 1.25 ms, 32³ = 3.91 ms,
103
+ 64³ = 42.7 ms — at 32³ and up faster than the stage-0 baseline despite
104
+ reflection being on.
105
+ - [x] **Stage 5 — MAC staggering migration (user-approved).** 223 tests green.
106
+ - Layout: `velocity_x/y/z` are face lattices ((rx+1)·ry·rz etc.); new derived
107
+ `face_solid_x/y/z` pin masks (either adjacent cell solid → face velocity is
108
+ a boundary condition). `setVelocityAt`/`addVelocityAt` write both faces per
109
+ axis; `sampleVelocity` interpolates per staggered lattice. Public
110
+ signatures unchanged.
111
+ - Kernels: new `v3_mac_*` divergence / gradient / SL + MacCormack advection
112
+ (velocity + scalar) / face-solid / vorticity confinement. The pressure
113
+ solvers and the mask/diag builder needed ZERO changes — stage 2's
114
+ open-boundary diagonal already was the MAC system. The legacy `v3_grid_*`
115
+ collocated kernels remain as standalone utilities with their specs.
116
+ - Exactness confirmed by the gate: cold post-divergence == linear residual
117
+ (D∘G ≡ L); wall-face penetration exactly 0; hydrostatic balance to 0.07;
118
+ the compressive checkerboard is annihilated (0.59 → 0.058).
119
+ - Scheme matrix on MAC: MC velocity is now long-horizon STABLE (10 s peak
120
+ 0.96 vs collocated 1.9×), but (a) MC+reflection still pumps (3.6×) and
121
+ (b) MC SCALAR transport still inflates mass ~1.7× — back-traces sample
122
+ through solid walls and the corrector amplifies the bleed. Defaults stay
123
+ SL + reflection; MacCormack documented opt-in.
124
+ - Perf: 2.56 / 7.39 / 68.5 ms at 32×8×32 / 32³ / 64³ (~2× collocated). The
125
+ SL kernel's cross-component carrier reconstruction was optimized from
126
+ general trilinear to the exact fixed 4-tap MAC average (−31..39% step),
127
+ pinned by a trilinear-reference equivalence spec.
128
+
129
+ ## Phase A (post-MAC) — solid-aware traces + MacCormack default. DONE.
130
+
131
+ - Back-traces (and MacCormack probes) are CLIPPED against solid cells
132
+ (`v3_mac_clip_trace`, bisection); scalar samples are solid-MASKED with
133
+ weight renormalization (`v3_grid_sample_scalar_masked`). Kills momentum
134
+ tunnelling through thin walls (spec-pinned) and wall dye bleed.
135
+ - **Negative finding, gate-corrected:** MacCormack SCALAR mass inflation
136
+ (1.71×) was NOT wall sampling — bit-identical before/after the fix. It is
137
+ the corrector re-adding ~double the gather bias: intrinsic
138
+ non-conservation. Scalars are therefore ALWAYS semi-Lagrangian
139
+ (hard-coded; `advection_scheme` selects the VELOCITY scheme only).
140
+ - MC + reflection re-probed WITH clipping: still pumps (10 s peak 3.7×) —
141
+ intrinsic to the pairing, stays barred.
142
+ - **Defaults: `advection_scheme = MACCORMACK` (velocity),
143
+ `advection_reflection = false`.** Gate: vortex KE retention 0.53 → 0.80
144
+ on the default path; sealed gravity 0.070 → 0.047; warm divergence
145
+ settles at 2.96e-2 / 2.8e-4 (SOR/PCG — one mid-step projection fewer than
146
+ SL+refl's 9.6e-3 / 3.1e-5; still ~300× below pre-MAC).
147
+ - **Perf lesson (measured 8.6×!):** V8 refuses to inline loop-bearing
148
+ callees — calling `v3_mac_clip_trace` per face made the SL kernel 8.6×
149
+ slower. Hot kernels now inline the straight-line "end cell solid?" test
150
+ and call the bisection helper only on wall hits (performance contract
151
+ documented on the helper). Final step(): MC default 2.74 ms @ 32×8×32 /
152
+ 10.4 ms @ 32³; SL+refl 1.97 / 7.35.
153
+
154
+ ## Phase B — FluidObstacleSystem. DONE.
155
+
156
+ - `FluidObstacle` component (inflation knob) + `FluidObstacleSystem`:
157
+ voxelizes each (FluidObstacle, Collider, Transform) entity's shape into
158
+ every overlapping FluidComponent's solid mask per tick, via
159
+ `PosedShape3D.signed_distance_at_point` (new posed point queries on
160
+ PosedShape3D, spec'd). Rigid pose — Transform.scale ignored, matching the
161
+ physics collider convention. Cell solid iff sdf(centre) <= inflation;
162
+ iteration clipped to the shape AABB.
163
+ - Ownership contract: with >= 1 obstacle the system clears + rebuilds EVERY
164
+ field's mask each tick (moving obstacles leave no stale walls); with none
165
+ it touches nothing. Register BEFORE FluidSystem.
166
+ - 8 system tests incl. rotation, inflation-for-thin-walls, origin offset,
167
+ broad-phase, and a same-tick FluidSystem integration check.
168
+
169
+ ## Phase C — moving-solid face velocity BCs. DONE.
170
+
171
+ - **Pinned faces are the wall's normal-velocity BC**, not dead storage:
172
+ - `v3_mac_compute_face_solid` takes the velocity arrays and zeroes a face
173
+ only on the unpinned→pinned TRANSITION (static-wall default).
174
+ Pinned→pinned recomputes preserve the stored BC; pinned→unpinned keeps
175
+ the wall velocity behind as the uncovered fluid's wake seed. Spec'd
176
+ (`v3_mac_compute_face_solid.spec.js`).
177
+ - `v3_mac_compute_divergence` reads pinned faces at face value (the
178
+ face_solid params are gone) — this is the entire transmission mechanism:
179
+ a stamped wall face shows up as inflow/outflow in the adjacent fluid
180
+ cell and the projection does the rest.
181
+ - The pressure gradient, advection, damping and effectors all SKIP pinned
182
+ faces, so a stamped BC survives the whole step bit-exactly (asserted in
183
+ quality 8).
184
+ - `FluidObstacleSystem` second pass: per obstacle, the wall velocity is
185
+ **`RigidBody.linearVelocity`** (world m/s — the obstacle tuple is the full
186
+ physics tuple FluidObstacle+Collider+RigidBody+Transform); cells it
187
+ voxelized get all six face slots stamped with the grid-units velocity.
188
+ The system refreshes each field's masks BEFORE stamping so the transition
189
+ zeroing is consumed there and the stamped values stick through the
190
+ simulator's own per-step refresh.
191
+ - **Velocity source rationale** (replaced the initial Transform-delta
192
+ finite difference in the same session): pose differencing turns teleports
193
+ (spawn snaps, network corrections) into one-tick air detonations — the
194
+ exact failure mode BodyKind.KinematicPosition was deferred over in the
195
+ contact solver. Reading the body instead: air and contacts agree on wall
196
+ speed, first tick stamps (no `_prev` warm-up), no system-private pose
197
+ state to lose on snapshot restore, sleeping bodies stamp nothing (solver
198
+ zeroes velocities on sleep), and `angularVelocity` sits ready for the
199
+ ω×r follow-up. Pose-animated colliders read as stationary walls that
200
+ teleport — for air just as for contacts; movers go through
201
+ KinematicVelocity/Dynamic. Teleport spec pins the no-blast contract.
202
+ - **Stop transition**: a wall that stops must stamp ONE more pass (zeros)
203
+ over its faces — pinned values persist by design, so without it the
204
+ divergence sees phantom inflow forever (`_was_moving` flag; obstacles
205
+ that never move never pay). Piston spec covers move → push → stop.
206
+ - Rotation-induced surface velocity (ω × r) is NOT modelled — a spinner
207
+ reads as a translating wall only (documented on the system).
208
+ - Ordering contract: PhysicsSystem → FluidObstacleSystem → FluidSystem.
209
+ - Gate: scenario 8 (piston slab, faces stamped +4, 10 steps) — mean u ahead
210
+ 2.08 baked, wake side 2.60 sign-checked; BC face value asserted == 4
211
+ after 10 steps. Static scenarios all unchanged.
212
+
213
+ ## Follow-ups (ordered by leverage)
214
+
215
+ 1. **Obstacle voxelization change detection** — static scenes currently pay
216
+ the full clear+rebuild every tick.
217
+ 2. **MC + reflection** — only via the second-order advection-reflection
218
+ formulation (Narain et al. 2019, implicit-midpoint form); the naive
219
+ pairing re-injects corrector overshoots (3.7×, with or without clipping).
220
+ 3. **bench.spec.js MAC refresh** — the `.skip` suites run, but the
221
+ per-primitive sections still measure the legacy collocated kernels.
222
+ 4. **ω × r surface velocity for rotating obstacles** — needs per-cell wall
223
+ velocity instead of one translation vector per obstacle; same stamping
224
+ machinery, finer-grained source.
96
225
 
97
226
  ## Review findings driving this (2026-06-12 session)
98
227
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Marks an entity's {@link Collider} as a fluid obstacle: each fixed update,
3
+ * {@link FluidObstacleSystem} voxelizes the collider's shape (via its signed
4
+ * distance function, under the entity's rigid `Transform` pose) into the solid
5
+ * mask of every overlapping {@link FluidComponent}, and stamps the entity's
6
+ * {@link RigidBody} velocity onto the wall faces as the moving-wall boundary
7
+ * condition. Wind stops at — and is pushed by — the bodies the physics
8
+ * already knows about; no separate fluid geometry to author.
9
+ *
10
+ * Requirements on the entity (the same tuple {@link PhysicsSystem} uses, so
11
+ * any physical body qualifies as-is):
12
+ * - a `Collider` whose shape implements
13
+ * {@link AbstractShape3D#signed_distance_at_point} (all analytic
14
+ * primitives do; mesh-ish shapes that don't will throw at voxelization),
15
+ * - a `RigidBody` — the wall-velocity source (see the system doc for the
16
+ * exact semantics). Static walls use `BodyKind.Static` as usual.
17
+ * - a `Transform` for the world pose. Following the collider convention,
18
+ * the pose is RIGID — `Transform.scale` is ignored, exactly as the
19
+ * physics narrowphase ignores it.
20
+ *
21
+ * Entities missing any part of the tuple are not obstacles and are skipped —
22
+ * standard ECS tuple semantics, nothing throws.
23
+ *
24
+ * This component is pure data; the behaviour lives on the system.
25
+ */
26
+ export class FluidObstacle {
27
+ /**
28
+ * Voxelization threshold in WORLD units: a fluid cell becomes solid when
29
+ * the signed distance from its centre to the shape is `<= inflation`.
30
+ *
31
+ * `0` (default) marks exactly the cells whose centres lie inside the
32
+ * shape. Positive values grow the voxel footprint outward — useful for
33
+ * thin obstacles that would otherwise slip between cell centres (a wall
34
+ * thinner than one cell needs `inflation >= cell_size / 2` to voxelize
35
+ * reliably), or to give the fluid a safety margin around fast movers.
36
+ * Negative values shrink it.
37
+ *
38
+ * @type {number}
39
+ */
40
+ inflation: number;
41
+ /**
42
+ * True when the previous tick stamped a non-zero wall velocity. A body
43
+ * that stops (or sleeps — the solver zeroes velocities on sleep) must
44
+ * stamp once more with zeros to clear its stale moving BC, because
45
+ * pinned faces preserve their values across mask recomputes by design.
46
+ * System-private.
47
+ * @type {boolean}
48
+ */
49
+ _was_moving: boolean;
50
+ /**
51
+ * @param {FluidObstacle} other
52
+ * @return {boolean}
53
+ */
54
+ equals(other: FluidObstacle): boolean;
55
+ /**
56
+ * @return {number}
57
+ */
58
+ hash(): number;
59
+ toJSON(): {
60
+ inflation: number;
61
+ };
62
+ fromJSON(json: any): void;
63
+ /**
64
+ * @readonly
65
+ * @type {boolean}
66
+ */
67
+ readonly isFluidObstacle: boolean;
68
+ }
69
+ export namespace FluidObstacle {
70
+ let typeName: string;
71
+ }
72
+ //# sourceMappingURL=FluidObstacle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FluidObstacle.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/ecs/FluidObstacle.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;IAEI;;;;;;;;;;;;OAYG;IACH,WAFU,MAAM,CAEF;IAEd;;;;;;;OAOG;IACH,aAFU,OAAO,CAEG;IAEpB;;;OAGG;IACH,cAHW,aAAa,GACZ,OAAO,CAUlB;IAED;;OAEG;IACH,QAFY,MAAM,CAIjB;IAED;;MAIC;IAED,0BAEC;IASL;;;OAGG;IACH,0BAFU,OAAO,CAEsB;CAZtC;;kBAIS,MAAM"}
@@ -0,0 +1,97 @@
1
+ import { computeHashFloat } from "../../../../core/primitives/numbers/computeHashFloat.js";
2
+
3
+ /**
4
+ * Marks an entity's {@link Collider} as a fluid obstacle: each fixed update,
5
+ * {@link FluidObstacleSystem} voxelizes the collider's shape (via its signed
6
+ * distance function, under the entity's rigid `Transform` pose) into the solid
7
+ * mask of every overlapping {@link FluidComponent}, and stamps the entity's
8
+ * {@link RigidBody} velocity onto the wall faces as the moving-wall boundary
9
+ * condition. Wind stops at — and is pushed by — the bodies the physics
10
+ * already knows about; no separate fluid geometry to author.
11
+ *
12
+ * Requirements on the entity (the same tuple {@link PhysicsSystem} uses, so
13
+ * any physical body qualifies as-is):
14
+ * - a `Collider` whose shape implements
15
+ * {@link AbstractShape3D#signed_distance_at_point} (all analytic
16
+ * primitives do; mesh-ish shapes that don't will throw at voxelization),
17
+ * - a `RigidBody` — the wall-velocity source (see the system doc for the
18
+ * exact semantics). Static walls use `BodyKind.Static` as usual.
19
+ * - a `Transform` for the world pose. Following the collider convention,
20
+ * the pose is RIGID — `Transform.scale` is ignored, exactly as the
21
+ * physics narrowphase ignores it.
22
+ *
23
+ * Entities missing any part of the tuple are not obstacles and are skipped —
24
+ * standard ECS tuple semantics, nothing throws.
25
+ *
26
+ * This component is pure data; the behaviour lives on the system.
27
+ */
28
+ export class FluidObstacle {
29
+
30
+ /**
31
+ * Voxelization threshold in WORLD units: a fluid cell becomes solid when
32
+ * the signed distance from its centre to the shape is `<= inflation`.
33
+ *
34
+ * `0` (default) marks exactly the cells whose centres lie inside the
35
+ * shape. Positive values grow the voxel footprint outward — useful for
36
+ * thin obstacles that would otherwise slip between cell centres (a wall
37
+ * thinner than one cell needs `inflation >= cell_size / 2` to voxelize
38
+ * reliably), or to give the fluid a safety margin around fast movers.
39
+ * Negative values shrink it.
40
+ *
41
+ * @type {number}
42
+ */
43
+ inflation = 0;
44
+
45
+ /**
46
+ * True when the previous tick stamped a non-zero wall velocity. A body
47
+ * that stops (or sleeps — the solver zeroes velocities on sleep) must
48
+ * stamp once more with zeros to clear its stale moving BC, because
49
+ * pinned faces preserve their values across mask recomputes by design.
50
+ * System-private.
51
+ * @type {boolean}
52
+ */
53
+ _was_moving = false;
54
+
55
+ /**
56
+ * @param {FluidObstacle} other
57
+ * @return {boolean}
58
+ */
59
+ equals(other) {
60
+ if (other === this) {
61
+ return true;
62
+ }
63
+ if (other === null || other === undefined || other.isFluidObstacle !== true) {
64
+ return false;
65
+ }
66
+ return this.inflation === other.inflation;
67
+ }
68
+
69
+ /**
70
+ * @return {number}
71
+ */
72
+ hash() {
73
+ return computeHashFloat(this.inflation);
74
+ }
75
+
76
+ toJSON() {
77
+ return {
78
+ inflation: this.inflation,
79
+ };
80
+ }
81
+
82
+ fromJSON(json) {
83
+ if (json.inflation !== undefined) this.inflation = json.inflation;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @readonly
89
+ * @type {string}
90
+ */
91
+ FluidObstacle.typeName = "FluidObstacle";
92
+
93
+ /**
94
+ * @readonly
95
+ * @type {boolean}
96
+ */
97
+ FluidObstacle.prototype.isFluidObstacle = true;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ECS system that voxelizes collider geometry into fluid solid masks and
3
+ * stamps rigid-body velocities onto the walls as moving-wall boundary
4
+ * conditions.
5
+ *
6
+ * Every fixed update, each entity carrying ({@link FluidObstacle},
7
+ * {@link Collider}, {@link RigidBody}, {@link Transform}) — the same tuple
8
+ * {@link PhysicsSystem} simulates, plus the marker — is swept against every
9
+ * {@link FluidComponent}: the collider's shape — posed rigidly at the
10
+ * transform's position/rotation via {@link PosedShape3D} — is sampled at the
11
+ * centre of every fluid cell inside its world AABB, and cells whose signed
12
+ * distance is `<= obstacle.inflation` become solid. The fluid then treats
13
+ * those cells exactly like hand-authored walls: faces pinned, pressure
14
+ * Neumann, traces clipped.
15
+ *
16
+ * **Ownership contract**: while at least one obstacle exists, this system
17
+ * OWNS the solid mask of every fluid field — masks are cleared and rebuilt
18
+ * from scratch each tick, so moving obstacles leave no stale walls behind.
19
+ * Hand-written `setSolidAt` state will be wiped; mix the two styles only by
20
+ * expressing the static geometry as obstacle entities too (a `BoxShape3D`
21
+ * collider is cheaper to author than a splat loop anyway). With NO obstacles
22
+ * present the system leaves every mask untouched.
23
+ *
24
+ * **Moving walls — the wall-velocity specification**:
25
+ *
26
+ * - The velocity source is `RigidBody.linearVelocity` (world m/s) — the
27
+ * SAME value the contact solver resolves against, so air and contacts
28
+ * always agree on how fast a wall moves. It is stamped onto every face
29
+ * of every cell the obstacle voxelized (converted to grid units per
30
+ * field); the projection's divergence reads pinned faces at face value,
31
+ * so a moving collider genuinely pushes and drags the air around it.
32
+ * - Pose is still read from `Transform`; velocity is NOT derived from
33
+ * pose deltas. A teleported body (spawn snap, network correction)
34
+ * relocates its walls but stamps no velocity — no one-tick air blast.
35
+ * This mirrors the engine's own contact-solver rule: see
36
+ * {@link BodyKind}.KinematicPosition, deferred for exactly this reason.
37
+ * Consequently pose-animated colliders read as stationary walls that
38
+ * teleport, for air just as for contacts — drive movers through
39
+ * `BodyKind.KinematicVelocity` (or Dynamic) to displace air.
40
+ * - `BodyKind.Static` bodies have zero velocity and stamp nothing.
41
+ * Sleeping bodies stamp nothing either — the solver zeroes velocities
42
+ * on sleep.
43
+ * - A body that stops stamps ONE extra pass of zeros
44
+ * (`FluidObstacle._was_moving`): pinned faces preserve their values
45
+ * across mask recomputes by design, so without it a stale moving BC
46
+ * would drive phantom inflow forever.
47
+ * - A retreating wall leaves its velocity behind on unpinned faces as the
48
+ * wake seed for the fluid that takes its place.
49
+ * - Rotation-induced surface velocity (ω × r from
50
+ * `RigidBody.angularVelocity`) is not modelled yet — fast spinners read
51
+ * as translating walls only (follow-up in REVIEW_02_PLAN.md).
52
+ *
53
+ * **Ordering**: register AFTER {@link PhysicsSystem} (so this tick's solved
54
+ * velocities and integrated poses are read) and BEFORE {@link FluidSystem}
55
+ * (so the simulation step sees this tick's walls and wall velocities).
56
+ * Engine systems run in registration order.
57
+ *
58
+ * Cost: clearing is O(cells) per field; voxelization is one
59
+ * `signed_distance_at_point` per cell inside each obstacle's AABB ∩ field.
60
+ * Static scenes pay the same as moving ones — change detection is a
61
+ * follow-up (REVIEW_02_PLAN.md) if profiling ever points here.
62
+ */
63
+ export class FluidObstacleSystem extends System<any, any, any, any, any> {
64
+ /**
65
+ * @param {FluidComponent} fluid
66
+ */
67
+ static "__#143@#refresh_masks"(fluid: FluidComponent): void;
68
+ /**
69
+ * @param {FluidComponent} fluid
70
+ */
71
+ static "__#143@#clear_field"(fluid: FluidComponent): void;
72
+ /**
73
+ * Mark every cell of `fluid` whose centre lies within `inflation` of the
74
+ * posed shape as solid. Iteration is clipped to the shape's world AABB
75
+ * (grown by `inflation`), so far-away obstacles cost nothing.
76
+ *
77
+ * @param {FluidComponent} fluid
78
+ * @param {PosedShape3D} posed
79
+ * @param {Float64Array} aabb world AABB of the posed shape
80
+ * @param {number} inflation world-units SDF threshold
81
+ * @param {Float64Array} point length-3 scratch
82
+ */
83
+ static "__#143@#voxelize"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array): void;
84
+ /**
85
+ * Write the obstacle's translation velocity onto every face of every cell
86
+ * it voxelized — the moving-wall boundary condition. Runs AFTER the mask
87
+ * refresh, so the values stick (already-pinned faces keep their stored
88
+ * velocity through subsequent recomputes).
89
+ *
90
+ * Velocity is converted to grid units (cells/second) per field. Where
91
+ * obstacles overlap, the later-visited one wins on shared faces — both
92
+ * claims are walls, the disagreement is sub-cell.
93
+ *
94
+ * @param {FluidComponent} fluid
95
+ * @param {PosedShape3D} posed
96
+ * @param {Float64Array} aabb
97
+ * @param {number} inflation
98
+ * @param {Float64Array} point
99
+ * @param {number} wvx world-units-per-second obstacle velocity
100
+ * @param {number} wvy
101
+ * @param {number} wvz
102
+ */
103
+ static "__#143@#stamp_wall_velocity"(fluid: FluidComponent, posed: PosedShape3D, aabb: Float64Array, inflation: number, point: Float64Array, wvx: number, wvy: number, wvz: number): void;
104
+ constructor();
105
+ dependencies: (typeof FluidObstacle)[];
106
+ components_used: (ResourceAccessSpecification<typeof RigidBody> | ResourceAccessSpecification<typeof Collider> | ResourceAccessSpecification<typeof Transform> | ResourceAccessSpecification<typeof FluidComponent> | ResourceAccessSpecification<typeof FluidObstacle>)[];
107
+ #private;
108
+ }
109
+ import { System } from "../../../ecs/System.js";
110
+ import { FluidObstacle } from "./FluidObstacle.js";
111
+ import { RigidBody } from "../../ecs/RigidBody.js";
112
+ import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
113
+ import { Collider } from "../../ecs/Collider.js";
114
+ import { Transform } from "../../../ecs/transform/Transform.js";
115
+ import { FluidComponent } from "./FluidComponent.js";
116
+ import { PosedShape3D } from "../../../../core/geom/3d/shape/PosedShape3D.js";
117
+ //# sourceMappingURL=FluidObstacleSystem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FluidObstacleSystem.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/physics/fluid/ecs/FluidObstacleSystem.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH;IA+GI;;OAEG;IACH,sCAFW,cAAc,QAIxB;IAED;;OAEG;IACH,oCAFW,cAAc,QAOxB;IAED;;;;;;;;;;OAUG;IACH,iCANW,cAAc,SACd,YAAY,QACZ,YAAY,aACZ,MAAM,SACN,YAAY,QAiDtB;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACH,4CATW,cAAc,SACd,YAAY,QACZ,YAAY,aACZ,MAAM,SACN,YAAY,OACZ,MAAM,OACN,MAAM,OACN,MAAM,QAqEhB;IAxVe,cAAmB;IAwEnC,uCAA+B;IAE/B,2QAOE;;CAwQL;uBAxVsB,wBAAwB;8BAKjB,oBAAoB;0BAFxB,wBAAwB;4CAJN,uDAAuD;yBAG1E,uBAAuB;0BADtB,qCAAqC;+BAGhC,qBAAqB;6BAPvB,gDAAgD"}