Skip to main content

fbx_dom/objects/
animation_curve.rs

1//! FBX `AnimationCurve` — Assimp [`AnimationCurve`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXAnimation.cpp) / [`FBXDocument.h`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXDocument.h).
2
3use std::collections::HashMap;
4use std::convert::TryFrom;
5use std::num::{ParseFloatError, ParseIntError};
6
7use crate::OwnedObject;
8use crate::Property;
9
10use super::{AttrExtractor, FbxObjectTag, FbxTryFromReason, FbxTypeMismatch, fbx_object_tag};
11
12const ATTR_KEY_TIME: &str = "KeyTime";
13const ATTR_KEY_VALUE_FLOAT: &str = "KeyValueFloat";
14const ATTR_KEY_ATTR_DATA_FLOAT: &str = "KeyAttrDataFloat";
15const ATTR_KEY_ATTR_FLAGS: &str = "KeyAttrFlags";
16
17#[derive(Debug, PartialEq)]
18pub struct AnimationCurve {
19    object: OwnedObject,
20    pub keys: Vec<i64>,
21    pub values: Vec<f32>,
22    pub attributes: Vec<f32>,
23    pub flags: Vec<u32>,
24}
25
26impl AnimationCurve {
27    pub fn inner(&self) -> &OwnedObject {
28        &self.object
29    }
30
31    pub fn into_inner(self) -> OwnedObject {
32        self.object
33    }
34
35    pub fn properties(&self) -> &HashMap<String, Property> {
36        &self.object.properties
37    }
38
39    pub fn property(&self, name: &str) -> Option<&Property> {
40        self.object.properties.get(name)
41    }
42}
43
44impl TryFrom<OwnedObject> for AnimationCurve {
45    type Error = FbxTypeMismatch;
46
47    fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
48        // Check if tagged as AnimationCurve
49        if fbx_object_tag(&o) != FbxObjectTag::AnimationCurve {
50            return Err(FbxTypeMismatch::wrong_object_kind(
51                o,
52                "AnimationCurve".to_string(),
53            ));
54        }
55
56        let attrs = &o.attributes;
57
58        // Extract KeyTime in keys
59        let key_time_el = match attrs.extract_case_insensitive(ATTR_KEY_TIME) {
60            Some(a) => a,
61            None => {
62                return Err(FbxTypeMismatch::new(
63                    o,
64                    FbxTryFromReason::MissingAttribute {
65                        name: ATTR_KEY_TIME.to_string(),
66                    },
67                ));
68            }
69        };
70        let keys_result: Result<Vec<i64>, ParseIntError> = key_time_el
71            .get_tokens()
72            .iter()
73            .flat_map(|t| t.split(','))
74            .map(|t| t.trim())
75            .filter(|t| !t.is_empty())
76            .map(|t| t.parse::<i64>())
77            .collect();
78        let Ok(keys) = keys_result else {
79            return Err(FbxTypeMismatch::new(
80                o,
81                FbxTryFromReason::InvalidAttributeFormat {
82                    name: ATTR_KEY_TIME.to_string(),
83                    detail: format!("invalid int token: {}", keys_result.unwrap_err()),
84                },
85            ));
86        };
87
88        // Extract KeyValueFloat in values
89        let key_value_el = match attrs.extract_case_insensitive(ATTR_KEY_VALUE_FLOAT) {
90            Some(a) => a,
91            None => {
92                return Err(FbxTypeMismatch::new(
93                    o,
94                    FbxTryFromReason::MissingAttribute {
95                        name: ATTR_KEY_VALUE_FLOAT.to_string(),
96                    },
97                ));
98            }
99        };
100        let values_result: Result<Vec<f32>, ParseFloatError> = key_value_el
101            .get_tokens()
102            .iter()
103            .flat_map(|t| t.split(','))
104            .map(|t| t.trim())
105            .filter(|t| !t.is_empty())
106            .map(|t| t.parse::<f32>())
107            .collect();
108        let Ok(values) = values_result else {
109            return Err(FbxTypeMismatch::new(
110                o,
111                FbxTryFromReason::InvalidAttributeFormat {
112                    name: ATTR_KEY_VALUE_FLOAT.to_string(),
113                    detail: format!("invalid float token: {}", values_result.unwrap_err()),
114                },
115            ));
116        };
117
118        // Check if the number of keys and values match
119        if keys.len() != values.len() {
120            return Err(FbxTypeMismatch::new(
121                o,
122                FbxTryFromReason::InvalidAttributeFormat {
123                    name: ATTR_KEY_VALUE_FLOAT.to_string(),
124                    detail: format!(
125                        "the number of key times ({}) does not match the number of keyframe values ({})",
126                        keys.len(),
127                        values.len()
128                    ),
129                },
130            ));
131        }
132
133        // Check if the keys are in ascending order
134        let mut is_sorted = true;
135        for i in 0..keys.len().saturating_sub(1) {
136            if keys[i] >= keys[i + 1] {
137                is_sorted = false;
138                break;
139            }
140        }
141        if !is_sorted {
142            return Err(FbxTypeMismatch::new(
143                o,
144                FbxTryFromReason::InvalidAttributeFormat {
145                    name: ATTR_KEY_TIME.to_string(),
146                    detail: "the keyframes are not in ascending order".to_string(),
147                },
148            ));
149        }
150
151        // Extract KeyAttrDataFloat in attributes
152        let attributes_result: Result<Vec<f32>, ParseFloatError> = attrs
153            .extract_case_insensitive(ATTR_KEY_ATTR_DATA_FLOAT)
154            .map(|a| a.get_tokens())
155            .unwrap_or(&Vec::new())
156            .iter()
157            .flat_map(|t| t.split(','))
158            .map(|t| t.trim())
159            .filter(|t| !t.is_empty())
160            .map(|t| t.parse::<f32>())
161            .collect();
162        let Ok(attributes) = attributes_result else {
163            return Err(FbxTypeMismatch::new(
164                o,
165                FbxTryFromReason::InvalidAttributeFormat {
166                    name: ATTR_KEY_ATTR_DATA_FLOAT.to_string(),
167                    detail: format!("invalid float token: {}", attributes_result.unwrap_err()),
168                },
169            ));
170        };
171
172        // Extract KeyAttrFlags in flags
173        let flags_result: Result<Vec<u32>, ParseIntError> = attrs
174            .extract_case_insensitive(ATTR_KEY_ATTR_FLAGS)
175            .map(|a| a.get_tokens())
176            .unwrap_or(&Vec::new())
177            .iter()
178            .flat_map(|t| t.split(','))
179            .map(|t| t.trim())
180            .filter(|t| !t.is_empty())
181            .map(|t| t.parse::<u32>())
182            .collect();
183        let Ok(flags) = flags_result else {
184            return Err(FbxTypeMismatch::new(
185                o,
186                FbxTryFromReason::InvalidAttributeFormat {
187                    name: ATTR_KEY_ATTR_FLAGS.to_string(),
188                    detail: format!("invalid int token: {}", flags_result.unwrap_err()),
189                },
190            ));
191        };
192
193        Ok(AnimationCurve {
194            object: o,
195            keys,
196            values,
197            attributes,
198            flags,
199        })
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use std::collections::HashMap;
206    use std::convert::TryFrom;
207
208    use fbxscii::{ElementAttribute, LeafAttribute};
209
210    use crate::objects::{ANIMATION_CURVE_CLASS_NAME, ANIMATION_CURVE_TYPE_NAME, FbxTryFromReason};
211    use crate::{OwnedObject, Property};
212
213    use super::{ATTR_KEY_TIME, ATTR_KEY_VALUE_FLOAT, AnimationCurve};
214
215    fn leaf(tokens: &[&str]) -> ElementAttribute {
216        ElementAttribute::Leaf(Box::new(LeafAttribute {
217            key: String::new(),
218            tokens: tokens.iter().map(|s| (*s).to_string()).collect(),
219        }))
220    }
221
222    fn owned_curve(attrs: HashMap<String, ElementAttribute>) -> OwnedObject {
223        OwnedObject {
224            object_index: 1,
225            name: "AnimCurve::X".into(),
226            type_name: ANIMATION_CURVE_TYPE_NAME.into(),
227            class_name: ANIMATION_CURVE_CLASS_NAME.into(),
228            properties: HashMap::new(),
229            attributes: attrs,
230            connected_object_ids: vec![],
231            object_property_targets: vec![],
232            pp_property_targets: HashMap::new(),
233        }
234    }
235
236    #[test]
237    fn parses_keys_values_optional_attr_flags() {
238        let mut props = HashMap::new();
239        props.insert("UserData".to_string(), Property::String("x".into()));
240
241        let mut attrs = HashMap::new();
242        attrs.insert(ATTR_KEY_TIME.into(), leaf(&["0,46186158000"]));
243        attrs.insert(ATTR_KEY_VALUE_FLOAT.into(), leaf(&["0,1"]));
244        attrs.insert("KeyAttrDataFloat".into(), leaf(&["0.5"]));
245        attrs.insert("KeyAttrFlags".into(), leaf(&["1,2"]));
246
247        let mut o = owned_curve(attrs);
248        o.properties = props;
249
250        let c = AnimationCurve::try_from(o).unwrap();
251        assert_eq!(c.keys, vec![0i64, 46186158000]);
252        assert_eq!(c.values, vec![0.0f32, 1.0]);
253        assert_eq!(c.attributes, vec![0.5f32]);
254        assert_eq!(c.flags, vec![1u32, 2u32]);
255        assert_eq!(c.property("UserData"), Some(&Property::String("x".into())));
256    }
257
258    #[test]
259    fn rejects_mismatched_key_value_lengths() {
260        let mut attrs = HashMap::new();
261        attrs.insert(ATTR_KEY_TIME.into(), leaf(&["0,1"]));
262        attrs.insert(ATTR_KEY_VALUE_FLOAT.into(), leaf(&["0"]));
263        let o = owned_curve(attrs);
264        let err = AnimationCurve::try_from(o).unwrap_err();
265        assert!(matches!(
266            err.reason,
267            FbxTryFromReason::InvalidAttributeFormat { ref name, .. }
268            if name == ATTR_KEY_VALUE_FLOAT
269        ));
270    }
271
272    #[test]
273    fn rejects_non_ascending_keys() {
274        let mut attrs = HashMap::new();
275        attrs.insert(ATTR_KEY_TIME.into(), leaf(&["10,5"]));
276        attrs.insert(ATTR_KEY_VALUE_FLOAT.into(), leaf(&["0,1"]));
277        let o = owned_curve(attrs);
278        let err = AnimationCurve::try_from(o).unwrap_err();
279        assert!(matches!(
280            err.reason,
281            FbxTryFromReason::InvalidAttributeFormat { ref name, .. }
282            if name == ATTR_KEY_TIME
283        ));
284    }
285}