Skip to main content

ranim_core/animation/eval/
iterative.rs

1//! Iterative (stateful, stepped) evaluation: the
2//! [`IterativeEval`](crate::animation::eval::iterative::IterativeEval)
3//! capability trait and its
4//! [`Iterative`](crate::animation::eval::iterative::Iterative) adapter into the
5//! general [`Eval`] protocol.
6//!
7//! **Content is sequence**: an iterative segment owns its simulation step
8//! (`sim_step`, declared via `with_steps(N)`), its integration state, and its
9//! current progress. Advancing folds direction management — forward integrates
10//! `sim_step`-by-`sim_step`, backward resets and replays — so the runtime
11//! only ever asks "evaluate at progress `alpha`". No seconds, no scene clock,
12//! no `logic_fps` reach a segment's content.
13//!
14//! If `with_steps` is not called, the segment uses the default step of
15//! `1 / 120` (`DEFAULT_SIM_STEP`): every unit of normalized progress is integrated in 120 uniform
16//! substeps. Use `with_steps(N)` when a simulation needs a finer or coarser
17//! content resolution.
18
19use std::{cell::RefCell, marker::PhantomData};
20
21use super::Eval;
22
23/// The capability of an iterative, stateful evaluation.
24///
25/// This is what iterative animation types implement: particles, springs,
26/// physics simulations, and anything without a closed form. The state is owned
27/// and advanced by the [`Iterative`] adapter, and `step` receives it as a
28/// mutable reference.
29///
30/// Only one method, no defaults. There is no `reset` to forget or get wrong —
31/// the adapter restores the stored initial state itself. Physics parameters,
32/// palettes, and logical durations belong on `self` or in local variables
33/// captured by a closure (not in global constants); everything mutable must
34/// live in the state value, so a reset restores it all.
35///
36/// `step` receives the current progress `alpha` and the segment's own
37/// uniform progress step `delta_alpha` (`= sim_step = 1 / N`). These are
38/// **dimensionless progress**, independent of rate shaping and placement; the
39/// segment's content is a pure function of progress. To recover physical
40/// seconds, scale by the segment's own logical duration:
41///
42/// ```rust,ignore
43/// fn step(&self, state: &mut NBodyState, _alpha: f64, delta_alpha: f64) {
44///     let dt = self.sim_secs * delta_alpha; // physical seconds
45///     state.integrate(dt);
46/// }
47/// ```
48///
49/// Segments that genuinely need scene-clock-shaped time must be authored at the
50/// top level, where the author knows the placement.
51pub trait IterativeEval {
52    /// State produced and advanced by this evaluator.
53    type Output;
54
55    /// Advance the state by one progress step.
56    ///
57    /// `alpha` is the current progress, `delta_alpha` the segment's uniform
58    /// step (both dimensionless progress). The step size is declared on the
59    /// [`Iterative`] adapter with
60    /// [`Iterative::with_steps`]: `delta_alpha = 1 / N` for `with_steps(N)`,
61    /// or the default `1 / 120` when it is not called.
62    fn step(&self, output: &mut Self::Output, alpha: f64, delta_alpha: f64);
63}
64
65/// An [`IterativeEval`] backed by a stepping function.
66///
67/// The wrapper binds the function's mutable input type as the evaluator's
68/// unique [`Output`](IterativeEval::Output). Prefer [`Iterative::from_fn`] to
69/// constructing this type directly.
70pub struct IterativeFn<S, F> {
71    eval: F,
72    output: PhantomData<fn() -> S>,
73}
74
75impl<S, F> IterativeFn<S, F>
76where
77    F: Fn(&mut S, f64, f64),
78{
79    /// Bind a stepping function to its state type.
80    pub fn new(eval: F) -> Self {
81        Self {
82            eval,
83            output: PhantomData,
84        }
85    }
86}
87
88impl<S, F> IterativeEval for IterativeFn<S, F>
89where
90    F: Fn(&mut S, f64, f64),
91{
92    type Output = S;
93
94    fn step(&self, output: &mut Self::Output, alpha: f64, delta_alpha: f64) {
95        (self.eval)(output, alpha, delta_alpha)
96    }
97}
98
99/// The default content step when the author does not declare one: 1/120
100/// progress per step (the historical 120 Hz logic-grid resolution, now
101/// expressed as a per-segment content property rather than a scene parameter).
102const DEFAULT_SIM_STEP: f64 = 1.0 / 120.0;
103
104/// The memoization snapshot backing an iterative segment.
105///
106/// Holds the progress already reached (`alpha`) and the state there. Because
107/// `eval_alpha` is a **pure query** on `&self` — an animation's content is
108/// immutable once defined — the adapter keeps this snapshot behind a
109/// `RefCell`: advancing writes into it, projecting reads from it, and neither
110/// escapes the `&self` query contract. This is memoization, not mutation of
111/// the animation's definition.
112struct Snapshot<S> {
113    alpha: f64,
114    state: S,
115}
116
117/// Adapter turning an [`IterativeEval`] into the general [`Eval`] protocol.
118///
119/// The adapter owns the segment's definition — the initial state, the `sim_step`,
120/// and the stepping closure — all immutable. The only mutation is the snapshot
121/// cache (`alpha` + `state`) behind a `RefCell`, so `eval_alpha(target)`
122/// integrates to `target` only when `target` is ahead of the snapshot, resets
123/// and replays when behind, and otherwise returns the cached state directly.
124/// Repeated queries at the same `alpha` are O(1).
125///
126/// ```rust,ignore
127/// let sim_secs = 10.0;
128/// let animation = Iterative::from_fn(state0, move |state, _alpha, delta_alpha| {
129///     state.integrate(sim_secs * delta_alpha);
130/// })
131/// .with_steps(240)
132/// .with_duration(sim_secs);
133/// ```
134pub struct Iterative<E: IterativeEval> {
135    eval: E,
136    initial: E::Output,
137    sim_step: f64,
138    snapshot: RefCell<Snapshot<E::Output>>,
139}
140
141impl<E> Iterative<E>
142where
143    E: IterativeEval,
144    E::Output: Clone,
145{
146    /// Create an iterative segment from an initial state and a named
147    /// [`IterativeEval`] implementation.
148    ///
149    /// The content step defaults to `1 / 120` of normalized progress. Call
150    /// [`with_steps`](Self::with_steps) to override it, e.g.
151    /// `.with_steps(240)` for `1 / 240` progress increments.
152    pub fn new(initial: E::Output, eval: E) -> Self {
153        Self {
154            eval,
155            snapshot: RefCell::new(Snapshot {
156                alpha: 0.0,
157                state: initial.clone(),
158            }),
159            initial,
160            sim_step: DEFAULT_SIM_STEP,
161        }
162    }
163
164    /// Declare the content's step count `N`.
165    ///
166    /// The segment then integrates in uniform `1 / N` progress increments, so
167    /// every [`IterativeEval::step`] call receives `delta_alpha = 1 / N`.
168    /// Without this call the default is `1 / 120` (`DEFAULT_SIM_STEP`).
169    ///
170    /// This is the segment's **content resolution**, not the render sampling
171    /// rate and not the scene clock. A physical time step can be recovered as
172    /// `duration_secs * delta_alpha`.
173    ///
174    /// ```rust,ignore
175    /// Iterative::from_fn(initial_state, step_function)
176    ///     .with_steps(240) // 1/240 progress per integration step
177    ///     .with_duration(10.0); // => 1/24 seconds per integration step
178    /// ```
179    pub fn with_steps(mut self, n: usize) -> Self {
180        assert!(n > 0, "iterative step count must be positive");
181        self.sim_step = 1.0 / n as f64;
182        self
183    }
184}
185
186impl<S, F> Iterative<IterativeFn<S, F>>
187where
188    S: Clone,
189    F: Fn(&mut S, f64, f64),
190{
191    /// Create an iterative segment from an initial state and a stepping
192    /// function.
193    ///
194    /// Uses the default `1 / 120` content step; call
195    /// [`with_steps`](Iterative::with_steps) to choose another resolution.
196    pub fn from_fn(initial: S, eval: F) -> Self {
197        Self::new(initial, IterativeFn::new(eval))
198    }
199}
200
201impl<E> Eval for Iterative<E>
202where
203    E: IterativeEval,
204    E::Output: Clone,
205{
206    type Output = E::Output;
207
208    fn sim_step(&self) -> Option<f64> {
209        Some(self.sim_step)
210    }
211
212    fn eval_alpha(&self, target: f64) -> Self::Output {
213        let mut snap = self.snapshot.borrow_mut();
214        if target < snap.alpha {
215            // Backward target: reset and replay from the start.
216            snap.state = self.initial.clone();
217            snap.alpha = 0.0;
218        }
219        // Integrate by whole steps (index-based: no floating-point drift).
220        let start_idx = (snap.alpha / self.sim_step).floor() as usize;
221        let end_idx = (target / self.sim_step).floor() as usize;
222        for i in start_idx..end_idx {
223            let alpha = i as f64 * self.sim_step;
224            self.eval.step(&mut snap.state, alpha, self.sim_step);
225        }
226        snap.alpha = target;
227        snap.state.clone()
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use crate::core_item::vitem::VItem;
235
236    struct MoveRight;
237
238    impl IterativeEval for MoveRight {
239        type Output = VItem;
240
241        fn step(&self, state: &mut Self::Output, _alpha: f64, delta_alpha: f64) {
242            state.points[0].x += delta_alpha as f32;
243        }
244    }
245
246    #[test]
247    fn iterative_evaluators_step_by_sim_step() {
248        let named = Iterative::new(VItem::default(), MoveRight).with_steps(4);
249        assert_eq!(named.eval_alpha(0.5).points[0].x, 0.5);
250
251        let from_fn = Iterative::from_fn(
252            VItem::default(),
253            |state: &mut VItem, _alpha: f64, delta_alpha: f64| {
254                state.points[0].x += delta_alpha as f32;
255            },
256        )
257        .with_steps(4);
258        assert_eq!(from_fn.eval_alpha(0.5).points[0].x, 0.5);
259    }
260}