@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
@@ -1,15 +1,14 @@
1
1
  import { assert } from "../../../core/assert.js";
2
2
  import { sor_optimal_omega } from "../../../core/math/linalg/sor_optimal_omega.js";
3
- import { v3_grid_advect_maccormack_scalar } from "./solver/v3_grid_advect_maccormack_scalar.js";
4
- import { v3_grid_advect_maccormack_velocity } from "./solver/v3_grid_advect_maccormack_velocity.js";
5
- import { v3_grid_advect_sl_velocity } from "./solver/v3_grid_advect_sl_velocity.js";
6
3
  import { v3_grid_apply_diffusion } from "./solver/v3_grid_apply_diffusion.js";
7
- import { v3_grid_apply_vorticity_confinement } from "./solver/v3_grid_apply_vorticity_confinement.js";
8
- import { v3_grid_apply_scalar_advection } from "./solver/v3_grid_apply_scalar_advection.js";
9
- import { v3_grid_compute_divergence } from "./solver/v3_grid_compute_divergence.js";
10
4
  import { v3_grid_solve_pressure } from "./solver/v3_grid_solve_pressure.js";
11
5
  import { v3_grid_solve_pressure_pcg } from "./solver/v3_grid_solve_pressure_pcg.js";
12
- import { v3_grid_subtract_pressure_gradient } from "./solver/v3_grid_subtract_pressure_gradient.js";
6
+ import { v3_mac_advect_maccormack_velocity } from "./solver/v3_mac_advect_maccormack_velocity.js";
7
+ import { v3_mac_advect_sl_velocity } from "./solver/v3_mac_advect_sl_velocity.js";
8
+ import { v3_mac_advect_sl_scalar } from "./solver/v3_mac_advect_scalar.js";
9
+ import { v3_mac_apply_vorticity_confinement } from "./solver/v3_mac_apply_vorticity_confinement.js";
10
+ import { v3_mac_compute_divergence } from "./solver/v3_mac_compute_divergence.js";
11
+ import { v3_mac_subtract_pressure_gradient } from "./solver/v3_mac_subtract_pressure_gradient.js";
13
12
 
14
13
  /**
15
14
  * Pressure solver selection. Both solve the same A·p = -div system over the
@@ -34,7 +33,12 @@ export const PressureSolver = {
34
33
  };
35
34
 
36
35
  /**
37
- * Velocity / scalar transport scheme selection.
36
+ * VELOCITY transport scheme selection. Passive scalars always use
37
+ * semi-Lagrangian transport regardless of this setting — gather-form
38
+ * MacCormack is non-conservative (the corrector re-adds roughly double the
39
+ * gather bias; measured 1.7x total dye mass in one second of a sealed
40
+ * vortex, independent of wall handling), and scalar conservation is a hard
41
+ * requirement, not a preference.
38
42
  *
39
43
  * - `"semi-lagrangian"` — first-order back-trace + trilinear gather (Stam
40
44
  * 1999). Cheapest; strongly dissipative — a vortex loses half its kinetic
@@ -42,10 +46,8 @@ export const PressureSolver = {
42
46
  *
43
47
  * - `"maccormack"` — unconditionally stable MacCormack (Selle et al. 2008):
44
48
  * a forward and a backward semi-Lagrangian pass with error correction and
45
- * a monotone min-max limiter. Second-order accurate in space and time at
46
- * ~2× the advection cost and advection is the minority of step() cost
47
- * next to pressure, so the end-to-end price is small. Allocates 3
48
- * additional N-sized scratch buffers, shared with the reflection scheme.
49
+ * a monotone min-max limiter. Second-order accurate in space and time.
50
+ * Allocates 3 additional face-lattice scratch buffers.
49
51
  *
50
52
  * @enum {string}
51
53
  */
@@ -55,7 +57,10 @@ export const AdvectionScheme = {
55
57
  };
56
58
 
57
59
  /**
58
- * Cell-centered 3D Stable Fluids solver (Stam, 1999/2003).
60
+ * 3D incompressible-flow solver on a MAC (staggered) grid — Stam's stable-
61
+ * fluids step structure (1999/2003) with face-centered velocity (Harlow &
62
+ * Welch 1965), an exact discrete Helmholtz projection, and optional
63
+ * advection-reflection / MacCormack transport.
59
64
  *
60
65
  * Holds tuning knobs plus transient working memory (per-step velocity snapshots,
61
66
  * divergence, ping-pong scratch). None of that working memory carries information
@@ -73,8 +78,8 @@ export const AdvectionScheme = {
73
78
  * form of the same two projections (see {@link advection_reflection}).
74
79
  * Advection itself is semi-Lagrangian or MacCormack per
75
80
  * {@link advection_scheme}.
76
- * 6. For each scalar attribute on the field: optionally diffuse, then advect
77
- * with the same scheme.
81
+ * 6. For each scalar attribute on the field: optionally diffuse, then
82
+ * advect (always semi-Lagrangian — see {@link AdvectionScheme}).
78
83
  *
79
84
  * Pressure is warm-started from the previous step's solution (`field.pressure`),
80
85
  * which converges much faster than starting from zero once the flow reaches a
@@ -151,13 +156,19 @@ export class FluidSimulator {
151
156
  *
152
157
  * Why it exists: the solver has no other dissipation mechanism that bounds
153
158
  * energy under sustained forcing. Incompressible projection cannot oppose
154
- * a uniform body force (a uniform field is divergence-free), so a constant
155
- * {@link GlobalFluidEffector} accelerates the fluid without limit — there
156
- * is no terminal state. A small damping rate gives constant forcing a
157
- * terminal velocity of approximately `acceleration / velocity_damping`
158
- * and makes transient gusts decay back to calm, which is what wind-field
159
- * use cases want (drive with {@link AmbientWindFluidEffector} for a
160
- * specific ambient target instead of a force).
159
+ * a uniform body force (a uniform field is divergence-free), so a bare
160
+ * `force` on a {@link GlobalFluidEffector} accelerates the fluid without
161
+ * limit — there is no terminal state. A small damping rate gives constant
162
+ * forcing a terminal velocity of approximately
163
+ * `acceleration / velocity_damping` and makes transient gusts decay back
164
+ * to calm.
165
+ *
166
+ * Relationship to {@link GlobalFluidEffector#drag}: the effector's drag
167
+ * is the same operation expressed as SCENE CONTENT — it relaxes toward a
168
+ * configurable ambient wind and travels with the effector entity. This
169
+ * knob is the SOLVER-level stability primitive: it exists even when no
170
+ * effectors are wired, and always relaxes toward zero. Use the effector
171
+ * for atmosphere design; use this to guarantee boundedness.
161
172
  *
162
173
  * The exponential form is frame-rate independent: two half-steps damp
163
174
  * exactly as much as one full step.
@@ -202,25 +213,23 @@ export class FluidSimulator {
202
213
  vorticity_confinement = 0;
203
214
 
204
215
  /**
205
- * Which transport scheme advects velocity and scalars see
206
- * {@link AdvectionScheme}.
207
- *
208
- * **Default semi-Lagrangian MacCormack is gated on a MAC-staggered
209
- * grid.** On the current collocated discretization the projection has an
210
- * operator-mismatch floor (high-frequency divergence it cannot see, let
211
- * alone remove quality.spec.js scenario 7), and MacCormack's
212
- * anti-diffusive corrector amplifies exactly that residue instead of
213
- * letting first-order smearing absorb it. Measured on the sealed-vortex
214
- * scenario: kinetic energy transiently grows to **1.9×** within 10 s
215
- * (vs SL's monotone decay), and a passive scalar's total mass inflates
216
- * **1.8×** in one second of orbiting. The kernels are correct in
217
- * isolation (their specs transport clean fields sharply and monotonely) —
218
- * it is the interaction with the collocated pressure floor that is
219
- * unstable. Flip this on once the grid is staggered.
216
+ * Which transport scheme advects VELOCITY see {@link AdvectionScheme}
217
+ * (scalars are always semi-Lagrangian; see the enum docstring for why).
218
+ *
219
+ * **Default MacCormack.** On the MAC grid with solid-clipped traces it is
220
+ * measured long-horizon stable (sealed-vortex energy peaks at 0.96x its
221
+ * initial over 10 s, monotone decay thereafter) and retains dramatically
222
+ * more energy than first-order transport: 0.80 vs 0.53 KE after one
223
+ * second (quality.spec.js scenario 2b).
224
+ *
225
+ * Do NOT combine with {@link advection_reflection}: the energy-preserving
226
+ * reflection re-injects the corrector overshoots and the pair pumps
227
+ * energy without bound (10 s peak 3.7x measured both with and without
228
+ * trace clipping). Reflection is the companion for SEMI_LAGRANGIAN.
220
229
  *
221
230
  * @type {string}
222
231
  */
223
- advection_scheme = AdvectionScheme.SEMI_LAGRANGIAN;
232
+ advection_scheme = AdvectionScheme.MACCORMACK;
224
233
 
225
234
  /**
226
235
  * Replace the plain "project → advect → project" sequence with the
@@ -235,25 +244,30 @@ export class FluidSimulator {
235
244
  *
236
245
  * Same two pressure solves as the default double-projection Stam step,
237
246
  * and two half-dt advections instead of one full-dt one — measured cost
238
- * +20% of step() at 32×8×32. The reflection replaces the
247
+ * ~+70% of step() at 32×8×32 on the MAC lattices (each half-step pays the
248
+ * cross-component carrier sampling). The reflection replaces the
239
249
  * energy-DISSIPATING first projection with an energy-PRESERVING
240
250
  * reflection about the divergence-free subspace.
241
251
  *
242
- * Default ON: with semi-Lagrangian advection this is measured stable
243
- * (sealed-vortex kinetic energy never exceeds 0.99× its initial over
244
- * 10 s) while retaining visibly more swirl — peak vorticity 0.73 vs 0.62
245
- * after one second (quality.spec.js scenario 2b). With MacCormack it
246
- * compounds the collocated-floor amplification (3.4× energy growth) —
247
- * see {@link advection_scheme}; that combination should wait for MAC
248
- * staggering.
252
+ * With semi-Lagrangian advection this is measured stable (sealed-vortex
253
+ * kinetic energy never exceeds its initial over 10 s) while retaining
254
+ * visibly more swirl — peak vorticity 0.74 vs 0.68 after one second
255
+ * (quality.spec.js scenario 2b). Do NOT combine with MacCormack — the
256
+ * reflection re-injects the corrector overshoots (10 s energy peak
257
+ * 3.7x); see {@link advection_scheme}.
249
258
  *
250
259
  * Only consulted when {@link project_before_advection} is true — the
251
260
  * scheme is defined around the mid-step projection. With
252
261
  * `project_before_advection = false` this flag is ignored.
253
262
  *
263
+ * Default OFF: the default velocity scheme is MacCormack, with which the
264
+ * reflection is unstable (see {@link advection_scheme}). Enable it when
265
+ * running SEMI_LAGRANGIAN velocity transport — that pairing is measured
266
+ * stable and retains visibly more swirl than plain SL.
267
+ *
254
268
  * @type {boolean}
255
269
  */
256
- advection_reflection = true;
270
+ advection_reflection = false;
257
271
 
258
272
  /**
259
273
  * Run the pressure projection *before* self-advection in addition to after.
@@ -318,13 +332,12 @@ export class FluidSimulator {
318
332
  pressure_iteration_scale = 0.25;
319
333
 
320
334
  // ────────────────────────────────────────────────────────────────────────────
321
- // Transient scratch — overwritten before read on every step. Cached by
322
- // cell-count so we don't allocate per step. Their value never crosses a step
323
- // boundary; if it did it would belong on the field (see `field.pressure`).
335
+ // Transient scratch — overwritten before read on every step. Cached and
336
+ // grown monotonically so we don't allocate per step. Their value never
337
+ // crosses a step boundary; if it did it would belong on the field (see
338
+ // `field.pressure`). Velocity-shaped buffers are sized per face lattice.
324
339
  // ────────────────────────────────────────────────────────────────────────────
325
340
 
326
- #scratch_cells = 0;
327
-
328
341
  /** @type {Float32Array|null} */ #prev_x = null;
329
342
  /** @type {Float32Array|null} */ #prev_y = null;
330
343
  /** @type {Float32Array|null} */ #prev_z = null;
@@ -340,31 +353,46 @@ export class FluidSimulator {
340
353
  /** @type {Float32Array|null} */ #pcg_As = null;
341
354
  /** @type {Float32Array|null} */ #pcg_precon = null;
342
355
 
343
- // Auxiliary velocity trio (allocated lazily): MacCormack's forward-pass
344
- // scratch AND the reflection scheme's ũ snapshot the two uses never
345
- // overlap in time within a step.
346
- #aux_scratch_cells = 0;
356
+ // Auxiliary velocity trio (allocated lazily, face-lattice sized): the
357
+ // reflection scheme's ũ snapshot / carrier copy, and vorticity
358
+ // confinement's curl scratch — the uses never overlap in time.
347
359
  /** @type {Float32Array|null} */ #aux_x = null;
348
360
  /** @type {Float32Array|null} */ #aux_y = null;
349
361
  /** @type {Float32Array|null} */ #aux_z = null;
350
362
 
363
+ // MacCormack forward-pass trio (allocated lazily, face-lattice sized).
364
+ // Deliberately separate from #aux: in the reflection path #aux holds the
365
+ // carrier during the very advection that needs this scratch.
366
+ /** @type {Float32Array|null} */ #mc_x = null;
367
+ /** @type {Float32Array|null} */ #mc_y = null;
368
+ /** @type {Float32Array|null} */ #mc_z = null;
369
+
351
370
  /**
352
- * Grow the cached scratch buffers if the requested size exceeds what's allocated.
353
- * Idempotent and monotonic never shrinks. Called at the top of {@link step} and
354
- * {@link project} so user code never has to think about it.
355
- * @param {number} cell_count
371
+ * Grow the cached scratch buffers to fit the field's lattices. The prev
372
+ * trio and the diffusion scratch are sized per FACE grid (the largest of
373
+ * which also covers every cell-count use); divergence and scalar scratch
374
+ * are cell-count. Idempotent and monotonic — never shrinks. Called at the
375
+ * top of {@link step} so user code never has to think about it.
376
+ * @param {FluidField} field
356
377
  */
357
- #ensure_scratch(cell_count) {
358
- if (this.#scratch_cells >= cell_count) {
359
- return;
378
+ #ensure_scratch(field) {
379
+ const res = field.getResolution();
380
+ const n_u = (res[0] + 1) * res[1] * res[2];
381
+ const n_v = res[0] * (res[1] + 1) * res[2];
382
+ const n_w = res[0] * res[1] * (res[2] + 1);
383
+ const n_cell = field.cellCount();
384
+ const n_face_max = Math.max(n_u, n_v, n_w);
385
+
386
+ if (this.#prev_x === null || this.#prev_x.length < n_u) this.#prev_x = new Float32Array(n_u);
387
+ if (this.#prev_y === null || this.#prev_y.length < n_v) this.#prev_y = new Float32Array(n_v);
388
+ if (this.#prev_z === null || this.#prev_z.length < n_w) this.#prev_z = new Float32Array(n_w);
389
+ if (this.#diffusion_scratch === null || this.#diffusion_scratch.length < n_face_max) {
390
+ this.#diffusion_scratch = new Float32Array(n_face_max);
360
391
  }
361
- this.#scratch_cells = cell_count;
362
- this.#prev_x = new Float32Array(cell_count);
363
- this.#prev_y = new Float32Array(cell_count);
364
- this.#prev_z = new Float32Array(cell_count);
365
- this.#divergence = new Float32Array(cell_count);
366
- this.#diffusion_scratch = new Float32Array(cell_count);
367
- this.#scalar_scratch = new Float32Array(cell_count);
392
+ if (this.#scalar_scratch === null || this.#scalar_scratch.length < n_cell) {
393
+ this.#scalar_scratch = new Float32Array(n_cell);
394
+ }
395
+ // #divergence is dtype-tracked separately by #ensure_divergence_matches.
368
396
  }
369
397
 
370
398
  /**
@@ -385,53 +413,66 @@ export class FluidSimulator {
385
413
  }
386
414
 
387
415
  /**
388
- * Lazily allocate the auxiliary velocity trio (3 N-sized Float32 buffers)
389
- * used by MacCormack advection and the reflection scheme. Idempotent and
416
+ * Lazily allocate the auxiliary velocity trio (face-lattice sized) used by
417
+ * the reflection scheme and vorticity confinement. Idempotent and
390
418
  * monotonic.
391
- * @param {number} cell_count
419
+ * @param {FluidField} field
392
420
  */
393
- #ensure_aux_scratch(cell_count) {
394
- if (this.#aux_scratch_cells >= cell_count) {
395
- return;
396
- }
397
- this.#aux_scratch_cells = cell_count;
398
- this.#aux_x = new Float32Array(cell_count);
399
- this.#aux_y = new Float32Array(cell_count);
400
- this.#aux_z = new Float32Array(cell_count);
421
+ #ensure_aux_scratch(field) {
422
+ const res = field.getResolution();
423
+ const n_u = (res[0] + 1) * res[1] * res[2];
424
+ const n_v = res[0] * (res[1] + 1) * res[2];
425
+ const n_w = res[0] * res[1] * (res[2] + 1);
426
+ if (this.#aux_x === null || this.#aux_x.length < n_u) this.#aux_x = new Float32Array(n_u);
427
+ if (this.#aux_y === null || this.#aux_y.length < n_v) this.#aux_y = new Float32Array(n_v);
428
+ if (this.#aux_z === null || this.#aux_z.length < n_w) this.#aux_z = new Float32Array(n_w);
429
+ }
430
+
431
+ /**
432
+ * Lazily allocate the MacCormack forward-pass trio (face-lattice sized).
433
+ * Idempotent and monotonic.
434
+ * @param {FluidField} field
435
+ */
436
+ #ensure_mc_scratch(field) {
437
+ const res = field.getResolution();
438
+ const n_u = (res[0] + 1) * res[1] * res[2];
439
+ const n_v = res[0] * (res[1] + 1) * res[2];
440
+ const n_w = res[0] * res[1] * (res[2] + 1);
441
+ if (this.#mc_x === null || this.#mc_x.length < n_u) this.#mc_x = new Float32Array(n_u);
442
+ if (this.#mc_y === null || this.#mc_y.length < n_v) this.#mc_y = new Float32Array(n_v);
443
+ if (this.#mc_z === null || this.#mc_z.length < n_w) this.#mc_z = new Float32Array(n_w);
401
444
  }
402
445
 
403
446
  /**
404
447
  * Advect the field's velocity from `sources` along the given carrier, into
405
448
  * the field's velocity buffers, using the configured
406
449
  * {@link advection_scheme}. The carrier may alias the field's own velocity
407
- * arrays (it is point-read only); sources must not.
450
+ * arrays; sources must not.
408
451
  *
409
452
  * @param {FluidField} field
410
453
  * @param {number[]} res
411
454
  * @param {number} dt
412
- * @param {Float32Array[]} sources `[src_x, src_y, src_z]`
413
- * @param {Float32Array} carrier_x
414
- * @param {Float32Array} carrier_y
415
- * @param {Float32Array} carrier_z
455
+ * @param {Float32Array[]} sources `[src_u, src_v, src_w]` face grids
456
+ * @param {Float32Array[]} carrier `[car_u, car_v, car_w]` face grids
416
457
  */
417
- #advect_velocity(field, res, dt, sources, carrier_x, carrier_y, carrier_z) {
458
+ #advect_velocity(field, res, dt, sources, carrier) {
418
459
  const outputs = [field.velocity_x, field.velocity_y, field.velocity_z];
419
460
  if (this.advection_scheme === AdvectionScheme.MACCORMACK) {
420
- this.#ensure_aux_scratch(field.cellCount());
421
- v3_grid_advect_maccormack_velocity(
422
- outputs, sources,
423
- carrier_x, carrier_y, carrier_z,
424
- [this.#aux_x, this.#aux_y, this.#aux_z],
461
+ this.#ensure_mc_scratch(field);
462
+ v3_mac_advect_maccormack_velocity(
463
+ outputs, sources, carrier,
464
+ [this.#mc_x, this.#mc_y, this.#mc_z],
425
465
  res[0], res[1], res[2],
426
466
  dt,
467
+ field.face_solid_x, field.face_solid_y, field.face_solid_z,
427
468
  field.solid
428
469
  );
429
470
  } else {
430
- v3_grid_advect_sl_velocity(
431
- outputs, sources,
432
- carrier_x, carrier_y, carrier_z,
471
+ v3_mac_advect_sl_velocity(
472
+ outputs, sources, carrier,
433
473
  res[0], res[1], res[2],
434
474
  dt,
475
+ field.face_solid_x, field.face_solid_y, field.face_solid_z,
435
476
  field.solid
436
477
  );
437
478
  }
@@ -497,26 +538,31 @@ export class FluidSimulator {
497
538
  }
498
539
 
499
540
  /**
500
- * Projection body — assumes `field.solid_neighbour_mask` is already fresh.
541
+ * Projection body — assumes the field's derived masks (cell neighbour
542
+ * mask, pressure diagonal, face pins) are already fresh.
543
+ *
544
+ * On the MAC layout this is an exact discrete Helmholtz projection: the
545
+ * face-difference divergence and the face gradient compose to exactly the
546
+ * 7-point Laplacian the solvers invert, so post-projection divergence is
547
+ * zero up to solver residual — including beside walls.
548
+ *
501
549
  * @param {FluidField} field
502
550
  */
503
551
  #project_with_current_mask(field) {
504
- this.#ensure_scratch(field.cellCount());
505
552
  this.#ensure_divergence_matches(field);
506
553
 
507
554
  const res = field.getResolution();
508
- const solid = field.solid;
509
555
  const iterations = this.resolvePressureIterations(res[0], res[1], res[2]);
510
556
 
511
557
  if (iterations === 0) {
512
558
  return;
513
559
  }
514
560
 
515
- v3_grid_compute_divergence(
561
+ v3_mac_compute_divergence(
516
562
  this.#divergence,
517
563
  field.velocity_x, field.velocity_y, field.velocity_z,
518
564
  res[0], res[1], res[2],
519
- solid
565
+ field.solid
520
566
  );
521
567
 
522
568
  if (this.pressure_solver === PressureSolver.MICPCG) {
@@ -538,30 +584,42 @@ export class FluidSimulator {
538
584
  );
539
585
  }
540
586
 
541
- v3_grid_subtract_pressure_gradient(
587
+ v3_mac_subtract_pressure_gradient(
542
588
  field.velocity_x, field.velocity_y, field.velocity_z,
543
589
  field.pressure,
544
- res[0], res[1], res[2],
545
- field.solid_neighbour_mask
590
+ field.face_solid_x, field.face_solid_y, field.face_solid_z,
591
+ res[0], res[1], res[2]
546
592
  );
547
593
  }
548
594
 
549
595
  /**
550
- * Diffuse a single velocity component in-place, using `source_copy_buf` as the
551
- * "before" snapshot and `#diffusion_scratch` as the ping-pong scratch.
552
- * @param {Float32Array} component
596
+ * Diffuse a single velocity component (one face lattice) in-place, using
597
+ * `source_copy_buf` as the "before" snapshot and `#diffusion_scratch` as
598
+ * the ping-pong scratch. The face-pin mask plays the diffusion kernel's
599
+ * "solid" role: pinned faces are frozen and act as no-flux neighbours.
600
+ *
601
+ * The shared scratch buffers can be larger than this lattice (they are
602
+ * sized to the largest one); the kernel's internal whole-buffer copies
603
+ * require exact-length views.
604
+ *
605
+ * @param {Float32Array} component Face lattice, exact length.
553
606
  * @param {Float32Array} source_copy_buf
554
- * @param {number[]} res
555
- * @param {Uint8Array} solid
607
+ * @param {number} dim_x
608
+ * @param {number} dim_y
609
+ * @param {number} dim_z
610
+ * @param {Uint8Array} face_solid
556
611
  */
557
- #diffuse_velocity_component(component, source_copy_buf, res, solid) {
558
- source_copy_buf.set(component);
612
+ #diffuse_velocity_component(component, source_copy_buf, dim_x, dim_y, dim_z, face_solid) {
613
+ const n = component.length;
614
+ const copy = source_copy_buf.length === n ? source_copy_buf : source_copy_buf.subarray(0, n);
615
+ const scratch = this.#diffusion_scratch.length === n ? this.#diffusion_scratch : this.#diffusion_scratch.subarray(0, n);
616
+ copy.set(component);
559
617
  v3_grid_apply_diffusion(
560
- component, source_copy_buf, this.#diffusion_scratch,
561
- res[0], res[1], res[2],
618
+ component, copy, scratch,
619
+ dim_x, dim_y, dim_z,
562
620
  this.velocity_diffusion_rate,
563
621
  this.velocity_diffusion_iterations,
564
- solid
622
+ face_solid
565
623
  );
566
624
  }
567
625
 
@@ -595,57 +653,75 @@ export class FluidSimulator {
595
653
  const cell_count = field.cellCount();
596
654
  assert.greaterThan(cell_count, 0, "field has not been built");
597
655
 
598
- this.#ensure_scratch(cell_count);
656
+ this.#ensure_scratch(field);
599
657
 
600
658
  const res = field.getResolution();
659
+ const n_u = field.velocity_x.length;
660
+ const n_v = field.velocity_y.length;
661
+ const n_w = field.velocity_z.length;
601
662
 
602
663
  // 1. External forces.
603
664
  for (let i = 0; i < effectors.length; i++) {
604
665
  effectors[i].apply(field, time_delta, world_to_grid);
605
666
  }
606
667
 
668
+ // Refresh the pre-baked derived masks (cell neighbour mask, pressure
669
+ // diagonal, face pins) ONCE for the whole step — after effectors (a
670
+ // custom effector may splat solids), before anything that consumes
671
+ // them. Both projections below reuse them. Faces that just became
672
+ // pinned have their velocity zeroed here (static-wall default);
673
+ // already-pinned faces keep whatever boundary condition their solid's
674
+ // owner wrote (FluidObstacleSystem writes moving-wall velocities).
675
+ field.recomputeSolidNeighbourMask();
676
+
677
+ const solid = field.solid;
678
+
607
679
  // 1b. Velocity damping — the solver's only energy sink under sustained
608
- // forcing. Solid cells are included in the multiply (their velocity
609
- // is 0, so damping them is a no-op and the loop stays branch-free).
680
+ // forcing. Pinned faces are SKIPPED: their value is the wall's
681
+ // boundary condition, not fluid momentum.
610
682
  if (this.velocity_damping > 0) {
611
683
  const decay = Math.exp(-this.velocity_damping * time_delta);
612
- const vx = field.velocity_x;
613
- const vy = field.velocity_y;
614
- const vz = field.velocity_z;
615
- for (let i = 0; i < cell_count; i++) {
616
- vx[i] *= decay;
617
- vy[i] *= decay;
618
- vz[i] *= decay;
684
+ const vu = field.velocity_x;
685
+ const vv = field.velocity_y;
686
+ const vw = field.velocity_z;
687
+ const pin_u = field.face_solid_x;
688
+ const pin_v = field.face_solid_y;
689
+ const pin_w = field.face_solid_z;
690
+ for (let i = 0; i < n_u; i++) {
691
+ if (pin_u[i] === 0) vu[i] *= decay;
692
+ }
693
+ for (let i = 0; i < n_v; i++) {
694
+ if (pin_v[i] === 0) vv[i] *= decay;
695
+ }
696
+ for (let i = 0; i < n_w; i++) {
697
+ if (pin_w[i] === 0) vw[i] *= decay;
619
698
  }
620
699
  }
621
700
 
622
701
  // 1c. Vorticity confinement — an additional force term that sharpens
623
- // vortex cores (see the knob's docstring). Uses the prev trio and
624
- // the diffusion scratch as working memory; both are overwritten by
625
- // later phases anyway.
702
+ // vortex cores (see the knob's docstring). Needs the fresh face
703
+ // pins; uses the prev + aux trios and the diffusion scratch as
704
+ // working memory, all overwritten by later phases anyway.
626
705
  if (this.vorticity_confinement > 0) {
627
- v3_grid_apply_vorticity_confinement(
706
+ this.#ensure_aux_scratch(field);
707
+ v3_mac_apply_vorticity_confinement(
628
708
  field.velocity_x, field.velocity_y, field.velocity_z,
629
- this.#prev_x, this.#prev_y, this.#prev_z, this.#diffusion_scratch,
709
+ field.face_solid_x, field.face_solid_y, field.face_solid_z,
710
+ this.#prev_x, this.#prev_y, this.#prev_z,
711
+ this.#aux_x, this.#aux_y, this.#aux_z, this.#diffusion_scratch,
630
712
  res[0], res[1], res[2],
631
713
  this.vorticity_confinement * time_delta,
632
- field.solid
714
+ solid
633
715
  );
634
716
  }
635
717
 
636
- // Refresh the pre-baked solid-neighbour mask ONCE for the whole step —
637
- // after effectors (a custom effector may splat solids), before anything
638
- // that consumes it. Both projections below reuse it, saving an O(N)
639
- // byte pass versus recomputing per projection.
640
- field.recomputeSolidNeighbourMask();
641
-
642
- const solid = field.solid;
643
-
644
- // 2. Velocity diffusion (viscosity). Cheap to skip when disabled.
718
+ // 2. Velocity diffusion (viscosity). Cheap to skip when disabled. Each
719
+ // face lattice diffuses with its own dimensions; the face-pin masks
720
+ // make walls no-flux exactly like solid cells used to.
645
721
  if (this.velocity_diffusion_rate > 0 && this.velocity_diffusion_iterations > 0) {
646
- this.#diffuse_velocity_component(field.velocity_x, this.#prev_x, res, solid);
647
- this.#diffuse_velocity_component(field.velocity_y, this.#prev_y, res, solid);
648
- this.#diffuse_velocity_component(field.velocity_z, this.#prev_z, res, solid);
722
+ this.#diffuse_velocity_component(field.velocity_x, this.#prev_x, res[0] + 1, res[1], res[2], field.face_solid_x);
723
+ this.#diffuse_velocity_component(field.velocity_y, this.#prev_y, res[0], res[1] + 1, res[2], field.face_solid_y);
724
+ this.#diffuse_velocity_component(field.velocity_z, this.#prev_z, res[0], res[1], res[2] + 1, field.face_solid_z);
649
725
  }
650
726
 
651
727
  // 3-5. Transport block: project + self-advect (+ optional reflection).
@@ -656,17 +732,15 @@ export class FluidSimulator {
656
732
  // the `advection_reflection` docstring. Same two projections as the
657
733
  // plain path; two half-dt advections instead of one full-dt one.
658
734
  const half_dt = 0.5 * time_delta;
659
- const n = cell_count;
660
735
 
661
736
  // ũ = advect(u, u, dt/2)
662
737
  this.#prev_x.set(field.velocity_x);
663
738
  this.#prev_y.set(field.velocity_y);
664
739
  this.#prev_z.set(field.velocity_z);
665
- this.#advect_velocity(field, res, half_dt, prev, this.#prev_x, this.#prev_y, this.#prev_z);
740
+ this.#advect_velocity(field, res, half_dt, prev, prev);
666
741
 
667
- // Snapshot ũ (after the advect — MacCormack uses the same aux trio
668
- // as its forward scratch during the kernel).
669
- this.#ensure_aux_scratch(n);
742
+ // Snapshot ũ.
743
+ this.#ensure_aux_scratch(field);
670
744
  this.#aux_x.set(field.velocity_x);
671
745
  this.#aux_y.set(field.velocity_y);
672
746
  this.#aux_z.set(field.velocity_z);
@@ -674,19 +748,23 @@ export class FluidSimulator {
674
748
  // u½ = project(ũ)
675
749
  this.#project_with_current_mask(field);
676
750
 
677
- // û = 2·u½ − ũ — the reflection about the divergence-free subspace.
751
+ // û = 2·u½ − ũ — the reflection about the divergence-free subspace,
752
+ // per face lattice.
678
753
  const ax = this.#aux_x, ay = this.#aux_y, az = this.#aux_z;
679
754
  const fx = field.velocity_x, fy = field.velocity_y, fz = field.velocity_z;
680
755
  const px = this.#prev_x, py = this.#prev_y, pz = this.#prev_z;
681
- for (let i = 0; i < n; i++) {
682
- px[i] = 2 * fx[i] - ax[i];
683
- py[i] = 2 * fy[i] - ay[i];
684
- pz[i] = 2 * fz[i] - az[i];
685
- }
686
-
687
- // u′ = advect(û, u½, dt/2). Output aliases the carrier (the field's
688
- // own velocity) allowed, the carrier is point-read only.
689
- this.#advect_velocity(field, res, half_dt, prev, fx, fy, fz);
756
+ for (let i = 0; i < n_u; i++) px[i] = 2 * fx[i] - ax[i];
757
+ for (let i = 0; i < n_v; i++) py[i] = 2 * fy[i] - ay[i];
758
+ for (let i = 0; i < n_w; i++) pz[i] = 2 * fz[i] - az[i];
759
+
760
+ // u′ = advect(û, u½, dt/2). The advection kernels sample the
761
+ // carrier's OTHER lattices spatially, so the carrier must not
762
+ // alias the outputs copy into the aux trio (ũ there is dead,
763
+ // already folded into û).
764
+ this.#aux_x.set(fx);
765
+ this.#aux_y.set(fy);
766
+ this.#aux_z.set(fz);
767
+ this.#advect_velocity(field, res, half_dt, prev, [ax, ay, az]);
690
768
 
691
769
  // u₁ = project(u′)
692
770
  this.#project_with_current_mask(field);
@@ -699,11 +777,11 @@ export class FluidSimulator {
699
777
  this.#prev_x.set(field.velocity_x);
700
778
  this.#prev_y.set(field.velocity_y);
701
779
  this.#prev_z.set(field.velocity_z);
702
- this.#advect_velocity(field, res, time_delta, prev, this.#prev_x, this.#prev_y, this.#prev_z);
780
+ this.#advect_velocity(field, res, time_delta, prev, prev);
703
781
 
704
782
  // Re-project after advection. Solids cannot have changed since the
705
783
  // top-of-step mask refresh (only the simulator has touched the
706
- // field), so the mask is still fresh.
784
+ // field), so the masks are still fresh.
707
785
  this.#project_with_current_mask(field);
708
786
  }
709
787
 
@@ -714,8 +792,13 @@ export class FluidSimulator {
714
792
 
715
793
  if (this.scalar_diffusion_rate > 0 && this.scalar_diffusion_iterations > 0) {
716
794
  this.#scalar_scratch.set(attr.data);
795
+ // The diffusion scratch is face-lattice sized; the kernel's
796
+ // internal whole-buffer copies need an exact cell-count view.
797
+ const ping_pong = this.#diffusion_scratch.length === cell_count
798
+ ? this.#diffusion_scratch
799
+ : this.#diffusion_scratch.subarray(0, cell_count);
717
800
  v3_grid_apply_diffusion(
718
- attr.data, this.#scalar_scratch, this.#diffusion_scratch,
801
+ attr.data, this.#scalar_scratch, ping_pong,
719
802
  res[0], res[1], res[2],
720
803
  this.scalar_diffusion_rate,
721
804
  this.scalar_diffusion_iterations,
@@ -724,25 +807,16 @@ export class FluidSimulator {
724
807
  }
725
808
 
726
809
  this.#scalar_scratch.set(attr.data);
727
- if (this.advection_scheme === AdvectionScheme.MACCORMACK) {
728
- // #diffusion_scratch is free here (the optional diffusion above
729
- // has fully consumed it) — reuse it as the forward-pass buffer.
730
- v3_grid_advect_maccormack_scalar(
731
- attr.data, this.#scalar_scratch, this.#diffusion_scratch,
732
- field.velocity_x, field.velocity_y, field.velocity_z,
733
- res[0], res[1], res[2],
734
- time_delta,
735
- solid
736
- );
737
- } else {
738
- v3_grid_apply_scalar_advection(
739
- attr.data, this.#scalar_scratch,
740
- field.velocity_x, field.velocity_y, field.velocity_z,
741
- res[0], res[1], res[2],
742
- time_delta,
743
- solid
744
- );
745
- }
810
+ // Scalars are ALWAYS semi-Lagrangian, independent of
811
+ // advection_scheme — gather-MacCormack is non-conservative (see
812
+ // the AdvectionScheme docstring).
813
+ v3_mac_advect_sl_scalar(
814
+ attr.data, this.#scalar_scratch,
815
+ field.velocity_x, field.velocity_y, field.velocity_z,
816
+ res[0], res[1], res[2],
817
+ time_delta,
818
+ solid
819
+ );
746
820
  }
747
821
  }
748
822
  }