1use 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 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 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 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 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 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 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 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}