Skip to main content

dreamwell_engine/content/
animation.rs

1//! Imported animation types — GPU-neutral keyframe data.
2
3use serde::{Deserialize, Serialize};
4
5/// Interpolation mode for keyframes.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
7pub enum Interpolation {
8    Step,
9    #[default]
10    Linear,
11    CubicSpline,
12}
13
14/// A single keyframe at a specific time.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Keyframe {
17    /// Time in seconds from animation start.
18    pub time: f32,
19    /// Value array — interpretation depends on the track target:
20    /// - Translation: `[x, y, z]`
21    /// - Rotation: `[qx, qy, qz, qw]`
22    /// - Scale: `[sx, sy, sz]`
23    pub value: Vec<f32>,
24}
25
26/// An animation track targeting a specific node property.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AnimationTrack {
29    /// Index of the target node in the scene's node list.
30    pub node_index: usize,
31    /// Property being animated.
32    pub property: AnimatedProperty,
33    /// Interpolation mode.
34    pub interpolation: Interpolation,
35    /// Keyframes sorted by time.
36    pub keyframes: Vec<Keyframe>,
37}
38
39/// Which transform property a track animates.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub enum AnimatedProperty {
42    Translation,
43    Rotation,
44    Scale,
45}
46
47/// A complete imported animation clip.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ImportedAnimation {
50    /// Animation name (from source file).
51    pub name: String,
52    /// Duration in seconds.
53    pub duration: f32,
54    /// Animation tracks.
55    pub tracks: Vec<AnimationTrack>,
56}
57
58impl ImportedAnimation {
59    /// Number of tracks in this animation.
60    pub fn track_count(&self) -> usize {
61        self.tracks.len()
62    }
63
64    /// Total number of keyframes across all tracks.
65    pub fn total_keyframes(&self) -> usize {
66        self.tracks.iter().map(|t| t.keyframes.len()).sum()
67    }
68
69    /// Validate animation data.
70    pub fn validate(&self) -> Result<(), String> {
71        if self.duration < 0.0 || !self.duration.is_finite() {
72            return Err(format!("content_animation_invalid_duration:{}", self.duration));
73        }
74        for (i, track) in self.tracks.iter().enumerate() {
75            // Keyframes must be sorted by time.
76            for w in track.keyframes.windows(2) {
77                if w[1].time < w[0].time {
78                    return Err(format!(
79                        "content_animation_unsorted_keyframes:track[{i}] time {} > {}",
80                        w[0].time, w[1].time
81                    ));
82                }
83            }
84        }
85        Ok(())
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn sample_animation() -> ImportedAnimation {
94        ImportedAnimation {
95            name: "Walk".into(),
96            duration: 1.0,
97            tracks: vec![AnimationTrack {
98                node_index: 0,
99                property: AnimatedProperty::Translation,
100                interpolation: Interpolation::Linear,
101                keyframes: vec![
102                    Keyframe {
103                        time: 0.0,
104                        value: vec![0.0, 0.0, 0.0],
105                    },
106                    Keyframe {
107                        time: 0.5,
108                        value: vec![1.0, 0.0, 0.0],
109                    },
110                    Keyframe {
111                        time: 1.0,
112                        value: vec![2.0, 0.0, 0.0],
113                    },
114                ],
115            }],
116        }
117    }
118
119    #[test]
120    fn valid_animation() {
121        let anim = sample_animation();
122        assert!(anim.validate().is_ok());
123        assert_eq!(anim.track_count(), 1);
124        assert_eq!(anim.total_keyframes(), 3);
125    }
126
127    #[test]
128    fn negative_duration() {
129        let mut anim = sample_animation();
130        anim.duration = -1.0;
131        assert!(anim.validate().unwrap_err().contains("invalid_duration"));
132    }
133
134    #[test]
135    fn unsorted_keyframes() {
136        let mut anim = sample_animation();
137        anim.tracks[0].keyframes[1].time = 2.0; // 0.0, 2.0, 1.0 — unsorted
138        assert!(anim.validate().unwrap_err().contains("unsorted"));
139    }
140
141    #[test]
142    fn interpolation_default_linear() {
143        assert_eq!(Interpolation::default(), Interpolation::Linear);
144    }
145}