Skip to main content

ranim_core/
lib.rs

1//! The core of ranim.
2
3#![warn(missing_docs)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![allow(rustdoc::private_intra_doc_links)]
6#![doc(
7    html_logo_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg",
8    html_favicon_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg"
9)]
10
11/// Anchors and semantic bounds.
12pub mod anchor;
13pub mod animation;
14/// Color utilities.
15pub mod color;
16/// Component data.
17pub mod components;
18/// Fundamental scene primitives.
19pub mod core_item;
20/// Scene evaluation driver (lightweight session, no ECS).
21pub mod scene_evaluator;
22/// Fundamental traits.
23pub mod traits;
24pub use scene_evaluator::SceneEvaluator;
25/// Utilities.
26pub mod utils;
27
28pub use glam;
29pub use num;
30use std::fmt::Debug;
31
32use animation::{AnimStack, Animation, AnimationCell};
33pub use animation::{AnimationInfo, AnimationInfoKind};
34use core_item::CoreItem;
35
36/// Commonly used ranim APIs.
37pub mod prelude {
38    pub use crate::color::prelude::*;
39    pub use crate::traits::*;
40
41    pub use crate::animation::{
42        AnimSequence, AnimStack, Animation, AnimationExt, Eval, Placeable, StaticAnim,
43    };
44    pub use crate::core_item::camera_frame::CameraFrame;
45    pub use crate::{RanimScene, TimeMark};
46}
47
48/// Extract one or more target values from a reference.
49pub trait Extract {
50    /// Extraction target.
51    type Target: Clone;
52    /// Append extracted values to `buf`.
53    fn extract_into(&self, buf: &mut Vec<Self::Target>);
54    /// Extract into a newly allocated vector.
55    fn extract(&self) -> Vec<Self::Target> {
56        let mut buf = Vec::new();
57        self.extract_into(&mut buf);
58        buf
59    }
60}
61
62impl<E: Extract, I> Extract for I
63where
64    for<'a> &'a I: IntoIterator<Item = &'a E>,
65{
66    type Target = E::Target;
67
68    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
69        for element in self {
70            element.extract_into(buf);
71        }
72    }
73}
74
75/// A marker attached to a time in a scene definition.
76#[derive(Debug, Clone)]
77pub enum TimeMark {
78    /// Capture a picture with a name.
79    Capture(String),
80}
81
82/// Animation definition builder passed to scene constructors.
83///
84/// The public [`RanimScene::root`] stack is the scene's animation composition
85/// root. Calling [`RanimScene::play`] is a convenience alias for pushing into
86/// that stack.
87#[derive(Default)]
88pub struct RanimScene {
89    /// Root animation stack. Modules pushed here share the same local origin.
90    pub root: AnimStack,
91    time_marks: Vec<(f64, TimeMark)>,
92}
93
94impl RanimScene {
95    /// Create an empty scene definition.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Push an animation module into the root stack.
101    pub fn play<A: Animation + 'static>(&mut self, animation: A) -> &mut Self {
102        self.root.push(animation);
103        self
104    }
105
106    /// Insert a time mark.
107    pub fn insert_time_mark(&mut self, sec: f64, time_mark: TimeMark) {
108        self.time_marks.push((sec, time_mark));
109    }
110
111    /// Finish the definition and produce an immutable, evaluable recipe.
112    pub fn seal(self) -> SealedRanimScene {
113        let total_secs = self.root.duration_secs();
114        SealedRanimScene {
115            total_secs,
116            animations: self.root.into_built_animations(),
117            time_marks: self.time_marks,
118        }
119    }
120}
121
122impl Debug for RanimScene {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("RanimScene")
125            .field("animations", &self.root.built_animations().len())
126            .field("duration_secs", &self.root.duration_secs())
127            .finish()
128    }
129}
130
131/// Immutable animation recipe produced by [`RanimScene::seal`].
132pub struct SealedRanimScene {
133    total_secs: f64,
134    animations: Vec<AnimationCell>,
135    time_marks: Vec<(f64, TimeMark)>,
136}
137
138impl SealedRanimScene {
139    /// Consume this scene into a [`SceneEvaluator`] driving session.
140    ///
141    /// `logic_fps` is the fixed logic grid resolution; the time model defaults
142    /// to 120 Hz. Iterative (stateful) segments require the evaluator: the pure
143    /// [`eval_at_sec`](Self::eval_at_sec) path does not advance their state.
144    pub fn into_evaluator(self, logic_fps: f64) -> SceneEvaluator {
145        SceneEvaluator::new(self, logic_fps)
146    }
147
148    /// Total scene duration.
149    pub fn total_secs(&self) -> f64 {
150        self.total_secs
151    }
152
153    /// Scene time marks.
154    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
155        &self.time_marks
156    }
157
158    /// Hierarchical runtime animation information for preview tooling.
159    pub fn get_animation_infos(&self) -> Vec<AnimationInfo> {
160        self.animations
161            .iter()
162            .map(AnimationCell::animation_info)
163            .collect()
164    }
165
166    /// Evaluate all clips active at `target_sec` and extract scene primitives.
167    pub fn eval_at_sec(&self, target_sec: f64) -> impl Iterator<Item = ((usize, usize), CoreItem)> {
168        self.animations
169            .iter()
170            .enumerate()
171            .filter_map(move |(animation_id, animation)| {
172                if !animation.enabled() {
173                    return None;
174                }
175
176                let range = animation.time_range();
177                let active = range.contains(&target_sec)
178                    || (target_sec == self.total_secs && target_sec == range.end);
179                active
180                    .then(|| animation.eval_at_sec(target_sec))
181                    .flatten()
182                    .map(move |items| (animation_id, items))
183            })
184            .flat_map(|(animation_id, items)| {
185                items
186                    .into_iter()
187                    .flat_map(|item| item.extract())
188                    .enumerate()
189                    .map(move |(part, item)| ((animation_id, part), item))
190            })
191    }
192
193    /// Evaluate by normalized scene progress.
194    pub fn eval_at_alpha(&self, alpha: f64) -> impl Iterator<Item = ((usize, usize), CoreItem)> {
195        self.eval_at_sec(self.total_secs * alpha)
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::{
203        animation::{AnimSequence, AnimationExt, Placeable, Static},
204        core_item::vitem::VItem,
205    };
206
207    fn leaf(duration: f64) -> impl Placeable {
208        Static(VItem::default()).with_duration(duration)
209    }
210
211    #[test]
212    fn scene_play_pushes_into_the_root_stack() {
213        let mut scene = RanimScene::new();
214        scene.play(seq![leaf(2.0), leaf(3.0)]);
215        let sealed = scene.seal();
216
217        assert_eq!(sealed.total_secs(), 5.0);
218        let infos = sealed.get_animation_infos();
219        assert_eq!(infos[0].range, 0.0..5.0);
220        assert_eq!(infos[0].children[0].range, 0.0..2.0);
221        assert_eq!(infos[0].children[1].range, 2.0..5.0);
222    }
223
224    #[test]
225    fn extracted_items_have_unique_semantic_part_ids() {
226        let mut scene = RanimScene::new();
227        scene.play(Static(vec![VItem::default(), VItem::default()]).with_duration(1.0));
228        let sealed = scene.seal();
229
230        let ids = sealed
231            .eval_at_sec(0.5)
232            .map(|(id, _)| id)
233            .collect::<Vec<_>>();
234
235        assert_eq!(ids, [(0, 0), (0, 1)]);
236    }
237
238    #[test]
239    fn scene_modules_share_the_root_origin() {
240        let mut reusable = AnimSequence::new();
241        reusable.push(leaf(2.0)).forward(1.0).push(leaf(1.0));
242
243        let mut scene = RanimScene::new();
244        scene.play(reusable);
245        scene.root.push(leaf(5.0));
246        let sealed = scene.seal();
247
248        assert_eq!(sealed.total_secs(), 5.0);
249        let infos = sealed.get_animation_infos();
250        assert_eq!(infos[0].range, 0.0..4.0);
251        assert_eq!(infos[0].children[0].range, 0.0..2.0);
252        assert_eq!(infos[0].children[1].range, 3.0..4.0);
253        assert_eq!(infos[1].range, 0.0..5.0);
254    }
255}