Skip to main content

alice_physics/
sim_modifier.rs

1//! Simulation Modifier Core System
2//!
3//! Trait and wrapper for physics-driven SDF modifiers.
4//! `ModifiedSdf` chains multiple modifiers that read simulation
5//! fields (temperature, pressure, stress) and alter the SDF
6//! distance at evaluation time.
7//!
8//! # Pattern
9//!
10//! Each modifier:
11//! 1. Owns its simulation field data (`ScalarField3D`)
12//! 2. Has `update(dt)` to advance simulation state
13//! 3. `modify_distance(x, y, z, dist)` alters SDF distance
14//!
15//! # World participants
16//!
17//! [`crate::thermal::ThermalModifier`], [`crate::phase_change::PhaseChangeModifier`],
18//! [`crate::pressure::PressureModifier`], [`crate::fracture::FractureModifier`]
19//! and [`crate::erosion::ErosionModifier`] implement
20//! [`crate::world_participant::Participant`], so they advance with world
21//! time when registered with the world (or driven by
22//! [`crate::world_participant::run_substep`]) and are carried in snapshots.
23//!
24//! * **Time.** [`crate::world_participant::StepRule::FollowSubstep`]: each
25//!   substep calls `update(h)` once, with the substep width `h` converted by
26//!   [`crate::math::Fix128::to_f32`] (a fixed rounding, the same on every
27//!   platform). A width `h ≤ 0` never reaches the modifier: the world refuses
28//!   it in the step rule check before any participant runs, so the modifier
29//!   has no path of its own for it. `substep` never returns `Err`; the f32
30//!   state is not checked for non-finite values.
31//! * **State payload** (version 1, little endian): `version: u32`, then every
32//!   field of the configuration in declaration order (`f32` as its bits,
33//!   `usize` as `u64`, `ErosionType` as `u8`: `Wind` 0, `Water` 1,
34//!   `Chemical` 2, `Ablation` 3), `enabled: u8` (0 or 1), each
35//!   `ScalarField3D` as `nx`, `ny`, `nz` (`u64`), the min and max corners
36//!   (3 `f32` each) and the cells, then the lists (`count: u64` and items):
37//!   the heat sources of the thermal modifier (tag `u8` `Point` 0: x, y, z,
38//!   power, radius / `Volume` 1: min, max, power) and the cracks of the
39//!   fracture modifier (start, end, direction, length, `active: u8`). The
40//!   configuration is part of the state: a restore brings it back. A field is
41//!   rebuilt from its sizes and bounds, so its cell size is recomputed from
42//!   the stored bounds. `check_state` refuses another version, a byte that
43//!   is not a valid bool or tag, a size whose cell count overflows, missing
44//!   and trailing bytes, without changing the modifier.
45//! * **Observations** are listed on each `Participant` impl; field values are
46//!   converted with [`crate::math::Fix128::from_f32`] (exact for finite
47//!   values).
48//! * **Not coupled yet.** The participant only advances in world time. It
49//!   stages no force on any body, reads no contact or body state (pressure
50//!   loads, erosion exposure and fracture stress are still applied by the caller
51//!   through the inherent methods), declares no ports and exchanges no shared
52//!   field (the thermal and phase-change temperatures are reconciled only
53//!   through [`crate::coupled_field`]). A modifier registered with the world
54//!   is owned by it, so it is not at the same time in a [`ModifiedSdf`]
55//!   chain; the changed geometry does not reach the world's SDF colliders.
56//!
57//! # Example
58//!
59//! ```
60//! use alice_physics::sdf_collider::ClosureSdf;
61//! use alice_physics::sim_modifier::ModifiedSdf;
62//! use alice_physics::thermal::{ThermalModifier, ThermalConfig};
63//! use alice_physics::pressure::{PressureModifier, PressureConfig};
64//! use alice_physics::sdf_collider::SdfField;
65//!
66//! let sphere_sdf = ClosureSdf::new(
67//!     |x, y, z| (x*x + y*y + z*z).sqrt() - 1.0,
68//!     |x, y, z| { let l = (x*x + y*y + z*z).sqrt().max(1e-6); (x/l, y/l, z/l) },
69//! );
70//! let bounds = (-2.0, -2.0, -2.0);
71//! let bounds_max = (2.0, 2.0, 2.0);
72//! let thermal = ThermalModifier::new(ThermalConfig::default(), 4, bounds, bounds_max);
73//! let pressure = PressureModifier::new(PressureConfig::default(), 4, bounds, bounds_max);
74//!
75//! let modified = ModifiedSdf::new(Box::new(sphere_sdf))
76//!     .with_modifier(Box::new(thermal))
77//!     .with_modifier(Box::new(pressure));
78//!
79//! let dist = modified.distance(0.0, 0.0, 0.0);
80//! ```
81//!
82//! Author: Moroya Sakamoto
83
84use crate::math::Fix128;
85use crate::sdf_collider::{fd_normal, SdfField, FD_NORMAL_BASE_EPS};
86use crate::sim_field::ScalarField3D;
87use crate::world_participant::{ObservationSink, StateError};
88
89#[cfg(not(feature = "std"))]
90use alloc::boxed::Box;
91#[cfg(not(feature = "std"))]
92use alloc::vec::Vec;
93
94// ============================================================================
95// PhysicsModifier Trait
96// ============================================================================
97
98/// Trait for simulation-driven SDF modifiers
99pub trait PhysicsModifier: Send + Sync {
100    /// Modify the SDF distance at a world-space point.
101    ///
102    /// Returns the modified distance. Positive offset = surface recedes
103    /// (material removed). Negative offset = surface expands.
104    fn modify_distance(&self, x: f32, y: f32, z: f32, original_dist: f32) -> f32;
105
106    /// Advance the simulation state by dt seconds.
107    fn update(&mut self, dt: f32);
108
109    /// Name of this modifier (for debugging)
110    fn name(&self) -> &str;
111
112    /// Whether this modifier has any active effect
113    fn is_active(&self) -> bool {
114        true
115    }
116}
117
118// ============================================================================
119// ModifiedSdf
120// ============================================================================
121
122/// SDF wrapper that applies a chain of physics modifiers.
123///
124/// Implements `SdfField`, so it can be used anywhere an SDF is expected.
125pub struct ModifiedSdf {
126    /// Original SDF field
127    original: Box<dyn SdfField>,
128    /// Chain of modifiers (applied in order)
129    modifiers: Vec<Box<dyn PhysicsModifier>>,
130    /// Epsilon for normal computation
131    normal_eps: f32,
132}
133
134impl ModifiedSdf {
135    /// Create a new modified SDF wrapping the original field
136    #[must_use]
137    pub fn new(original: Box<dyn SdfField>) -> Self {
138        Self {
139            original,
140            modifiers: Vec::new(),
141            normal_eps: FD_NORMAL_BASE_EPS,
142        }
143    }
144
145    /// Add a modifier to the chain
146    #[must_use]
147    pub fn with_modifier(mut self, modifier: Box<dyn PhysicsModifier>) -> Self {
148        self.modifiers.push(modifier);
149        self
150    }
151
152    /// Add a modifier (mutable)
153    pub fn add_modifier(&mut self, modifier: Box<dyn PhysicsModifier>) {
154        self.modifiers.push(modifier);
155    }
156
157    /// Remove all modifiers
158    pub fn clear_modifiers(&mut self) {
159        self.modifiers.clear();
160    }
161
162    /// Number of active modifiers
163    #[must_use]
164    pub fn modifier_count(&self) -> usize {
165        self.modifiers.len()
166    }
167
168    /// Update all modifier simulations
169    pub fn update(&mut self, dt: f32) {
170        for m in &mut self.modifiers {
171            m.update(dt);
172        }
173    }
174
175    /// Get mutable access to a modifier by index
176    #[allow(clippy::option_if_let_else)]
177    pub fn modifier_mut(&mut self, index: usize) -> Option<&mut dyn PhysicsModifier> {
178        match self.modifiers.get_mut(index) {
179            Some(m) => Some(m.as_mut()),
180            None => None,
181        }
182    }
183
184    /// Evaluate the modified distance at a point
185    #[inline]
186    fn eval_distance(&self, x: f32, y: f32, z: f32) -> f32 {
187        let mut d = self.original.distance(x, y, z);
188        for m in &self.modifiers {
189            if m.is_active() {
190                d = m.modify_distance(x, y, z, d);
191            }
192        }
193        d
194    }
195}
196
197impl SdfField for ModifiedSdf {
198    #[inline]
199    fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
200        self.eval_distance(x, y, z)
201    }
202
203    #[inline]
204    fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
205        fd_normal(
206            |a, b, c| self.eval_distance(a, b, c),
207            self.normal_eps,
208            x,
209            y,
210            z,
211        )
212    }
213
214    fn distance_and_normal(&self, x: f32, y: f32, z: f32) -> (f32, (f32, f32, f32)) {
215        let d = self.eval_distance(x, y, z);
216        let n = self.normal(x, y, z);
217        (d, n)
218    }
219}
220
221// ============================================================================
222// SingleModifiedSdf (lightweight single-modifier wrapper)
223// ============================================================================
224
225/// Lightweight SDF wrapper for a single modifier (avoids Vec overhead).
226///
227/// Evaluates as a [`ModifiedSdf`] holding that one modifier: the distance is
228/// `modifier.modify_distance(p, original(p))` while
229/// [`PhysicsModifier::is_active`] is true and `original(p)` otherwise, and
230/// [`Self::update`] advances the modifier whether or not it is active.
231pub struct SingleModifiedSdf<M: PhysicsModifier> {
232    /// Original SDF
233    pub original: Box<dyn SdfField>,
234    /// The modifier
235    pub modifier: M,
236    /// Epsilon for normals
237    normal_eps: f32,
238}
239
240impl<M: PhysicsModifier> SingleModifiedSdf<M> {
241    /// Create wrapper with a single modifier
242    pub fn new(original: Box<dyn SdfField>, modifier: M) -> Self {
243        Self {
244            original,
245            modifier,
246            normal_eps: FD_NORMAL_BASE_EPS,
247        }
248    }
249
250    /// Update the modifier simulation
251    pub fn update(&mut self, dt: f32) {
252        self.modifier.update(dt);
253    }
254
255    /// The original distance, passed through the modifier when it is active
256    /// (an inactive modifier is skipped, as in [`ModifiedSdf`]).
257    #[inline]
258    fn eval_distance(&self, x: f32, y: f32, z: f32) -> f32 {
259        let d = self.original.distance(x, y, z);
260        if self.modifier.is_active() {
261            self.modifier.modify_distance(x, y, z, d)
262        } else {
263            d
264        }
265    }
266}
267
268impl<M: PhysicsModifier> SdfField for SingleModifiedSdf<M> {
269    #[inline]
270    fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
271        self.eval_distance(x, y, z)
272    }
273
274    #[inline]
275    fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
276        fd_normal(
277            |a, b, c| self.eval_distance(a, b, c),
278            self.normal_eps,
279            x,
280            y,
281            z,
282        )
283    }
284
285    fn distance_and_normal(&self, x: f32, y: f32, z: f32) -> (f32, (f32, f32, f32)) {
286        let d = self.eval_distance(x, y, z);
287        let n = self.normal(x, y, z);
288        (d, n)
289    }
290}
291
292// ============================================================================
293// World participation (state payload and observations)
294// ============================================================================
295
296/// Version written in front of every modifier state payload.
297pub(crate) const MODIFIER_STATE_VERSION: u32 = 1;
298
299/// Appends a modifier state payload, little endian, version first.
300pub(crate) struct StateWriter<'a> {
301    out: &'a mut Vec<u8>,
302}
303
304impl<'a> StateWriter<'a> {
305    /// Starts a payload in `out` with [`MODIFIER_STATE_VERSION`].
306    pub(crate) fn new(out: &'a mut Vec<u8>) -> Self {
307        out.extend_from_slice(&MODIFIER_STATE_VERSION.to_le_bytes());
308        Self { out }
309    }
310
311    pub(crate) fn u8(&mut self, v: u8) {
312        self.out.push(v);
313    }
314
315    pub(crate) fn bool(&mut self, v: bool) {
316        self.out.push(u8::from(v));
317    }
318
319    pub(crate) fn u64(&mut self, v: u64) {
320        self.out.extend_from_slice(&v.to_le_bytes());
321    }
322
323    pub(crate) fn usize(&mut self, v: usize) {
324        self.u64(v as u64);
325    }
326
327    pub(crate) fn f32(&mut self, v: f32) {
328        self.out.extend_from_slice(&v.to_bits().to_le_bytes());
329    }
330
331    pub(crate) fn vec3(&mut self, v: (f32, f32, f32)) {
332        self.f32(v.0);
333        self.f32(v.1);
334        self.f32(v.2);
335    }
336
337    /// `nx`, `ny`, `nz` as u64, the bounds, then every cell.
338    pub(crate) fn field(&mut self, f: &ScalarField3D) {
339        self.usize(f.nx);
340        self.usize(f.ny);
341        self.usize(f.nz);
342        self.vec3(f.min);
343        self.vec3(f.max);
344        for &v in &f.data {
345            self.f32(v);
346        }
347    }
348}
349
350/// Reads a payload written by [`StateWriter`], refusing anything it cannot
351/// apply. Nothing is allocated before the bytes it describes are known to be
352/// present.
353pub(crate) struct StateReader<'a> {
354    bytes: &'a [u8],
355    at: usize,
356}
357
358impl<'a> StateReader<'a> {
359    /// Reads the version; any other than [`MODIFIER_STATE_VERSION`] is
360    /// [`StateError::InvalidValue`].
361    pub(crate) fn new(bytes: &'a [u8]) -> Result<Self, StateError> {
362        let mut r = Self { bytes, at: 0 };
363        let mut v = [0_u8; 4];
364        v.copy_from_slice(r.take(4)?);
365        if u32::from_le_bytes(v) != MODIFIER_STATE_VERSION {
366            return Err(StateError::InvalidValue);
367        }
368        Ok(r)
369    }
370
371    fn need(&self, n: usize) -> Result<(), StateError> {
372        let end = self.at.checked_add(n).ok_or(StateError::InvalidValue)?;
373        if end > self.bytes.len() {
374            return Err(StateError::Length {
375                expected: end,
376                found: self.bytes.len(),
377            });
378        }
379        Ok(())
380    }
381
382    fn take(&mut self, n: usize) -> Result<&'a [u8], StateError> {
383        self.need(n)?;
384        let s = &self.bytes[self.at..self.at + n];
385        self.at += n;
386        Ok(s)
387    }
388
389    pub(crate) fn u8(&mut self) -> Result<u8, StateError> {
390        Ok(self.take(1)?[0])
391    }
392
393    pub(crate) fn bool(&mut self) -> Result<bool, StateError> {
394        match self.u8()? {
395            0 => Ok(false),
396            1 => Ok(true),
397            _ => Err(StateError::InvalidValue),
398        }
399    }
400
401    pub(crate) fn u64(&mut self) -> Result<u64, StateError> {
402        let mut v = [0_u8; 8];
403        v.copy_from_slice(self.take(8)?);
404        Ok(u64::from_le_bytes(v))
405    }
406
407    pub(crate) fn usize(&mut self) -> Result<usize, StateError> {
408        usize::try_from(self.u64()?).map_err(|_| StateError::InvalidValue)
409    }
410
411    pub(crate) fn f32(&mut self) -> Result<f32, StateError> {
412        let mut v = [0_u8; 4];
413        v.copy_from_slice(self.take(4)?);
414        Ok(f32::from_bits(u32::from_le_bytes(v)))
415    }
416
417    pub(crate) fn vec3(&mut self) -> Result<(f32, f32, f32), StateError> {
418        Ok((self.f32()?, self.f32()?, self.f32()?))
419    }
420
421    /// A count of items, each at least `min_item_bytes` long, checked against
422    /// the bytes left before the caller allocates for it.
423    pub(crate) fn count(&mut self, min_item_bytes: usize) -> Result<usize, StateError> {
424        let n = self.usize()?;
425        self.need(
426            n.checked_mul(min_item_bytes)
427                .ok_or(StateError::InvalidValue)?,
428        )?;
429        Ok(n)
430    }
431
432    /// A field written by [`StateWriter::field`], rebuilt with
433    /// [`ScalarField3D::new`] (cell sizes follow from the stored bounds).
434    pub(crate) fn field(&mut self) -> Result<ScalarField3D, StateError> {
435        let nx = self.usize()?;
436        let ny = self.usize()?;
437        let nz = self.usize()?;
438        let min = self.vec3()?;
439        let max = self.vec3()?;
440        let cells = nx
441            .checked_mul(ny)
442            .and_then(|c| c.checked_mul(nz))
443            .ok_or(StateError::InvalidValue)?;
444        self.need(cells.checked_mul(4).ok_or(StateError::InvalidValue)?)?;
445        let mut f = ScalarField3D::new(nx, ny, nz, min, max);
446        for v in &mut f.data {
447            *v = self.f32()?;
448        }
449        Ok(f)
450    }
451
452    /// Refuses trailing bytes.
453    pub(crate) fn finish(self) -> Result<(), StateError> {
454        if self.at == self.bytes.len() {
455            Ok(())
456        } else {
457            Err(StateError::Length {
458                expected: self.at,
459                found: self.bytes.len(),
460            })
461        }
462    }
463}
464
465/// Pushes the largest cell of `f` on `channel`; nothing when `f` has no
466/// cells, NaN cells are skipped.
467pub(crate) fn observe_max(out: &mut ObservationSink, channel: u32, f: &ScalarField3D) {
468    let max = f
469        .data
470        .iter()
471        .copied()
472        .filter(|v| !v.is_nan())
473        .reduce(f32::max);
474    if let Some(m) = max {
475        out.push(channel, Fix128::from_f32(m));
476    }
477}
478
479/// Pushes the sum of the cells of `f` on `channel`, each cell converted to
480/// [`Fix128`] first (exact for finite cells, so the sum does not depend on
481/// float rounding).
482pub(crate) fn observe_sum(out: &mut ObservationSink, channel: u32, f: &ScalarField3D) {
483    let sum = f
484        .data
485        .iter()
486        .fold(Fix128::ZERO, |acc, &v| acc + Fix128::from_f32(v));
487    out.push(channel, sum);
488}
489
490// ============================================================================
491// Tests
492// ============================================================================
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use crate::sdf_collider::ClosureSdf;
498
499    /// Simple modifier that uniformly expands the SDF
500    struct ExpandModifier {
501        amount: f32,
502    }
503
504    impl PhysicsModifier for ExpandModifier {
505        fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
506            d - self.amount
507        }
508        fn update(&mut self, _dt: f32) {}
509        fn name(&self) -> &'static str {
510            "expand"
511        }
512    }
513
514    #[test]
515    fn test_modified_sdf_no_modifiers() {
516        let sphere = ClosureSdf::new(
517            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
518            |x, y, z| {
519                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
520                if len < 1e-10 {
521                    (0.0, 1.0, 0.0)
522                } else {
523                    (x / len, y / len, z / len)
524                }
525            },
526        );
527
528        let modified = ModifiedSdf::new(Box::new(sphere));
529        let d = modified.distance(2.0, 0.0, 0.0);
530        assert!(
531            (d - 1.0).abs() < 0.01,
532            "No modifiers should pass through, got {d}"
533        );
534    }
535
536    #[test]
537    fn test_modified_sdf_expand() {
538        let sphere = ClosureSdf::new(
539            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
540            |x, y, z| {
541                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
542                if len < 1e-10 {
543                    (0.0, 1.0, 0.0)
544                } else {
545                    (x / len, y / len, z / len)
546                }
547            },
548        );
549
550        let modified = ModifiedSdf::new(Box::new(sphere))
551            .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
552
553        // Original: distance at (2,0,0) = 1.0
554        // After expand by 0.5: distance = 0.5
555        let d = modified.distance(2.0, 0.0, 0.0);
556        assert!(
557            (d - 0.5).abs() < 0.01,
558            "Expand should reduce distance, got {d}"
559        );
560    }
561
562    #[test]
563    fn test_modifier_chain() {
564        let sphere = ClosureSdf::new(
565            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
566            |x, y, z| {
567                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
568                if len < 1e-10 {
569                    (0.0, 1.0, 0.0)
570                } else {
571                    (x / len, y / len, z / len)
572                }
573            },
574        );
575
576        let modified = ModifiedSdf::new(Box::new(sphere))
577            .with_modifier(Box::new(ExpandModifier { amount: 0.3 }))
578            .with_modifier(Box::new(ExpandModifier { amount: 0.2 }));
579
580        // Total expand = 0.5
581        let d = modified.distance(2.0, 0.0, 0.0);
582        assert!(
583            (d - 0.5).abs() < 0.01,
584            "Chain should sum expansions, got {d}"
585        );
586    }
587
588    #[test]
589    fn test_single_modified_sdf() {
590        let sphere = ClosureSdf::new(
591            |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
592            |x, y, z| {
593                let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
594                if len < 1e-10 {
595                    (0.0, 1.0, 0.0)
596                } else {
597                    (x / len, y / len, z / len)
598                }
599            },
600        );
601
602        let modified = SingleModifiedSdf::new(Box::new(sphere), ExpandModifier { amount: 0.5 });
603
604        let d = modified.distance(2.0, 0.0, 0.0);
605        assert!((d - 0.5).abs() < 0.01, "Single modifier expand, got {d}");
606    }
607
608    /// 地面 (distance = y): 数値誤差なしで offset を観測できる
609    fn ground() -> ClosureSdf {
610        ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0))
611    }
612
613    /// update(dt) で amount が dt ずつ増える (状態を持つ) modifier
614    struct GrowModifier {
615        amount: f32,
616    }
617
618    impl PhysicsModifier for GrowModifier {
619        fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
620            d - self.amount
621        }
622        fn update(&mut self, dt: f32) {
623            self.amount += dt;
624        }
625        fn name(&self) -> &'static str {
626            "grow"
627        }
628    }
629
630    #[test]
631    fn clear_modifiers_restores_original_field_and_allows_re_adding() {
632        let mut modified = ModifiedSdf::new(Box::new(ground()))
633            .with_modifier(Box::new(ExpandModifier { amount: 0.25 }))
634            .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
635        assert_eq!(modified.modifier_count(), 2);
636        // y = 2 → 2 - 0.25 - 0.5 = 1.25
637        assert!((modified.distance(0.0, 2.0, 0.0) - 1.25).abs() < 1e-6);
638
639        modified.clear_modifiers();
640        assert_eq!(modified.modifier_count(), 0);
641        assert!(modified.modifier_mut(0).is_none());
642        // 元の field そのまま
643        assert!((modified.distance(0.0, 2.0, 0.0) - 2.0).abs() < 1e-6);
644        let (nx, ny, nz) = modified.normal(0.0, 2.0, 0.0);
645        assert!(nx.abs() < 1e-6 && (ny - 1.0).abs() < 1e-6 && nz.abs() < 1e-6);
646        // 空でもう一度 clear しても問題なし、再追加で再び効く
647        modified.clear_modifiers();
648        modified.add_modifier(Box::new(ExpandModifier { amount: 1.0 }));
649        assert_eq!(modified.modifier_count(), 1);
650        assert!((modified.distance(0.0, 2.0, 0.0) - 1.0).abs() < 1e-6);
651    }
652
653    #[test]
654    fn modifier_mut_gives_in_place_access_by_index() {
655        let mut modified = ModifiedSdf::new(Box::new(ground()))
656            .with_modifier(Box::new(GrowModifier { amount: 0.25 }))
657            .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
658        // y = 2 → 2 - 0.25 - 0.5 = 1.25
659        assert!((modified.distance(0.0, 2.0, 0.0) - 1.25).abs() < 1e-6);
660
661        // index 順に対応する modifier が返る
662        assert_eq!(modified.modifier_mut(0).map(|m| m.name()), Some("grow"));
663        assert_eq!(modified.modifier_mut(1).map(|m| m.name()), Some("expand"));
664        assert!(modified.modifier_mut(2).is_none());
665
666        // 返された &mut を通した update は field に反映される: grow 0.25 → 0.75
667        match modified.modifier_mut(0) {
668            Some(m) => m.update(0.5),
669            None => panic!("modifier 0 must exist"),
670        }
671        assert!((modified.distance(0.0, 2.0, 0.0) - 0.75).abs() < 1e-6);
672        // expand (index 1) は状態を持たないので update しても不変
673        match modified.modifier_mut(1) {
674            Some(m) => m.update(0.5),
675            None => panic!("modifier 1 must exist"),
676        }
677        assert!((modified.distance(0.0, 2.0, 0.0) - 0.75).abs() < 1e-6);
678        // ModifiedSdf::update(dt) は全 modifier を進める = modifier_mut(0).update と同じ効果
679        modified.update(0.25);
680        assert!((modified.distance(0.0, 2.0, 0.0) - 0.5).abs() < 1e-6);
681        assert_eq!(modified.modifier_count(), 2);
682    }
683
684    /// A modifier that would move the surface by `amount` but reports itself
685    /// inactive, so both wrappers must skip it.
686    struct InactiveModifier {
687        amount: f32,
688        updates: u32,
689    }
690
691    impl PhysicsModifier for InactiveModifier {
692        fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
693            d - self.amount
694        }
695        fn update(&mut self, _dt: f32) {
696            self.updates += 1;
697        }
698        fn name(&self) -> &'static str {
699            "inactive"
700        }
701        fn is_active(&self) -> bool {
702            false
703        }
704    }
705
706    /// oracle: on the ground `distance = y`, an expansion by `a` gives
707    /// `distance = y − a` and normal `(0, 1, 0)`; an inactive modifier leaves
708    /// `y`; `SingleModifiedSdf::update` advances the modifier (`GrowModifier`
709    /// grows by `dt`) whether or not it is active.
710    #[test]
711    fn distance_and_normal_of_both_wrappers_on_the_ground() {
712        let multi = ModifiedSdf::new(Box::new(ground()))
713            .with_modifier(Box::new(ExpandModifier { amount: 0.5 }))
714            .with_modifier(Box::new(InactiveModifier {
715                amount: 9.0,
716                updates: 0,
717            }));
718        let (d, n) = multi.distance_and_normal(0.0, 2.0, 0.0);
719        assert!((d - 1.5).abs() < 1e-6, "{d}");
720        assert!(
721            n.0.abs() < 1e-6 && (n.1 - 1.0).abs() < 1e-6 && n.2.abs() < 1e-6,
722            "{n:?}"
723        );
724
725        let mut grow = SingleModifiedSdf::new(Box::new(ground()), GrowModifier { amount: 0.25 });
726        grow.update(0.5);
727        let (d, n) = grow.distance_and_normal(0.0, 2.0, 0.0);
728        assert!((d - 1.25).abs() < 1e-6, "{d}");
729        assert!(
730            n.0.abs() < 1e-6 && (n.1 - 1.0).abs() < 1e-6 && n.2.abs() < 1e-6,
731            "{n:?}"
732        );
733        let n = grow.normal(1.0, 3.0, -1.0);
734        assert!((n.1 - 1.0).abs() < 1e-6, "{n:?}");
735
736        let mut off = SingleModifiedSdf::new(
737            Box::new(ground()),
738            InactiveModifier {
739                amount: 9.0,
740                updates: 0,
741            },
742        );
743        off.update(0.5);
744        off.update(0.5);
745        assert_eq!(off.modifier.updates, 2);
746        assert!((off.distance(0.0, 2.0, 0.0) - 2.0).abs() < 1e-6);
747    }
748
749    /// oracle: the payload is the version `1` as 4 little-endian bytes, then
750    /// each item little endian: a `u8` as itself, a `bool` as 0/1, a `u64` /
751    /// `usize` as 8 bytes, an `f32` as its 4 bit bytes, a vector as three
752    /// `f32`s, a field as `nx, ny, nz` (u64), `min`, `max`, then the cells.
753    #[test]
754    fn state_writer_byte_layout() {
755        let mut out = Vec::new();
756        let mut w = StateWriter::new(&mut out);
757        w.u8(7);
758        w.bool(true);
759        w.bool(false);
760        w.u64(0x0102_0304_0506_0708);
761        w.usize(5);
762        w.f32(1.5);
763        w.vec3((1.0, -2.0, 0.5));
764        let mut f = ScalarField3D::new(2, 1, 1, (0.0, 0.0, 0.0), (2.0, 1.0, 1.0));
765        f.data = vec![0.25, -4.0];
766        w.field(&f);
767
768        let mut want: Vec<u8> = vec![
769            1, 0, 0, 0, 7, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1, 5, 0, 0, 0, 0, 0, 0, 0,
770        ];
771        for v in [1.5_f32, 1.0, -2.0, 0.5] {
772            want.extend_from_slice(&v.to_bits().to_le_bytes());
773        }
774        for n in [2_u64, 1, 1] {
775            want.extend_from_slice(&n.to_le_bytes());
776        }
777        for v in [0.0_f32, 0.0, 0.0, 2.0, 1.0, 1.0, 0.25, -4.0] {
778            want.extend_from_slice(&v.to_bits().to_le_bytes());
779        }
780        assert_eq!(out, want);
781
782        // read back what was written
783        let mut r = StateReader::new(&out).expect("version 1");
784        assert_eq!(r.u8(), Ok(7));
785        assert_eq!(r.bool(), Ok(true));
786        assert_eq!(r.bool(), Ok(false));
787        assert_eq!(r.u64(), Ok(0x0102_0304_0506_0708));
788        assert_eq!(r.usize(), Ok(5));
789        assert_eq!(r.f32(), Ok(1.5));
790        assert_eq!(r.vec3(), Ok((1.0, -2.0, 0.5)));
791        let g = r.field().expect("field");
792        assert_eq!(
793            (g.nx, g.ny, g.nz, g.min, g.max),
794            (2, 1, 1, (0.0, 0.0, 0.0), (2.0, 1.0, 1.0))
795        );
796        assert_eq!(g.data, vec![0.25, -4.0]);
797        assert_eq!(r.finish(), Ok(()));
798    }
799
800    /// oracle: the reader refuses a version other than 1, a `bool` byte other
801    /// than 0/1, bytes that end early (`Length { expected: end, found: len }`),
802    /// a count whose items cannot fit, a field whose cell count overflows, and
803    /// trailing bytes (`Length { expected: read, found: len }`).
804    #[test]
805    fn state_reader_refusals() {
806        assert_eq!(
807            StateReader::new(&[2, 0, 0, 0]).err(),
808            Some(StateError::InvalidValue)
809        );
810        assert_eq!(
811            StateReader::new(&[1, 0]).err(),
812            Some(StateError::Length {
813                expected: 4,
814                found: 2
815            })
816        );
817        let mut r = StateReader::new(&[1, 0, 0, 0, 2]).expect("version");
818        assert_eq!(r.bool(), Err(StateError::InvalidValue));
819        let mut r = StateReader::new(&[1, 0, 0, 0, 9, 9]).expect("version");
820        assert_eq!(
821            r.f32(),
822            Err(StateError::Length {
823                expected: 8,
824                found: 6
825            })
826        );
827
828        // a count of 3 items of 4 bytes with only 4 bytes left
829        let mut bytes = vec![1, 0, 0, 0];
830        bytes.extend_from_slice(&3_u64.to_le_bytes());
831        bytes.extend_from_slice(&[0; 4]);
832        let mut r = StateReader::new(&bytes).expect("version");
833        assert_eq!(
834            r.count(4),
835            Err(StateError::Length {
836                expected: 24,
837                found: 16
838            })
839        );
840        // a count whose byte size overflows usize
841        let mut bytes = vec![1, 0, 0, 0];
842        bytes.extend_from_slice(&u64::MAX.to_le_bytes());
843        let mut r = StateReader::new(&bytes).expect("version");
844        assert_eq!(r.count(2), Err(StateError::InvalidValue));
845        let mut r = StateReader::new(&bytes[..]).expect("version");
846        assert_eq!(r.count(1), Err(StateError::InvalidValue));
847
848        // a field of u64::MAX × 2 × 1 cells overflows the cell count
849        let mut bytes = vec![1, 0, 0, 0];
850        for n in [u64::MAX, 2, 1] {
851            bytes.extend_from_slice(&n.to_le_bytes());
852        }
853        bytes.extend_from_slice(&[0; 24]);
854        let mut r = StateReader::new(&bytes).expect("version");
855        assert_eq!(r.field().err(), Some(StateError::InvalidValue));
856
857        // trailing byte after one u8
858        let mut r = StateReader::new(&[1, 0, 0, 0, 5, 6]).expect("version");
859        assert_eq!(r.u8(), Ok(5));
860        assert_eq!(
861            r.finish(),
862            Err(StateError::Length {
863                expected: 5,
864                found: 6
865            })
866        );
867    }
868
869    /// oracle: `observe_max` pushes the largest non-NaN cell (3 of
870    /// `[1, NaN, 3, −2]`) and nothing for a field without cells;
871    /// `observe_sum` pushes `1 + 3 − 2 + 0.5 = 2.5` exactly.
872    #[test]
873    fn observe_max_and_sum_closed_form() {
874        let mut f = ScalarField3D::new(4, 1, 1, (0.0, 0.0, 0.0), (4.0, 1.0, 1.0));
875        f.data = vec![1.0, f32::NAN, 3.0, -2.0];
876        let mut out = ObservationSink::new();
877        observe_max(&mut out, 4, &f);
878        assert_eq!(out.values(), &[(4, Fix128::from_int(3))]);
879
880        let mut g = ScalarField3D::new(4, 1, 1, (0.0, 0.0, 0.0), (4.0, 1.0, 1.0));
881        g.data = vec![1.0, 0.5, 3.0, -2.0];
882        let mut out = ObservationSink::new();
883        observe_sum(&mut out, 1, &g);
884        assert_eq!(out.values(), &[(1, Fix128::from_ratio(5, 2))]);
885
886        let mut empty = ScalarField3D::new(1, 1, 1, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
887        empty.data.clear();
888        let mut out = ObservationSink::new();
889        observe_max(&mut out, 0, &empty);
890        assert!(out.values().is_empty());
891    }
892}