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}