Skip to main content

ranim_core/
scene_evaluator.rs

1//! A lightweight session driver for scene evaluation (no ECS).
2//!
3//! The [`SceneEvaluator`] owns the lowered animation cells and drives them
4//! along a fixed logic grid:
5//!
6//! - [`SceneEvaluator::advance_to`] advances the internal clock to the floor
7//!   logic tick of a render sample time, stepping every active cell (iterative
8//!   leaves integrate; functional leaves step as no-ops);
9//! - [`SceneEvaluator::sample_into`] is pure: it samples every active cell at
10//!   the internal clock and extracts `CoreItem`s, mirroring
11//!   [`SealedRanimScene::eval_at_sec`]'s `(animation_id, part)` identities;
12//! - [`SceneEvaluator::seek`] resets all cells and replays (deterministic
13//!   contract: replay equals forward advancement).
14//!
15//! This is the M1 runtime: self-contained iterative animations work without
16//! ECS. See `docs`/design notes for the surrounding design.
17
18use crate::{
19    Extract, SealedRanimScene, TimeMark,
20    animation::{AnimationCell, AnimationInfo},
21    core_item::CoreItem,
22};
23
24/// A reusable frame-local output buffer of `((animation_id, part), CoreItem)`.
25pub type EvaluatedFrame = Vec<((usize, usize), CoreItem)>;
26
27/// Lightweight scene evaluation session.
28pub struct SceneEvaluator {
29    cells: Vec<AnimationCell>,
30    total_secs: f64,
31    time_marks: Vec<(f64, TimeMark)>,
32    logic_fps: f64,
33    clock: f64,
34}
35
36impl SceneEvaluator {
37    /// Consume a sealed scene and create a driving session.
38    ///
39    /// `logic_fps` is the fixed logic grid resolution (`1 / logic_fps` is the
40    /// integration step). The time model defaults to 120 Hz; render fps only
41    /// decides which logic states are sampled.
42    pub fn new(scene: SealedRanimScene, logic_fps: f64) -> Self {
43        assert!(
44            logic_fps.is_finite() && logic_fps > 0.0,
45            "logic_fps must be finite and positive"
46        );
47        Self {
48            cells: scene.animations,
49            total_secs: scene.total_secs,
50            time_marks: scene.time_marks,
51            logic_fps,
52            clock: 0.0,
53        }
54    }
55
56    /// Total scene duration.
57    pub fn total_secs(&self) -> f64 {
58        self.total_secs
59    }
60
61    /// Scene time marks.
62    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
63        &self.time_marks
64    }
65
66    /// Hierarchical runtime animation information for preview tooling.
67    pub fn animation_infos(&self) -> Vec<AnimationInfo> {
68        self.cells
69            .iter()
70            .map(AnimationCell::animation_info)
71            .collect()
72    }
73
74    /// Current internal clock (the last advanced floor logic tick).
75    pub fn clock(&self) -> f64 {
76        self.clock
77    }
78
79    /// Advance the logic grid to the floor tick of `render_secs`.
80    ///
81    /// This is the only entry point that performs tick advancement. Calling it
82    /// with a time earlier than the current clock is a no-op; use
83    /// [`seek`](Self::seek) to move backwards.
84    pub fn advance_to(&mut self, render_secs: f64) {
85        let target = (render_secs * self.logic_fps).floor() / self.logic_fps;
86        while self.clock + 1e-9 < target {
87            let prev_tick_secs = self.clock;
88            let tick_secs = prev_tick_secs + 1.0 / self.logic_fps;
89            self.clock = tick_secs;
90            for cell in &mut self.cells {
91                cell.step_at_sec(tick_secs, prev_tick_secs);
92            }
93        }
94        self.clock = target;
95    }
96
97    /// Reset all cells and replay to `render_secs` (deterministic contract).
98    pub fn seek(&mut self, render_secs: f64) {
99        self.clock = 0.0;
100        for cell in &mut self.cells {
101            cell.reset_entered();
102        }
103        self.advance_to(render_secs);
104    }
105
106    /// Pure sampling of the current clock: no tick advancement.
107    ///
108    /// Extracts every active cell at the floor logic tick into `out` with
109    /// `(animation_id, part)` identities matching `SealedRanimScene::eval_at_sec`.
110    /// Call [`advance_to`](Self::advance_to) or [`seek`](Self::seek) first.
111    pub fn sample_into(&self, out: &mut EvaluatedFrame) {
112        for (animation_id, cell) in self.cells.iter().enumerate() {
113            if !cell.active_at(self.clock) {
114                continue;
115            }
116            let mut items = Vec::new();
117            cell.sample_at_sec(self.clock, &mut items);
118            for (part, item) in items
119                .into_iter()
120                .flat_map(|item| item.extract())
121                .enumerate()
122            {
123                out.push(((animation_id, part), item));
124            }
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::{
133        RanimScene,
134        animation::{AnimationExt, Eval, Placeable, SegmentTime},
135        core_item::vitem::VItem,
136        seq,
137    };
138
139    /// A constant-velocity iterative segment: x accumulates with local time.
140    struct ConstantVelocity {
141        v: f64,
142        x: f64,
143    }
144
145    impl Eval for ConstantVelocity {
146        type Output = VItem;
147
148        fn sample(&self, _time: &SegmentTime) -> VItem {
149            let mut item = VItem::default();
150            item.points[0].x = self.x as f32;
151            item
152        }
153
154        fn reset(&mut self) {
155            self.x = 0.0;
156        }
157
158        fn step(&mut self, time: &SegmentTime) {
159            self.x += self.v * time.local_delta_secs;
160        }
161    }
162
163    fn xs_of(frame: &EvaluatedFrame) -> Vec<f32> {
164        frame
165            .iter()
166            .filter_map(|(_, item)| match item {
167                CoreItem::VItem(v) => Some(v.points[0].x),
168                _ => None,
169            })
170            .collect()
171    }
172
173    #[test]
174    fn functional_scene_matches_pure_eval() {
175        let mut scene = RanimScene::new();
176        scene.play(
177            (move |alpha| {
178                let mut item = VItem::default();
179                item.points[0].x = alpha as f32;
180                item
181            })
182            .with_duration(2.0),
183        );
184        let sealed = scene.seal();
185
186        let mut ev = SceneEvaluator::new(sealed, 120.0);
187        for sec in [0.0, 0.25, 0.5, 1.0, 1.5, 2.0] {
188            let mut frame = EvaluatedFrame::new();
189            ev.advance_to(sec);
190            ev.sample_into(&mut frame);
191            // Pure path value at the same time (rate = linear, alpha = sec/2)
192            let expected = (sec / 2.0) as f32;
193            let got = xs_of(&frame);
194            assert_eq!(got, vec![expected], "at sec={sec}");
195        }
196    }
197
198    #[test]
199    fn iterative_segment_steps_along_logic_grid() {
200        let mut scene = RanimScene::new();
201        scene.play(
202            ConstantVelocity { v: 1.0, x: 0.0 }
203                .with_duration(2.0)
204                .at(0.0),
205        );
206        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
207
208        for sec in [0.0, 0.5, 1.0, 1.5, 2.0] {
209            let mut frame = EvaluatedFrame::new();
210            ev.advance_to(sec);
211            ev.sample_into(&mut frame);
212            assert_eq!(xs_of(&frame), vec![sec as f32], "at sec={sec}");
213        }
214    }
215
216    #[test]
217    fn iterative_eval_is_deterministic_and_seek_matches_forward() {
218        fn build_scene() -> SealedRanimScene {
219            let mut scene = RanimScene::new();
220            scene.play(
221                ConstantVelocity { v: 2.0, x: 0.0 }
222                    .with_duration(3.0)
223                    .at(0.0),
224            );
225            scene.seal()
226        }
227
228        let run = |seek_first: bool| {
229            let mut ev = SceneEvaluator::new(build_scene(), 120.0);
230            let mut trace = Vec::new();
231            for sec in [0.3, 0.7, 1.1, 1.9, 2.6] {
232                if seek_first {
233                    ev.seek(sec);
234                } else {
235                    ev.advance_to(sec);
236                }
237                let mut frame = EvaluatedFrame::new();
238                ev.sample_into(&mut frame);
239                trace.push(xs_of(&frame));
240            }
241            trace
242        };
243
244        let forward = run(false);
245        let seek = run(true);
246        assert_eq!(forward, seek);
247        // Matches the analytic value: x = v·t
248        assert_eq!(forward[2], vec![(2.0 * 1.1) as f32]);
249    }
250
251    #[test]
252    fn iterative_leaf_inside_sequence_steps() {
253        let mut scene = RanimScene::new();
254        scene.play(
255            seq![
256                ConstantVelocity { v: 1.0, x: 0.0 }.with_duration(1.0),
257                ConstantVelocity { v: 1.0, x: 0.0 }.with_duration(1.0),
258            ]
259            .at(0.0),
260        );
261
262        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
263        // t=1.5: the first is done (under sequence semantics it no longer appears
264        // in the frame, matching the pure path); the second is at 0.5 (x=0.5)
265        let mut frame = EvaluatedFrame::new();
266        ev.advance_to(1.5);
267        ev.sample_into(&mut frame);
268        assert_eq!(xs_of(&frame), vec![0.5]);
269
270        // Final state: the second completes with x=1.0
271        let mut frame = EvaluatedFrame::new();
272        ev.advance_to(2.0);
273        ev.sample_into(&mut frame);
274        assert_eq!(xs_of(&frame), vec![1.0]);
275    }
276}