Skip to main content

fbx_dom/objects/
animation_curve_node.rs

1//! FBX `AnimationCurveNode` — Assimp [`AnimationCurveNode`](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//!
3//! Target object and animated property name are resolved via connections in Assimp; this wrapper
4//! only exposes the property table on [`OwnedObject`].
5
6use std::collections::HashMap;
7use std::convert::TryFrom;
8
9use crate::Property;
10use crate::{ObjectPropertyConnection, OwnedDocument, OwnedObject};
11
12use super::{
13    AnimationCurve, FbxObjectTag, FbxTypeMismatch, Model, NodeAttributeRef, fbx_object_tag,
14};
15
16#[derive(Debug, PartialEq)]
17pub struct AnimationCurveNode(pub OwnedObject);
18
19impl AnimationCurveNode {
20    pub fn inner(&self) -> &OwnedObject {
21        &self.0
22    }
23
24    pub fn into_inner(self) -> OwnedObject {
25        self.0
26    }
27
28    pub fn properties(&self) -> &HashMap<String, Property> {
29        &self.0.properties
30    }
31
32    pub fn property(&self, name: &str) -> Option<&Property> {
33        self.0.properties.get(name)
34    }
35
36    /// First valid OP target with a non-empty property name, mirroring Assimp's search order.
37    fn get_target_property_connection(&self) -> Option<&ObjectPropertyConnection> {
38        self.inner()
39            .object_property_targets
40            .iter()
41            .find(|c| !c.property.is_empty())
42    }
43
44    pub fn get_target_model<'a>(&'a self, document: &'a OwnedDocument) -> Option<&'a Model> {
45        let target = self.get_target_property_connection()?;
46        document
47            .models
48            .iter()
49            .find(|m| m.inner().object_index == target.dest)
50    }
51
52    pub fn get_target_node_attribute<'a>(
53        &'a self,
54        document: &'a OwnedDocument,
55    ) -> Option<NodeAttributeRef<'a>> {
56        let target = self.get_target_property_connection()?;
57        if let Some(v) = document
58            .cameras
59            .iter()
60            .find(|x| x.inner().object_index == target.dest)
61        {
62            return Some(NodeAttributeRef::Camera(v));
63        }
64        if let Some(v) = document
65            .camera_switchers
66            .iter()
67            .find(|x| x.inner().object_index == target.dest)
68        {
69            return Some(NodeAttributeRef::CameraSwitcher(v));
70        }
71        if let Some(v) = document
72            .lights
73            .iter()
74            .find(|x| x.inner().object_index == target.dest)
75        {
76            return Some(NodeAttributeRef::Light(v));
77        }
78        if let Some(v) = document
79            .null_nodes
80            .iter()
81            .find(|x| x.inner().object_index == target.dest)
82        {
83            return Some(NodeAttributeRef::NullNode(v));
84        }
85        if let Some(v) = document
86            .limb_nodes
87            .iter()
88            .find(|x| x.inner().object_index == target.dest)
89        {
90            return Some(NodeAttributeRef::LimbNode(v));
91        }
92        document
93            .unknown_node_attributes
94            .iter()
95            .find(|x| x.object_index == target.dest)
96            .map(NodeAttributeRef::Unknown)
97    }
98
99    pub fn get_target_deformer<'a>(
100        &'a self,
101        document: &'a OwnedDocument,
102    ) -> Option<&'a OwnedObject> {
103        let target = self.get_target_property_connection()?;
104        if let Some(v) = document
105            .clusters
106            .iter()
107            .find(|x| x.inner().object_index == target.dest)
108        {
109            return Some(v.inner());
110        }
111        if let Some(v) = document
112            .skins
113            .iter()
114            .find(|x| x.inner().object_index == target.dest)
115        {
116            return Some(v.inner());
117        }
118        if let Some(v) = document
119            .blend_shapes
120            .iter()
121            .find(|x| x.inner().object_index == target.dest)
122        {
123            return Some(v.inner());
124        }
125        if let Some(v) = document
126            .blend_shape_channels
127            .iter()
128            .find(|x| x.inner().object_index == target.dest)
129        {
130            return Some(v.inner());
131        }
132        document
133            .unknown_deformers
134            .iter()
135            .find(|x| x.object_index == target.dest)
136    }
137
138    /// Resolve incoming `AnimationCurve -> AnimationCurveNode` OP links keyed by property name.
139    pub fn get_curves<'a>(
140        &'a self,
141        document: &'a OwnedDocument,
142    ) -> HashMap<&'a str, &'a AnimationCurve> {
143        let node_id = self.inner().object_index;
144        let mut out = HashMap::new();
145        for curve in &document.animation_curves {
146            for conn in &curve.inner().object_property_targets {
147                if conn.dest == node_id && !conn.property.is_empty() {
148                    out.insert(conn.property.as_str(), curve);
149                }
150            }
151        }
152        out
153    }
154}
155
156impl TryFrom<OwnedObject> for AnimationCurveNode {
157    type Error = FbxTypeMismatch;
158
159    fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
160        match fbx_object_tag(&o) {
161            FbxObjectTag::AnimationCurveNode => Ok(AnimationCurveNode(o)),
162            _ => Err(FbxTypeMismatch::wrong_object_kind(
163                o,
164                "AnimationCurveNode".to_string(),
165            )),
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use std::collections::HashMap;
173    use std::convert::TryFrom;
174
175    use crate::objects::{
176        ANIMATION_CURVE_CLASS_NAME, ANIMATION_CURVE_NODE_CLASS_NAME,
177        ANIMATION_CURVE_NODE_TYPE_NAME, ANIMATION_CURVE_TYPE_NAME, DEFORMER_TYPE_NAME,
178        MODEL_TYPE_NAME, NodeAttributeRef,
179    };
180    use crate::{ObjectPropertyConnection, OwnedDocument, OwnedObject, Property};
181
182    use super::AnimationCurveNode;
183
184    #[test]
185    fn property_accessors() {
186        let mut properties = HashMap::new();
187        properties.insert("d|Visibility".to_string(), Property::Float(1.0));
188        let o = OwnedObject {
189            object_index: 2,
190            name: "AnimCurveNode::T".into(),
191            type_name: ANIMATION_CURVE_NODE_TYPE_NAME.into(),
192            class_name: ANIMATION_CURVE_NODE_CLASS_NAME.into(),
193            properties,
194            attributes: HashMap::new(),
195            connected_object_ids: vec![],
196            object_property_targets: vec![],
197            pp_property_targets: HashMap::new(),
198        };
199        let n = AnimationCurveNode::try_from(o).unwrap();
200        assert_eq!(n.property("d|Visibility"), Some(&Property::Float(1.0)));
201    }
202
203    #[test]
204    fn resolves_target_model_and_curves() {
205        let node = AnimationCurveNode::try_from(OwnedObject {
206            object_index: 3000,
207            name: "AnimCurveNode::T".into(),
208            type_name: ANIMATION_CURVE_NODE_TYPE_NAME.into(),
209            class_name: ANIMATION_CURVE_NODE_CLASS_NAME.into(),
210            properties: HashMap::new(),
211            attributes: HashMap::new(),
212            connected_object_ids: vec![],
213            object_property_targets: vec![ObjectPropertyConnection {
214                dest: 3001,
215                property: "Lcl Translation".into(),
216            }],
217            pp_property_targets: HashMap::new(),
218        })
219        .unwrap();
220
221        let model = crate::objects::Model::try_from(OwnedObject {
222            object_index: 3001,
223            name: "Model::Node".into(),
224            type_name: MODEL_TYPE_NAME.into(),
225            class_name: "Mesh".into(),
226            properties: HashMap::new(),
227            attributes: HashMap::new(),
228            connected_object_ids: vec![],
229            object_property_targets: vec![],
230            pp_property_targets: HashMap::new(),
231        })
232        .unwrap();
233
234        let curve = crate::objects::AnimationCurve::try_from(OwnedObject {
235            object_index: 3002,
236            name: "AnimCurve::X".into(),
237            type_name: ANIMATION_CURVE_TYPE_NAME.into(),
238            class_name: ANIMATION_CURVE_CLASS_NAME.into(),
239            properties: HashMap::new(),
240            attributes: HashMap::from([
241                ("KeyTime".to_string(), {
242                    use fbxscii::{ElementAttribute, LeafAttribute};
243                    ElementAttribute::Leaf(Box::new(LeafAttribute {
244                        key: String::new(),
245                        tokens: vec!["0,1".into()],
246                    }))
247                }),
248                ("KeyValueFloat".to_string(), {
249                    use fbxscii::{ElementAttribute, LeafAttribute};
250                    ElementAttribute::Leaf(Box::new(LeafAttribute {
251                        key: String::new(),
252                        tokens: vec!["0,1".into()],
253                    }))
254                }),
255            ]),
256            connected_object_ids: vec![],
257            object_property_targets: vec![ObjectPropertyConnection {
258                dest: 3000,
259                property: "d|X".into(),
260            }],
261            pp_property_targets: HashMap::new(),
262        })
263        .unwrap();
264
265        let mut owned = OwnedDocument::default();
266        owned.models = vec![model];
267        owned.animation_curves = vec![curve];
268
269        assert_eq!(
270            node.get_target_model(&owned)
271                .map(|m| m.inner().object_index),
272            Some(3001)
273        );
274        let curves = node.get_curves(&owned);
275        assert_eq!(curves.len(), 1);
276        assert_eq!(
277            curves.get("d|X").map(|curve| curve.inner().object_index),
278            Some(3002)
279        );
280    }
281
282    #[test]
283    fn resolves_target_node_attribute_ref() {
284        let node = AnimationCurveNode::try_from(OwnedObject {
285            object_index: 3100,
286            name: "AnimCurveNode::T".into(),
287            type_name: ANIMATION_CURVE_NODE_TYPE_NAME.into(),
288            class_name: ANIMATION_CURVE_NODE_CLASS_NAME.into(),
289            properties: HashMap::new(),
290            attributes: HashMap::new(),
291            connected_object_ids: vec![],
292            object_property_targets: vec![ObjectPropertyConnection {
293                dest: 3101,
294                property: "d|Visibility".into(),
295            }],
296            pp_property_targets: HashMap::new(),
297        })
298        .unwrap();
299
300        let camera = crate::objects::Camera::try_from(OwnedObject {
301            object_index: 3101,
302            name: "NodeAttribute::Cam".into(),
303            type_name: "NodeAttribute".into(),
304            class_name: "Camera".into(),
305            properties: HashMap::new(),
306            attributes: HashMap::new(),
307            connected_object_ids: vec![],
308            object_property_targets: vec![],
309            pp_property_targets: HashMap::new(),
310        })
311        .unwrap();
312
313        let mut owned = OwnedDocument::default();
314        owned.cameras = vec![camera];
315        let target = node.get_target_node_attribute(&owned).unwrap();
316        assert!(matches!(target, NodeAttributeRef::Camera(_)));
317    }
318
319    #[test]
320    fn resolves_target_deformer() {
321        let node = AnimationCurveNode::try_from(OwnedObject {
322            object_index: 3200,
323            name: "AnimCurveNode::T".into(),
324            type_name: ANIMATION_CURVE_NODE_TYPE_NAME.into(),
325            class_name: ANIMATION_CURVE_NODE_CLASS_NAME.into(),
326            properties: HashMap::new(),
327            attributes: HashMap::new(),
328            connected_object_ids: vec![],
329            object_property_targets: vec![ObjectPropertyConnection {
330                dest: 3201,
331                property: "DeformPercent".into(),
332            }],
333            pp_property_targets: HashMap::new(),
334        })
335        .unwrap();
336
337        let mut owned = OwnedDocument::default();
338        owned.unknown_deformers = vec![OwnedObject {
339            object_index: 3201,
340            name: "Deformer::Custom".into(),
341            type_name: DEFORMER_TYPE_NAME.into(),
342            class_name: "CustomDeformer".into(),
343            properties: HashMap::new(),
344            attributes: HashMap::new(),
345            connected_object_ids: vec![],
346            object_property_targets: vec![],
347            pp_property_targets: HashMap::new(),
348        }];
349
350        let target = node.get_target_deformer(&owned).unwrap();
351        assert_eq!(target.object_index, 3201);
352    }
353}