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/// The audio plane of a scene.
15pub mod audio;
16/// Color utilities.
17pub mod color;
18/// Component data.
19pub mod components;
20/// Fundamental scene primitives.
21pub mod core_item;
22/// Scene evaluation driver (lightweight session, no ECS).
23pub mod scene_evaluator;
24/// Time vocabulary for animation evaluation.
25pub mod time;
26/// Fundamental traits.
27pub mod traits;
28pub use scene_evaluator::SceneEvaluator;
29/// Utilities.
30pub mod utils;
31
32pub use glam;
33use std::fmt::Debug;
34use std::sync::Arc;
35
36use animation::{
37    build::IntoAnimNode,
38    compose::stack::AnimStack,
39    node::{AnimNode, AnimationInfo, bake_audio},
40};
41use core_item::CoreItem;
42
43/// Commonly used ranim APIs.
44pub mod prelude {
45    pub use crate::color::prelude::*;
46    pub use crate::traits::*;
47
48    pub use crate::animation::build::{IntoAnimNode, PlaybackExt, Unplaced};
49    pub use crate::animation::compose::{
50        AnimIterExt,
51        lagged::{AnimLagged, LaggedFill},
52        sequence::AnimSequence,
53        stack::AnimStack,
54    };
55    pub use crate::animation::eval::{
56        Eval, EvalExt, Static, StaticAnim,
57        iterative::{Iterative, IterativeEval, IterativeFn},
58        pure::Pure,
59    };
60    pub use crate::animation::sound::Sound;
61    pub use crate::audio::{AudioClip, AudioTrack};
62    pub use crate::core_item::CoreItem;
63    pub use crate::core_item::camera_frame::CameraFrame;
64    pub use crate::core_item::transformed::{Transformed, TransformedExt};
65    pub use crate::{Extract, RanimScene, TimeMark};
66}
67
68/// Extract one or more target values from a reference.
69pub trait Extract {
70    /// Extraction target.
71    type Target: Clone;
72    /// Append extracted values to `buf`.
73    fn extract_into(&self, buf: &mut Vec<Self::Target>);
74    /// Extract into a newly allocated vector.
75    fn extract(&self) -> Vec<Self::Target> {
76        let mut buf = Vec::new();
77        self.extract_into(&mut buf);
78        buf
79    }
80}
81
82/// Sealed marker: types whose references iterate over `&Self::Item`.
83///
84/// `Extract` is blanket-implemented for these containers. Keeping the bound on a
85/// local (sealed) trait instead of raw `IntoIterator` lets coherence prove that
86/// tuples are disjoint, which is what makes the direct tuple impls below possible
87/// without a newtype wrapper or nightly `tuple_trait`.
88mod sealed {
89    pub trait IntoExtractIter {
90        type Item;
91    }
92    impl<E> IntoExtractIter for Vec<E> {
93        type Item = E;
94    }
95    impl<E> IntoExtractIter for std::collections::VecDeque<E> {
96        type Item = E;
97    }
98    impl<E> IntoExtractIter for std::collections::LinkedList<E> {
99        type Item = E;
100    }
101    impl<E> IntoExtractIter for std::collections::HashSet<E> {
102        type Item = E;
103    }
104    impl<E> IntoExtractIter for std::collections::BTreeSet<E> {
105        type Item = E;
106    }
107    impl<E> IntoExtractIter for std::collections::BinaryHeap<E> {
108        type Item = E;
109    }
110    impl<E> IntoExtractIter for Option<E> {
111        type Item = E;
112    }
113    impl<E, const N: usize> IntoExtractIter for [E; N] {
114        type Item = E;
115    }
116}
117use sealed::IntoExtractIter;
118
119impl<I: IntoExtractIter> Extract for I
120where
121    I::Item: Extract,
122    for<'a> &'a I: IntoIterator<Item = &'a I::Item>,
123{
124    type Target = <I::Item as Extract>::Target;
125
126    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
127        for element in self {
128            element.extract_into(buf);
129        }
130    }
131}
132
133/// Direct `Extract` impls for tuples. All elements must extract to the same
134/// `Target`, which is the natural semantics of a single `Target` associated type.
135macro_rules! impl_extract_for_tuple {
136    ($(($E:ident, $e:ident)),*) => {
137        impl<T: Clone, $($E: Extract<Target = T>),*> Extract for ($($E,)*) {
138            type Target = T;
139
140            fn extract_into(&self, buf: &mut Vec<Self::Target>) {
141                let ($($e,)*) = self;
142                $($e.extract_into(buf);)*
143            }
144        }
145    };
146}
147
148// Arity 1..=15, same as the sibling `impl_interpolatable_tuple` pattern.
149variadics_please::all_tuples!(impl_extract_for_tuple, 1, 15, E, e);
150
151/// A marker attached to a time in a scene definition.
152#[derive(Debug, Clone)]
153pub enum TimeMark {
154    /// Capture a picture with a name.
155    Capture(String),
156}
157
158/// Animation definition builder passed to scene constructors.
159///
160/// The public [`RanimScene::root`] stack is the scene's animation composition
161/// root. Calling [`RanimScene::play`] is a convenience alias for pushing into
162/// that stack.
163#[derive(Default)]
164pub struct RanimScene {
165    /// Root animation stack. Modules pushed here share the same local origin.
166    ///
167    /// Audio leaves ([`Sound`](crate::animation::sound::Sound)) compose in the same tree beside visual
168    /// animations; [`RanimScene::seal`] bakes them through the same cell
169    /// remaps the visuals experience.
170    pub root: AnimStack,
171    time_marks: Vec<(f64, TimeMark)>,
172}
173
174impl RanimScene {
175    /// Create an empty scene definition.
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Push an animation module into the root stack.
181    pub fn play<A: IntoAnimNode + 'static>(&mut self, animation: A) -> &mut Self {
182        self.root.push(animation);
183        self
184    }
185
186    /// Insert a time mark.
187    pub fn insert_time_mark(&mut self, sec: f64, time_mark: TimeMark) {
188        self.time_marks.push((sec, time_mark));
189    }
190
191    /// Finish the definition and produce an immutable, evaluable recipe.
192    ///
193    /// The tree is the only animation representation: visual cells are
194    /// point-walked per frame by [`SceneEvaluator::sample_at`], and sound
195    /// leaves are baked here — see [`bake_audio`](crate::animation) — both
196    /// experiencing the same cell remaps along the path.
197    pub fn seal(self) -> SealedRanimScene {
198        let total_secs = self.root.duration_secs();
199        let animations = self.root.into_built_animations();
200        let sample_rate = crate::audio::MASTER_SAMPLE_RATE as f64;
201        let audio: Arc<[f32]> = bake_audio(&animations, total_secs, sample_rate).into();
202        SealedRanimScene {
203            total_secs,
204            animations,
205            audio,
206            time_marks: self.time_marks,
207        }
208    }
209}
210
211impl Debug for RanimScene {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.debug_struct("RanimScene")
214            .field("animations", &self.root.built_animations().len())
215            .field("duration_secs", &self.root.duration_secs())
216            .finish()
217    }
218}
219
220/// Immutable animation recipe produced by [`RanimScene::seal`].
221pub struct SealedRanimScene {
222    total_secs: f64,
223    animations: Vec<AnimNode>,
224    /// The baked audio plane: interleaved stereo at the master sample rate
225    /// over `[0, total_secs]`, produced once at seal. Empty when the tree
226    /// has no sound leaves.
227    audio: Arc<[f32]>,
228    time_marks: Vec<(f64, TimeMark)>,
229}
230
231impl SealedRanimScene {
232    /// Consume this scene into a [`SceneEvaluator`] driving session.
233    ///
234    /// `logic_fps` is retained for call-site compatibility; it no longer drives
235    /// stepping (each iterative segment owns its own `sim_step`).
236    pub fn into_evaluator(self, logic_fps: f64) -> SceneEvaluator {
237        SceneEvaluator::new(self, logic_fps)
238    }
239
240    /// The baked audio plane: interleaved stereo at the master sample rate
241    /// over `[0, total_secs]` (shared, cheap to clone). Empty when the scene
242    /// has no sound leaves.
243    pub fn audio(&self) -> &Arc<[f32]> {
244        &self.audio
245    }
246
247    /// Total scene duration.
248    pub fn total_secs(&self) -> f64 {
249        self.total_secs
250    }
251
252    /// Scene time marks.
253    pub fn time_marks(&self) -> &[(f64, TimeMark)] {
254        &self.time_marks
255    }
256
257    /// Hierarchical runtime animation information for preview tooling.
258    pub fn get_animation_infos(&self) -> Vec<AnimationInfo> {
259        self.animations
260            .iter()
261            .map(AnimNode::animation_info)
262            .collect()
263    }
264
265    /// Sample all clips active at `target_sec` and extract scene primitives.
266    ///
267    /// Pure query path: every active cell evaluates itself at `target_sec`
268    /// (stateful segments reset/replay or integrate internally as needed).
269    pub fn eval_at_sec(&self, target_sec: f64) -> impl Iterator<Item = ((usize, usize), CoreItem)> {
270        self.animations
271            .iter()
272            .enumerate()
273            .filter_map(move |(animation_id, animation)| {
274                if !animation.enabled() {
275                    return None;
276                }
277
278                let mut items = Vec::new();
279                animation.eval_at(target_sec, &mut items);
280                (!items.is_empty()).then_some((animation_id, items))
281            })
282            .flat_map(|(animation_id, items)| {
283                items
284                    .into_iter()
285                    .flat_map(|item| item.extract())
286                    .enumerate()
287                    .map(move |(part, item)| ((animation_id, part), item))
288            })
289    }
290
291    /// Evaluate by normalized scene progress.
292    pub fn eval_at_alpha(&self, alpha: f64) -> impl Iterator<Item = ((usize, usize), CoreItem)> {
293        self.eval_at_sec(self.total_secs * alpha)
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::{
301        animation::{
302            build::{PlaybackExt, Unplaced},
303            compose::sequence::AnimSequence,
304            eval::Static,
305        },
306        core_item::vitem::VItem,
307    };
308
309    fn leaf(duration: f64) -> impl Unplaced {
310        Static(VItem::default()).with_duration(duration)
311    }
312
313    #[test]
314    fn scene_play_places_sequences_on_the_root_timeline() {
315        // A plain sequence is sized from its children.
316        let mut scene = RanimScene::new();
317        scene.play(seq![leaf(2.0), leaf(3.0)]);
318        let sealed = scene.seal();
319        assert_eq!(sealed.total_secs(), 5.0);
320        let infos = sealed.get_animation_infos();
321        assert_eq!(infos[0].range, 0.0..5.0);
322        assert_eq!(infos[0].children[0].range, 0.0..2.0);
323        assert_eq!(infos[0].children[1].range, 2.0..5.0);
324
325        // A reusable sequence module keeps its local gaps on the root stack,
326        // and later modules start at the root origin.
327        let mut reusable = AnimSequence::new();
328        reusable.push(leaf(2.0)).forward(1.0).push(leaf(1.0));
329        let mut scene = RanimScene::new();
330        scene.play(reusable);
331        scene.root.push(leaf(5.0));
332        let sealed = scene.seal();
333        assert_eq!(sealed.total_secs(), 5.0);
334        let infos = sealed.get_animation_infos();
335        assert_eq!(infos[0].range, 0.0..4.0);
336        assert_eq!(infos[0].children[1].range, 3.0..4.0);
337        assert_eq!(infos[1].range, 0.0..5.0);
338    }
339
340    #[test]
341    fn extracted_items_have_unique_semantic_part_ids() {
342        let mut scene = RanimScene::new();
343        scene.play(Static(vec![VItem::default(), VItem::default()]).with_duration(1.0));
344        let sealed = scene.seal();
345
346        let ids = sealed
347            .eval_at_sec(0.5)
348            .map(|(id, _)| id)
349            .collect::<Vec<_>>();
350
351        assert_eq!(ids, [(0, 0), (0, 1)]);
352    }
353
354    #[test]
355    fn heterogeneous_tuples_extract_to_core_items() {
356        use crate::core_item::{camera_frame::CameraFrame, mesh_item::MeshItem};
357
358        let camera = CameraFrame::default();
359        let vitem = VItem::default();
360        let mesh = MeshItem::default();
361
362        // Heterogeneous tuple -> single shared `Target` (`CoreItem`).
363        let items = (camera.clone(), vitem.clone()).extract();
364        assert_eq!(items.len(), 2);
365        assert!(matches!(&items[0], CoreItem::CameraFrame(_)));
366        assert!(matches!(&items[1], CoreItem::VItem(_)));
367
368        // Three distinct element types, still one `Target`.
369        let items = (camera.clone(), vitem.clone(), mesh.clone()).extract();
370        assert_eq!(items.len(), 3);
371
372        // Containers of tuples (container blanket impl + tuple impl).
373        let items = vec![
374            (camera.clone(), vitem.clone()),
375            (camera.clone(), vitem.clone()),
376        ]
377        .extract();
378        assert_eq!(items.len(), 4);
379
380        // High arity still works.
381        let t13 = (
382            camera.clone(),
383            vitem.clone(),
384            mesh.clone(),
385            camera.clone(),
386            vitem.clone(),
387            mesh.clone(),
388            camera.clone(),
389            vitem.clone(),
390            mesh.clone(),
391            camera.clone(),
392            vitem.clone(),
393            mesh.clone(),
394            camera.clone(),
395        );
396        assert_eq!(t13.extract().len(), 13);
397    }
398}