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