Skip to main content

fbx_dom/objects/
cluster.rs

1//! FBX `Deformer` / `Cluster` — Assimp [`Cluster`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXDocument.h).
2
3use std::collections::HashMap;
4use std::convert::TryFrom;
5
6use crate::{OwnedDocument, OwnedObject, Property};
7
8use super::{
9    AttrExtractor, FbxObjectTag, FbxTryFromReason, FbxTypeMismatch, Model, fbx_object_tag,
10};
11
12const ATTR_INDEXES: &str = "Indexes";
13const ATTR_WEIGHTS: &str = "Weights";
14const ATTR_TRANSFORM: &str = "Transform";
15const ATTR_TRANSFORM_LINK: &str = "TransformLink";
16
17#[derive(Debug, PartialEq)]
18pub struct Cluster {
19    object: OwnedObject,
20    pub indices: Vec<u32>,
21    pub weights: Vec<f32>,
22    pub transform: [[f32; 4]; 4],
23    pub transform_link: [[f32; 4]; 4],
24}
25
26impl Cluster {
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    /// Resolve `Model -> Cluster` link (Assimp destination-side lookup).
44    pub fn get_target_model<'a>(&'a self, document: &'a OwnedDocument) -> Option<&'a Model> {
45        let cluster_id = self.inner().object_index;
46        document
47            .models
48            .iter()
49            .find(|model| model.inner().connected_object_ids.contains(&cluster_id))
50    }
51}
52
53impl TryFrom<OwnedObject> for Cluster {
54    type Error = FbxTypeMismatch;
55
56    fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
57        if fbx_object_tag(&o) != FbxObjectTag::Cluster {
58            return Err(FbxTypeMismatch::wrong_object_kind(o, "Cluster".to_string()));
59        }
60
61        let attrs = &o.attributes;
62        let transform = match attrs.extract_case_insensitive(ATTR_TRANSFORM) {
63            Some(a) => match parse_matrix_4x4(a) {
64                Ok(m) => m,
65                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
66            },
67            None => {
68                return Err(FbxTypeMismatch::new(
69                    o,
70                    FbxTryFromReason::MissingAttribute {
71                        name: ATTR_TRANSFORM.to_string(),
72                    },
73                ));
74            }
75        };
76        let transform_link = match attrs.extract_case_insensitive(ATTR_TRANSFORM_LINK) {
77            Some(a) => match parse_matrix_4x4(a) {
78                Ok(m) => m,
79                Err(reason) => return Err(FbxTypeMismatch::new(o, reason)),
80            },
81            None => {
82                return Err(FbxTypeMismatch::new(
83                    o,
84                    FbxTryFromReason::MissingAttribute {
85                        name: ATTR_TRANSFORM_LINK.to_string(),
86                    },
87                ));
88            }
89        };
90
91        let indexes_attr = attrs.extract_case_insensitive(ATTR_INDEXES);
92        let weights_attr = attrs.extract_case_insensitive(ATTR_WEIGHTS);
93        if indexes_attr.is_some() != weights_attr.is_some() {
94            return Err(FbxTypeMismatch::new(
95                o,
96                FbxTryFromReason::InvalidAttributeFormat {
97                    name: "Cluster".to_string(),
98                    detail: "either Indexes or Weights are missing from Cluster".to_string(),
99                },
100            ));
101        }
102
103        let indices = indexes_attr
104            .map(|attr| {
105                attr.get_tokens()
106                    .iter()
107                    .flat_map(|t| t.split(','))
108                    .map(|t| t.trim())
109                    .filter(|t| !t.is_empty())
110                    .filter_map(|t| t.parse::<u32>().ok())
111                    .collect::<Vec<u32>>()
112            })
113            .unwrap_or_default();
114        let weights = weights_attr
115            .map(|attr| {
116                attr.get_tokens()
117                    .iter()
118                    .flat_map(|t| t.split(','))
119                    .map(|t| t.trim())
120                    .filter(|t| !t.is_empty())
121                    .filter_map(|t| t.parse::<f32>().ok())
122                    .collect::<Vec<f32>>()
123            })
124            .unwrap_or_default();
125        if indices.len() != weights.len() {
126            return Err(FbxTypeMismatch::new(
127                o,
128                FbxTryFromReason::InvalidAttributeFormat {
129                    name: "Cluster".to_string(),
130                    detail: "sizes of index and weight array don't match up".to_string(),
131                },
132            ));
133        }
134
135        Ok(Cluster {
136            object: o,
137            indices,
138            weights,
139            transform,
140            transform_link,
141        })
142    }
143}
144
145fn parse_matrix_4x4(attr: &fbxscii::ElementAttribute) -> Result<[[f32; 4]; 4], FbxTryFromReason> {
146    let flat: Vec<f32> = attr
147        .get_tokens()
148        .iter()
149        .flat_map(|t| t.split(','))
150        .map(|t| t.trim())
151        .filter(|t| !t.is_empty())
152        .filter_map(|t| t.parse::<f32>().ok())
153        .collect();
154    if flat.len() != 16 {
155        return Err(FbxTryFromReason::InvalidAttributeFormat {
156            name: "Matrix".to_string(),
157            detail: format!("expected 16 floats, got {}", flat.len()),
158        });
159    }
160    Ok([
161        [flat[0], flat[1], flat[2], flat[3]],
162        [flat[4], flat[5], flat[6], flat[7]],
163        [flat[8], flat[9], flat[10], flat[11]],
164        [flat[12], flat[13], flat[14], flat[15]],
165    ])
166}
167
168#[cfg(test)]
169mod tests {
170    use std::collections::HashMap;
171    use std::convert::TryFrom;
172
173    use fbxscii::{ElementAttribute, LeafAttribute};
174
175    use crate::OwnedDocument;
176    use crate::objects::{
177        DEFORMER_CLUSTER_CLASS_NAME, DEFORMER_TYPE_NAME, FbxTryFromReason, MODEL_TYPE_NAME, Model,
178    };
179    use crate::{OwnedObject, Property};
180
181    use super::{ATTR_INDEXES, ATTR_TRANSFORM, ATTR_TRANSFORM_LINK, ATTR_WEIGHTS, Cluster};
182
183    fn leaf(tokens: &[&str]) -> ElementAttribute {
184        ElementAttribute::Leaf(Box::new(LeafAttribute {
185            key: String::new(),
186            tokens: tokens.iter().map(|s| (*s).to_string()).collect(),
187        }))
188    }
189
190    fn matrix_csv() -> &'static str {
191        "1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1"
192    }
193
194    #[test]
195    fn parses_cluster_fields_and_properties() {
196        let mut attrs = HashMap::new();
197        attrs.insert(ATTR_TRANSFORM.into(), leaf(&[matrix_csv()]));
198        attrs.insert(ATTR_TRANSFORM_LINK.into(), leaf(&[matrix_csv()]));
199        attrs.insert(ATTR_INDEXES.into(), leaf(&["0,2,4"]));
200        attrs.insert(ATTR_WEIGHTS.into(), leaf(&["0.2,0.5,1.0"]));
201        let mut props = HashMap::new();
202        props.insert("Foo".into(), Property::Bool(true));
203        let o = OwnedObject {
204            object_index: 12,
205            name: "Cluster::A".into(),
206            type_name: DEFORMER_TYPE_NAME.into(),
207            class_name: DEFORMER_CLUSTER_CLASS_NAME.into(),
208            properties: props,
209            attributes: attrs,
210            connected_object_ids: vec![],
211            object_property_targets: vec![],
212            pp_property_targets: HashMap::new(),
213        };
214        let c = Cluster::try_from(o).unwrap();
215        assert_eq!(c.indices, vec![0, 2, 4]);
216        assert_eq!(c.weights, vec![0.2, 0.5, 1.0]);
217        assert_eq!(c.transform[0], [1.0, 0.0, 0.0, 0.0]);
218        assert_eq!(c.transform_link[3], [0.0, 0.0, 0.0, 1.0]);
219        assert_eq!(c.property("Foo"), Some(&Property::Bool(true)));
220    }
221
222    #[test]
223    fn errors_when_only_one_of_indexes_weights_exists() {
224        let mut attrs = HashMap::new();
225        attrs.insert(ATTR_TRANSFORM.into(), leaf(&[matrix_csv()]));
226        attrs.insert(ATTR_TRANSFORM_LINK.into(), leaf(&[matrix_csv()]));
227        attrs.insert(ATTR_INDEXES.into(), leaf(&["0,1"]));
228        let o = OwnedObject {
229            object_index: 13,
230            name: "Cluster::B".into(),
231            type_name: DEFORMER_TYPE_NAME.into(),
232            class_name: DEFORMER_CLUSTER_CLASS_NAME.into(),
233            properties: HashMap::new(),
234            attributes: attrs,
235            connected_object_ids: vec![],
236            object_property_targets: vec![],
237            pp_property_targets: HashMap::new(),
238        };
239        let err = Cluster::try_from(o).unwrap_err();
240        assert!(matches!(
241            err.reason,
242            FbxTryFromReason::InvalidAttributeFormat { .. }
243        ));
244    }
245
246    #[test]
247    fn resolves_target_model_from_connections() {
248        let cluster = Cluster::try_from(OwnedObject {
249            object_index: 20,
250            name: "Cluster::T".into(),
251            type_name: DEFORMER_TYPE_NAME.into(),
252            class_name: DEFORMER_CLUSTER_CLASS_NAME.into(),
253            properties: HashMap::new(),
254            attributes: HashMap::from([
255                ("Transform".to_string(), leaf(&[matrix_csv()])),
256                ("TransformLink".to_string(), leaf(&[matrix_csv()])),
257            ]),
258            connected_object_ids: vec![],
259            object_property_targets: vec![],
260            pp_property_targets: HashMap::new(),
261        })
262        .unwrap();
263
264        let model = Model::try_from(OwnedObject {
265            object_index: 30,
266            name: "Model::Node".into(),
267            type_name: MODEL_TYPE_NAME.into(),
268            class_name: "Mesh".into(),
269            properties: HashMap::new(),
270            attributes: HashMap::new(),
271            connected_object_ids: vec![20],
272            object_property_targets: vec![],
273            pp_property_targets: HashMap::new(),
274        })
275        .unwrap();
276
277        let mut owned = OwnedDocument::default();
278        owned.models = vec![model];
279        assert_eq!(
280            cluster
281                .get_target_model(&owned)
282                .map(|m| m.inner().object_index),
283            Some(30)
284        );
285    }
286}