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 exposes ONE
4//! interaction: [`sample_at`](SceneEvaluator::sample_at), a stateful function
5//! of time. Each cell evaluates itself at the target (direction management —
6//! forward, backward reset+replay, equal project — is internal to each stateful
7//! node), so the session needs no clock bookkeeping beyond remembering the last
8//! target.
9
10use crate::{
11    Extract, SealedRanimScene, TimeMark,
12    animation::node::{AnimNode, AnimationInfo},
13    audio::MASTER_SAMPLE_RATE,
14    core_item::CoreItem,
15};
16
17/// A reusable frame-local output buffer of `((animation_id, part), CoreItem)`.
18pub type EvaluatedFrame = Vec<((usize, usize), CoreItem)>;
19
20/// Lightweight scene evaluation session.
21pub struct SceneEvaluator {
22    cells: Vec<AnimNode>,
23    total_secs: f64,
24    audio: std::sync::Arc<[f32]>,
25    time_marks: Vec<(f64, TimeMark)>,
26    clock: f64,
27}
28
29impl SceneEvaluator {
30    /// Consume a sealed scene and create a driving session.
31    ///
32    /// `logic_fps` is retained only for call-site compatibility; it no longer
33    /// drives stepping (each iterative segment owns its `sim_step`).
34    #[allow(unused_variables)]
35    pub fn new(scene: SealedRanimScene, logic_fps: f64) -> Self {
36        Self {
37            cells: scene.animations,
38            total_secs: scene.total_secs,
39            audio: scene.audio,
40            time_marks: scene.time_marks,
41            clock: 0.0,
42        }
43    }
44
45    /// Total scene duration.
46    pub fn total_secs(&self) -> f64 {
47        self.total_secs
48    }
49
50    /// The top-level animation cells.
51    ///
52    /// Crate-internal: the audio tests walk these with a point-semantics
53    /// reference mixer that the seal-time bake must reproduce exactly.
54    #[cfg(test)]
55    pub(crate) fn cells(&self) -> &[AnimNode] {
56        &self.cells
57    }
58
59    /// Whether any sound leaves live in the tree.
60    pub fn has_audio(&self) -> bool {
61        self.cells.iter().any(AnimNode::has_audio)
62    }
63
64    /// The baked audio plane: interleaved stereo at the master sample rate
65    /// over `[0, total_secs]` (shared, cheap to clone). Empty when the scene
66    /// has no sound leaves.
67    pub fn audio(&self) -> &std::sync::Arc<[f32]> {
68        &self.audio
69    }
70
71    /// The scene's audio over `[0, out_secs]` as a fresh interleaved stereo
72    /// buffer.
73    ///
74    /// The audio plane was already mixed once at seal
75    /// ([`RanimScene::seal`](crate::RanimScene::seal)); this is a prefix copy
76    /// of that baked buffer, zero-padded past the scene end.
77    pub fn mix_audio(&self, out_secs: f64, sample_rate: u32) -> Vec<f32> {
78        assert_eq!(
79            sample_rate, MASTER_SAMPLE_RATE,
80            "the baked audio lives at the master sample rate"
81        );
82        let out_frames = (out_secs * sample_rate as f64).ceil() as usize;
83        let baked_frames = self.audio.len() / 2;
84        let copy = out_frames.min(baked_frames);
85        let mut out = Vec::with_capacity(out_frames * 2);
86        out.extend_from_slice(&self.audio[..copy * 2]);
87        out.resize(out_frames * 2, 0.0);
88        out
89    }
90
91    /// Scene time marks.
92    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
93        &self.time_marks
94    }
95
96    /// Hierarchical runtime animation information for preview tooling.
97    pub fn animation_infos(&self) -> Vec<AnimationInfo> {
98        self.cells.iter().map(AnimNode::animation_info).collect()
99    }
100
101    /// Last sampled target (the `clock` reading for preview tooling).
102    pub fn clock(&self) -> f64 {
103        self.clock
104    }
105
106    /// Sample the scene at `render_secs` — the ONLY session interaction.
107    ///
108    /// Every top-level cell evaluates itself at the target (forward/backward
109    /// direction management is internal), and the extracted items carry the
110    /// `(animation_id, part)` identities of `SealedRanimScene::eval_at_sec`.
111    pub fn sample_at(&mut self, render_secs: f64, out: &mut EvaluatedFrame) {
112        for (animation_id, cell) in self.cells.iter().enumerate() {
113            let mut items = Vec::new();
114            cell.eval_at(render_secs, &mut items);
115            for (part, item) in items
116                .into_iter()
117                .flat_map(|item| item.extract())
118                .enumerate()
119            {
120                out.push(((animation_id, part), item));
121            }
122        }
123        self.clock = render_secs;
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::{
131        RanimScene, SealedRanimScene,
132        animation::{
133            build::{PlaybackExt, Unplaced},
134            eval::Eval,
135        },
136        core_item::vitem::VItem,
137        seq,
138    };
139
140    /// A constant-velocity iterative segment: x accumulates with progress.
141    /// `sim_step` is the content's own step (1/N); `logical_secs` scales it
142    /// back to physical seconds. State lives behind a `RefCell` because
143    /// `eval_alpha` is a `&self` query.
144    struct ConstantVelocity {
145        v: f64,
146        logical_secs: f64,
147        sim_step: f64,
148        state: std::cell::RefCell<(f64, f64)>, // (x, alpha)
149    }
150
151    impl Eval for ConstantVelocity {
152        type Output = VItem;
153
154        fn eval_alpha(&self, target: f64) -> VItem {
155            let mut s = self.state.borrow_mut();
156            if target < s.1 {
157                s.0 = 0.0;
158                s.1 = 0.0;
159            }
160            let start_idx = (s.1 / self.sim_step).floor() as usize;
161            let end_idx = (target / self.sim_step).floor() as usize;
162            for _ in start_idx..end_idx {
163                s.0 += self.v * self.sim_step * self.logical_secs;
164            }
165            s.1 = target;
166            let mut item = VItem::default();
167            item.points[0].x = s.0 as f32;
168            item
169        }
170    }
171
172    /// A stateless segment: x = alpha.
173    struct ProgressX;
174
175    impl Eval for ProgressX {
176        type Output = VItem;
177
178        fn eval_alpha(&self, alpha: f64) -> VItem {
179            let mut item = VItem::default();
180            item.points[0].x = alpha as f32;
181            item
182        }
183    }
184
185    fn xs_of(frame: &EvaluatedFrame) -> Vec<f32> {
186        frame
187            .iter()
188            .filter_map(|(_, item)| match item {
189                CoreItem::VItem(v) => Some(v.points[0].x),
190                _ => None,
191            })
192            .collect()
193    }
194
195    fn cv(v: f64, logical_secs: f64) -> ConstantVelocity {
196        ConstantVelocity {
197            v,
198            logical_secs,
199            sim_step: 1.0 / 120.0,
200            state: std::cell::RefCell::new((0.0, 0.0)),
201        }
202    }
203
204    #[test]
205    fn functional_scene_matches_pure_eval() {
206        let mut scene = RanimScene::new();
207        scene.play(ProgressX.with_duration(2.0));
208        let sealed = scene.seal();
209
210        let mut ev = SceneEvaluator::new(sealed, 120.0);
211        for sec in [0.0, 0.25, 0.5, 1.0, 1.5, 2.0] {
212            let mut frame = EvaluatedFrame::new();
213            ev.sample_at(sec, &mut frame);
214            let expected = (sec / 2.0) as f32;
215            let got = xs_of(&frame);
216            assert_eq!(got, vec![expected], "at sec={sec}");
217        }
218    }
219
220    #[test]
221    fn iterative_leaves_step_along_the_logic_grid() {
222        let mut scene = RanimScene::new();
223        scene.play(cv(1.0, 2.0).with_duration(2.0).at(0.0));
224        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
225        for sec in [0.0, 0.5, 1.0, 1.5, 2.0] {
226            let mut frame = EvaluatedFrame::new();
227            ev.sample_at(sec, &mut frame);
228            assert_eq!(xs_of(&frame), vec![sec as f32], "at sec={sec}");
229        }
230
231        // Nested inside a sequence the leaf still steps on its own timeline.
232        let mut scene = RanimScene::new();
233        scene.play(
234            seq![
235                cv(1.0, 1.0).with_duration(1.0),
236                cv(1.0, 1.0).with_duration(1.0)
237            ]
238            .at(0.0),
239        );
240        let mut ev = SceneEvaluator::new(scene.seal(), 120.0);
241        for (sec, expected) in [(1.5, 0.5), (2.0, 1.0)] {
242            let mut frame = EvaluatedFrame::new();
243            ev.sample_at(sec, &mut frame);
244            assert_eq!(xs_of(&frame), vec![expected], "at sec={sec}");
245        }
246    }
247
248    #[test]
249    fn iterative_seek_matches_forward_and_resets_nested_leaves() {
250        fn run(scene: SealedRanimScene, backward: bool) -> Vec<Vec<f32>> {
251            let mut ev = SceneEvaluator::new(scene, 120.0);
252            let mut trace = Vec::new();
253            for sec in [0.3, 0.7, 1.1, 1.9, 2.6] {
254                if backward {
255                    // Jump backwards below the first sample so every leaf
256                    // has to re-simulate from the start.
257                    ev.sample_at(0.2, &mut EvaluatedFrame::new());
258                }
259                let mut frame = EvaluatedFrame::new();
260                ev.sample_at(sec, &mut frame);
261                trace.push(xs_of(&frame));
262            }
263            trace
264        }
265
266        let single = || {
267            let mut scene = RanimScene::new();
268            scene.play(cv(2.0, 3.0).with_duration(3.0).at(0.0));
269            scene.seal()
270        };
271        let forward = run(single(), false);
272        assert_eq!(forward, run(single(), true));
273        assert_eq!(forward[2], vec![(2.0 * 1.1) as f32]);
274
275        let nested = || {
276            let mut scene = RanimScene::new();
277            scene.play(
278                seq![
279                    cv(1.0, 1.0).with_duration(1.0),
280                    cv(1.0, 1.0).with_duration(1.0)
281                ]
282                .at(0.0),
283            );
284            scene.seal()
285        };
286        assert_eq!(run(nested(), false), run(nested(), true));
287    }
288}