Skip to main content

fbx_dom/objects/
model.rs

1//! FBX `Model` objects — Assimp [`Model`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXModel.cpp) / [`FBXDocument.h`](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    AttrExtractorExt, FbxObjectTag, FbxTypeMismatch, Material, ModelGeometryRef, NodeAttributeRef,
10    fbx_object_tag,
11};
12
13const ATTR_SHADING: &str = "Shading";
14const ATTR_CULLING: &str = "Culling";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ModelRotationOrder {
18    EulerXYZ = 0,
19    EulerXZY = 1,
20    EulerYZX = 2,
21    EulerYXZ = 3,
22    EulerZXY = 4,
23    EulerZYX = 5,
24    SphericXYZ = 6,
25}
26
27impl ModelRotationOrder {
28    fn from_i32(v: i32) -> Self {
29        match v {
30            1 => Self::EulerXZY,
31            2 => Self::EulerYZX,
32            3 => Self::EulerYXZ,
33            4 => Self::EulerZXY,
34            5 => Self::EulerZYX,
35            6 => Self::SphericXYZ,
36            _ => Self::EulerXYZ,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ModelTransformInheritance {
43    RrSs = 0,
44    RSrs = 1,
45    Rrs = 2,
46}
47
48impl ModelTransformInheritance {
49    fn from_i32(v: i32) -> Self {
50        match v {
51            1 => Self::RSrs,
52            2 => Self::Rrs,
53            _ => Self::RrSs,
54        }
55    }
56}
57
58/// Typed wrapper for a scene graph model / transform node (`Model::*` except unsupported effectors).
59#[derive(Debug, PartialEq)]
60pub struct Model {
61    object: OwnedObject,
62    pub shading: String,
63    pub culling: String,
64}
65
66impl Model {
67    pub fn inner(&self) -> &OwnedObject {
68        &self.object
69    }
70
71    pub fn into_inner(self) -> OwnedObject {
72        self.object
73    }
74
75    pub fn properties(&self) -> &HashMap<String, Property> {
76        &self.object.properties
77    }
78
79    pub fn property(&self, name: &str) -> Option<&Property> {
80        self.object.properties.get(name)
81    }
82
83    pub fn shading(&self) -> &str {
84        &self.shading
85    }
86
87    pub fn culling(&self) -> &str {
88        &self.culling
89    }
90
91    pub fn quaternion_interpolate(&self) -> i32 {
92        match self.property("QuaternionInterpolate") {
93            Some(Property::Int(v)) => *v,
94            _ => 0,
95        }
96    }
97    pub fn rotation_offset(&self) -> [f32; 3] {
98        match self.property("RotationOffset") {
99            Some(Property::Vec3(v)) => *v,
100            _ => [0.0, 0.0, 0.0],
101        }
102    }
103    pub fn rotation_pivot(&self) -> [f32; 3] {
104        match self.property("RotationPivot") {
105            Some(Property::Vec3(v)) => *v,
106            _ => [0.0, 0.0, 0.0],
107        }
108    }
109    pub fn scaling_offset(&self) -> [f32; 3] {
110        match self.property("ScalingOffset") {
111            Some(Property::Vec3(v)) => *v,
112            _ => [0.0, 0.0, 0.0],
113        }
114    }
115    pub fn scaling_pivot(&self) -> [f32; 3] {
116        match self.property("ScalingPivot") {
117            Some(Property::Vec3(v)) => *v,
118            _ => [0.0, 0.0, 0.0],
119        }
120    }
121    pub fn translation_active(&self) -> bool {
122        match self.property("TranslationActive") {
123            Some(Property::Bool(v)) => *v,
124            _ => false,
125        }
126    }
127    pub fn translation_min(&self) -> [f32; 3] {
128        match self.property("TranslationMin") {
129            Some(Property::Vec3(v)) => *v,
130            _ => [0.0, 0.0, 0.0],
131        }
132    }
133    pub fn translation_max(&self) -> [f32; 3] {
134        match self.property("TranslationMax") {
135            Some(Property::Vec3(v)) => *v,
136            _ => [0.0, 0.0, 0.0],
137        }
138    }
139    pub fn translation_min_x(&self) -> bool {
140        match self.property("TranslationMinX") {
141            Some(Property::Bool(v)) => *v,
142            _ => false,
143        }
144    }
145    pub fn translation_max_x(&self) -> bool {
146        match self.property("TranslationMaxX") {
147            Some(Property::Bool(v)) => *v,
148            _ => false,
149        }
150    }
151    pub fn translation_min_y(&self) -> bool {
152        match self.property("TranslationMinY") {
153            Some(Property::Bool(v)) => *v,
154            _ => false,
155        }
156    }
157    pub fn translation_max_y(&self) -> bool {
158        match self.property("TranslationMaxY") {
159            Some(Property::Bool(v)) => *v,
160            _ => false,
161        }
162    }
163    pub fn translation_min_z(&self) -> bool {
164        match self.property("TranslationMinZ") {
165            Some(Property::Bool(v)) => *v,
166            _ => false,
167        }
168    }
169    pub fn translation_max_z(&self) -> bool {
170        match self.property("TranslationMaxZ") {
171            Some(Property::Bool(v)) => *v,
172            _ => false,
173        }
174    }
175    pub fn rotation_order(&self) -> ModelRotationOrder {
176        match self.property("RotationOrder") {
177            Some(Property::Int(v)) => ModelRotationOrder::from_i32(*v),
178            _ => ModelRotationOrder::EulerXYZ,
179        }
180    }
181    pub fn rotation_space_for_limit_only(&self) -> bool {
182        match self.property("RotationSpaceForLimitOnly") {
183            Some(Property::Bool(v)) => *v,
184            _ => false,
185        }
186    }
187    pub fn rotation_stiffness_x(&self) -> f32 {
188        match self.property("RotationStiffnessX") {
189            Some(Property::Float(v)) => *v,
190            _ => 0.0,
191        }
192    }
193    pub fn rotation_stiffness_y(&self) -> f32 {
194        match self.property("RotationStiffnessY") {
195            Some(Property::Float(v)) => *v,
196            _ => 0.0,
197        }
198    }
199    pub fn rotation_stiffness_z(&self) -> f32 {
200        match self.property("RotationStiffnessZ") {
201            Some(Property::Float(v)) => *v,
202            _ => 0.0,
203        }
204    }
205    pub fn axis_len(&self) -> f32 {
206        match self.property("AxisLen") {
207            Some(Property::Float(v)) => *v,
208            _ => 0.0,
209        }
210    }
211    pub fn pre_rotation(&self) -> [f32; 3] {
212        match self.property("PreRotation") {
213            Some(Property::Vec3(v)) => *v,
214            _ => [0.0, 0.0, 0.0],
215        }
216    }
217    pub fn post_rotation(&self) -> [f32; 3] {
218        match self.property("PostRotation") {
219            Some(Property::Vec3(v)) => *v,
220            _ => [0.0, 0.0, 0.0],
221        }
222    }
223    pub fn rotation_active(&self) -> bool {
224        match self.property("RotationActive") {
225            Some(Property::Bool(v)) => *v,
226            _ => false,
227        }
228    }
229    pub fn rotation_min(&self) -> [f32; 3] {
230        match self.property("RotationMin") {
231            Some(Property::Vec3(v)) => *v,
232            _ => [0.0, 0.0, 0.0],
233        }
234    }
235    pub fn rotation_max(&self) -> [f32; 3] {
236        match self.property("RotationMax") {
237            Some(Property::Vec3(v)) => *v,
238            _ => [0.0, 0.0, 0.0],
239        }
240    }
241    pub fn rotation_min_x(&self) -> bool {
242        match self.property("RotationMinX") {
243            Some(Property::Bool(v)) => *v,
244            _ => false,
245        }
246    }
247    pub fn rotation_max_x(&self) -> bool {
248        match self.property("RotationMaxX") {
249            Some(Property::Bool(v)) => *v,
250            _ => false,
251        }
252    }
253    pub fn rotation_min_y(&self) -> bool {
254        match self.property("RotationMinY") {
255            Some(Property::Bool(v)) => *v,
256            _ => false,
257        }
258    }
259    pub fn rotation_max_y(&self) -> bool {
260        match self.property("RotationMaxY") {
261            Some(Property::Bool(v)) => *v,
262            _ => false,
263        }
264    }
265    pub fn rotation_min_z(&self) -> bool {
266        match self.property("RotationMinZ") {
267            Some(Property::Bool(v)) => *v,
268            _ => false,
269        }
270    }
271    pub fn rotation_max_z(&self) -> bool {
272        match self.property("RotationMaxZ") {
273            Some(Property::Bool(v)) => *v,
274            _ => false,
275        }
276    }
277    pub fn inherit_type(&self) -> ModelTransformInheritance {
278        match self.property("InheritType") {
279            Some(Property::Int(v)) => ModelTransformInheritance::from_i32(*v),
280            _ => ModelTransformInheritance::RrSs,
281        }
282    }
283    pub fn scaling_active(&self) -> bool {
284        match self.property("ScalingActive") {
285            Some(Property::Bool(v)) => *v,
286            _ => false,
287        }
288    }
289    pub fn scaling_min(&self) -> [f32; 3] {
290        match self.property("ScalingMin") {
291            Some(Property::Vec3(v)) => *v,
292            _ => [0.0, 0.0, 0.0],
293        }
294    }
295    pub fn scaling_max(&self) -> [f32; 3] {
296        match self.property("ScalingMax") {
297            Some(Property::Vec3(v)) => *v,
298            _ => [1.0, 1.0, 1.0],
299        }
300    }
301    pub fn scaling_min_x(&self) -> bool {
302        match self.property("ScalingMinX") {
303            Some(Property::Bool(v)) => *v,
304            _ => false,
305        }
306    }
307    pub fn scaling_max_x(&self) -> bool {
308        match self.property("ScalingMaxX") {
309            Some(Property::Bool(v)) => *v,
310            _ => false,
311        }
312    }
313    pub fn scaling_min_y(&self) -> bool {
314        match self.property("ScalingMinY") {
315            Some(Property::Bool(v)) => *v,
316            _ => false,
317        }
318    }
319    pub fn scaling_max_y(&self) -> bool {
320        match self.property("ScalingMaxY") {
321            Some(Property::Bool(v)) => *v,
322            _ => false,
323        }
324    }
325    pub fn scaling_min_z(&self) -> bool {
326        match self.property("ScalingMinZ") {
327            Some(Property::Bool(v)) => *v,
328            _ => false,
329        }
330    }
331    pub fn scaling_max_z(&self) -> bool {
332        match self.property("ScalingMaxZ") {
333            Some(Property::Bool(v)) => *v,
334            _ => false,
335        }
336    }
337    pub fn geometric_translation(&self) -> [f32; 3] {
338        match self.property("GeometricTranslation") {
339            Some(Property::Vec3(v)) => *v,
340            _ => [0.0, 0.0, 0.0],
341        }
342    }
343    pub fn geometric_rotation(&self) -> [f32; 3] {
344        match self.property("GeometricRotation") {
345            Some(Property::Vec3(v)) => *v,
346            _ => [0.0, 0.0, 0.0],
347        }
348    }
349    pub fn geometric_scaling(&self) -> [f32; 3] {
350        match self.property("GeometricScaling") {
351            Some(Property::Vec3(v)) => *v,
352            _ => [1.0, 1.0, 1.0],
353        }
354    }
355    pub fn min_damp_range_x(&self) -> f32 {
356        match self.property("MinDampRangeX") {
357            Some(Property::Float(v)) => *v,
358            _ => 0.0,
359        }
360    }
361    pub fn min_damp_range_y(&self) -> f32 {
362        match self.property("MinDampRangeY") {
363            Some(Property::Float(v)) => *v,
364            _ => 0.0,
365        }
366    }
367    pub fn min_damp_range_z(&self) -> f32 {
368        match self.property("MinDampRangeZ") {
369            Some(Property::Float(v)) => *v,
370            _ => 0.0,
371        }
372    }
373    pub fn max_damp_range_x(&self) -> f32 {
374        match self.property("MaxDampRangeX") {
375            Some(Property::Float(v)) => *v,
376            _ => 0.0,
377        }
378    }
379    pub fn max_damp_range_y(&self) -> f32 {
380        match self.property("MaxDampRangeY") {
381            Some(Property::Float(v)) => *v,
382            _ => 0.0,
383        }
384    }
385    pub fn max_damp_range_z(&self) -> f32 {
386        match self.property("MaxDampRangeZ") {
387            Some(Property::Float(v)) => *v,
388            _ => 0.0,
389        }
390    }
391    pub fn min_damp_strength_x(&self) -> f32 {
392        match self.property("MinDampStrengthX") {
393            Some(Property::Float(v)) => *v,
394            _ => 0.0,
395        }
396    }
397    pub fn min_damp_strength_y(&self) -> f32 {
398        match self.property("MinDampStrengthY") {
399            Some(Property::Float(v)) => *v,
400            _ => 0.0,
401        }
402    }
403    pub fn min_damp_strength_z(&self) -> f32 {
404        match self.property("MinDampStrengthZ") {
405            Some(Property::Float(v)) => *v,
406            _ => 0.0,
407        }
408    }
409    pub fn max_damp_strength_x(&self) -> f32 {
410        match self.property("MaxDampStrengthX") {
411            Some(Property::Float(v)) => *v,
412            _ => 0.0,
413        }
414    }
415    pub fn max_damp_strength_y(&self) -> f32 {
416        match self.property("MaxDampStrengthY") {
417            Some(Property::Float(v)) => *v,
418            _ => 0.0,
419        }
420    }
421    pub fn max_damp_strength_z(&self) -> f32 {
422        match self.property("MaxDampStrengthZ") {
423            Some(Property::Float(v)) => *v,
424            _ => 0.0,
425        }
426    }
427    pub fn preferred_angle_x(&self) -> f32 {
428        match self.property("PreferredAngleX") {
429            Some(Property::Float(v)) => *v,
430            _ => 0.0,
431        }
432    }
433    pub fn preferred_angle_y(&self) -> f32 {
434        match self.property("PreferredAngleY") {
435            Some(Property::Float(v)) => *v,
436            _ => 0.0,
437        }
438    }
439    pub fn preferred_angle_z(&self) -> f32 {
440        match self.property("PreferredAngleZ") {
441            Some(Property::Float(v)) => *v,
442            _ => 0.0,
443        }
444    }
445    pub fn show(&self) -> bool {
446        match self.property("Show") {
447            Some(Property::Bool(v)) => *v,
448            _ => true,
449        }
450    }
451    pub fn lod_box(&self) -> bool {
452        match self.property("LODBox") {
453            Some(Property::Bool(v)) => *v,
454            _ => false,
455        }
456    }
457    pub fn freeze(&self) -> bool {
458        match self.property("Freeze") {
459            Some(Property::Bool(v)) => *v,
460            _ => false,
461        }
462    }
463
464    /// Incoming object–object (`OO`) links from [`Material`] sources whose `connected_object_ids`
465    /// contain this model’s id — Assimp [`Model::ResolveLinks`](https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXModel.cpp) material branch (`OP` links are excluded).
466    ///
467    /// Use `Material::get_textures` / `Material::get_layered_textures` on each entry for texture data.
468    pub fn connected_materials<'a>(&'a self, document: &'a OwnedDocument) -> Vec<&'a Material> {
469        let id = self.object.object_index;
470        document
471            .materials
472            .iter()
473            .filter(|m| m.inner().connected_object_ids.contains(&id))
474            .collect()
475    }
476
477    /// Incoming `OO` links from `Geometry` sources (mesh, line, shape, or unknown geometry class).
478    pub fn connected_geometries<'a>(
479        &'a self,
480        document: &'a OwnedDocument,
481    ) -> Vec<ModelGeometryRef<'a>> {
482        let id = self.object.object_index;
483        let mut out = Vec::new();
484        for g in &document.mesh_geometries {
485            if g.inner().connected_object_ids.contains(&id) {
486                out.push(ModelGeometryRef::Mesh(g));
487            }
488        }
489        for g in &document.line_geometries {
490            if g.inner().connected_object_ids.contains(&id) {
491                out.push(ModelGeometryRef::Line(g));
492            }
493        }
494        for g in &document.shape_geometries {
495            if g.inner().connected_object_ids.contains(&id) {
496                out.push(ModelGeometryRef::Shape(g));
497            }
498        }
499        for o in &document.unknown_geometries {
500            if o.connected_object_ids.contains(&id) {
501                out.push(ModelGeometryRef::Unknown(o));
502            }
503        }
504        out
505    }
506
507    /// Incoming `OO` links from `NodeAttribute` sources, same discriminant shape as
508    /// `AnimationCurveNode::get_target_node_attribute`.
509    pub fn connected_node_attributes<'a>(
510        &'a self,
511        document: &'a OwnedDocument,
512    ) -> Vec<NodeAttributeRef<'a>> {
513        let id = self.object.object_index;
514        let mut out = Vec::new();
515        for v in &document.cameras {
516            if v.inner().connected_object_ids.contains(&id) {
517                out.push(NodeAttributeRef::Camera(v));
518            }
519        }
520        for v in &document.camera_switchers {
521            if v.inner().connected_object_ids.contains(&id) {
522                out.push(NodeAttributeRef::CameraSwitcher(v));
523            }
524        }
525        for v in &document.lights {
526            if v.inner().connected_object_ids.contains(&id) {
527                out.push(NodeAttributeRef::Light(v));
528            }
529        }
530        for v in &document.null_nodes {
531            if v.inner().connected_object_ids.contains(&id) {
532                out.push(NodeAttributeRef::NullNode(v));
533            }
534        }
535        for v in &document.limb_nodes {
536            if v.inner().connected_object_ids.contains(&id) {
537                out.push(NodeAttributeRef::LimbNode(v));
538            }
539        }
540        for o in &document.unknown_node_attributes {
541            if o.connected_object_ids.contains(&id) {
542                out.push(NodeAttributeRef::Unknown(o));
543            }
544        }
545        out
546    }
547}
548
549impl TryFrom<OwnedObject> for Model {
550    type Error = FbxTypeMismatch;
551
552    fn try_from(o: OwnedObject) -> Result<Self, Self::Error> {
553        if fbx_object_tag(&o) != FbxObjectTag::Model {
554            return Err(FbxTypeMismatch::wrong_object_kind(o, "Model".to_string()));
555        }
556
557        let shading = o
558            .attributes
559            .optional_token_case_insensitive(ATTR_SHADING)
560            .ok()
561            .flatten()
562            .map(ToString::to_string)
563            .unwrap_or_else(|| "Y".to_string());
564        let culling = o
565            .attributes
566            .optional_token_case_insensitive(ATTR_CULLING)
567            .ok()
568            .flatten()
569            .map(ToString::to_string)
570            .unwrap_or_default();
571
572        Ok(Model {
573            object: o,
574            shading,
575            culling,
576        })
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use std::collections::HashMap;
583    use std::convert::TryFrom;
584
585    use fbxscii::{ElementAttribute, LeafAttribute};
586
587    use crate::objects::{
588        GEOMETRY_TYPE_NAME, MATERIAL_CLASS_NAME, MATERIAL_TYPE_NAME, MODEL_TYPE_NAME, Model,
589        ModelGeometryRef, ModelRotationOrder, ModelTransformInheritance,
590        NODE_ATTRIBUTE_LIGHT_CLASS_NAME, NODE_ATTRIBUTE_TYPE_NAME, NodeAttributeRef,
591        TEXTURE_CLASS_NAME, TEXTURE_TYPE_NAME,
592    };
593    use crate::{ObjectPropertyConnection, OwnedDocument, OwnedObject, Property};
594
595    fn leaf(tokens: &[&str]) -> ElementAttribute {
596        ElementAttribute::Leaf(Box::new(LeafAttribute {
597            key: String::new(),
598            tokens: tokens.iter().map(|s| (*s).to_string()).collect(),
599        }))
600    }
601
602    #[test]
603    fn extracts_shading_and_culling_and_properties() {
604        let mut attrs = HashMap::new();
605        attrs.insert("Shading".into(), leaf(&["Phong"]));
606        attrs.insert("Culling".into(), leaf(&["CullingOff"]));
607        let mut props = HashMap::new();
608        props.insert("Show".into(), Property::Bool(false));
609        props.insert("RotationOrder".into(), Property::Int(5));
610        props.insert("InheritType".into(), Property::Int(2));
611        let o = OwnedObject {
612            object_index: 100,
613            name: "Model::A".into(),
614            type_name: MODEL_TYPE_NAME.into(),
615            class_name: "Mesh".into(),
616            properties: props,
617            attributes: attrs,
618            connected_object_ids: vec![],
619            object_property_targets: vec![],
620            pp_property_targets: HashMap::new(),
621        };
622        let m = Model::try_from(o).unwrap();
623        assert_eq!(m.shading(), "Phong");
624        assert_eq!(m.culling(), "CullingOff");
625        assert_eq!(m.show(), false);
626        assert_eq!(m.rotation_order(), ModelRotationOrder::EulerZYX);
627        assert_eq!(m.inherit_type(), ModelTransformInheritance::Rrs);
628    }
629
630    #[test]
631    fn defaults_match_assimp_header() {
632        let o = OwnedObject {
633            object_index: 101,
634            name: "Model::B".into(),
635            type_name: MODEL_TYPE_NAME.into(),
636            class_name: "Mesh".into(),
637            properties: HashMap::new(),
638            attributes: HashMap::new(),
639            connected_object_ids: vec![],
640            object_property_targets: vec![],
641            pp_property_targets: HashMap::new(),
642        };
643        let m = Model::try_from(o).unwrap();
644        assert_eq!(m.shading(), "Y");
645        assert_eq!(m.culling(), "");
646        assert_eq!(m.scaling_max(), [1.0, 1.0, 1.0]);
647        assert_eq!(m.geometric_scaling(), [1.0, 1.0, 1.0]);
648        assert_eq!(m.show(), true);
649        assert_eq!(m.lod_box(), false);
650        assert_eq!(m.freeze(), false);
651        assert_eq!(m.rotation_order(), ModelRotationOrder::EulerXYZ);
652        assert_eq!(m.inherit_type(), ModelTransformInheritance::RrSs);
653    }
654
655    #[test]
656    fn model_typed_property_getters_cover_most_fields() {
657        let props: HashMap<String, Property> = HashMap::from([
658            ("QuaternionInterpolate".into(), Property::Int(1)),
659            ("RotationOffset".into(), Property::Vec3([0.1, 0.2, 0.3])),
660            ("RotationPivot".into(), Property::Vec3([0.4, 0.5, 0.6])),
661            ("ScalingOffset".into(), Property::Vec3([0.7, 0.8, 0.9])),
662            ("ScalingPivot".into(), Property::Vec3([1.0, 1.1, 1.2])),
663            ("TranslationActive".into(), Property::Bool(true)),
664            ("TranslationMin".into(), Property::Vec3([1.0, 2.0, 3.0])),
665            ("TranslationMax".into(), Property::Vec3([4.0, 5.0, 6.0])),
666            ("TranslationMinX".into(), Property::Bool(true)),
667            ("TranslationMaxX".into(), Property::Bool(false)),
668            ("TranslationMinY".into(), Property::Bool(false)),
669            ("TranslationMaxY".into(), Property::Bool(true)),
670            ("TranslationMinZ".into(), Property::Bool(true)),
671            ("TranslationMaxZ".into(), Property::Bool(false)),
672            ("RotationOrder".into(), Property::Int(6)),
673            ("RotationSpaceForLimitOnly".into(), Property::Bool(true)),
674            ("RotationStiffnessX".into(), Property::Float(0.11)),
675            ("RotationStiffnessY".into(), Property::Float(0.22)),
676            ("RotationStiffnessZ".into(), Property::Float(0.33)),
677            ("AxisLen".into(), Property::Float(9.5)),
678            ("PreRotation".into(), Property::Vec3([10.0, 20.0, 30.0])),
679            ("PostRotation".into(), Property::Vec3([40.0, 50.0, 60.0])),
680            ("RotationActive".into(), Property::Bool(true)),
681            ("RotationMin".into(), Property::Vec3([-1.0, -2.0, -3.0])),
682            ("RotationMax".into(), Property::Vec3([1.0, 2.0, 3.0])),
683            ("RotationMinX".into(), Property::Bool(true)),
684            ("RotationMaxX".into(), Property::Bool(false)),
685            ("RotationMinY".into(), Property::Bool(false)),
686            ("RotationMaxY".into(), Property::Bool(true)),
687            ("RotationMinZ".into(), Property::Bool(true)),
688            ("RotationMaxZ".into(), Property::Bool(false)),
689            ("InheritType".into(), Property::Int(1)),
690            ("ScalingActive".into(), Property::Bool(true)),
691            ("ScalingMin".into(), Property::Vec3([0.5, 0.6, 0.7])),
692            ("ScalingMax".into(), Property::Vec3([2.0, 2.5, 3.0])),
693            ("ScalingMinX".into(), Property::Bool(true)),
694            ("ScalingMaxX".into(), Property::Bool(false)),
695            ("ScalingMinY".into(), Property::Bool(false)),
696            ("ScalingMaxY".into(), Property::Bool(true)),
697            ("ScalingMinZ".into(), Property::Bool(true)),
698            ("ScalingMaxZ".into(), Property::Bool(false)),
699            (
700                "GeometricTranslation".into(),
701                Property::Vec3([7.0, 8.0, 9.0]),
702            ),
703            (
704                "GeometricRotation".into(),
705                Property::Vec3([0.01, 0.02, 0.03]),
706            ),
707            ("GeometricScaling".into(), Property::Vec3([1.5, 2.5, 3.5])),
708            ("MinDampRangeX".into(), Property::Float(0.1)),
709            ("MinDampRangeY".into(), Property::Float(0.2)),
710            ("MinDampRangeZ".into(), Property::Float(0.3)),
711            ("MaxDampRangeX".into(), Property::Float(0.4)),
712            ("MaxDampRangeY".into(), Property::Float(0.5)),
713            ("MaxDampRangeZ".into(), Property::Float(0.6)),
714            ("MinDampStrengthX".into(), Property::Float(0.7)),
715            ("MinDampStrengthY".into(), Property::Float(0.8)),
716            ("MinDampStrengthZ".into(), Property::Float(0.9)),
717            ("MaxDampStrengthX".into(), Property::Float(1.1)),
718            ("MaxDampStrengthY".into(), Property::Float(1.2)),
719            ("MaxDampStrengthZ".into(), Property::Float(1.3)),
720            ("PreferredAngleX".into(), Property::Float(2.1)),
721            ("PreferredAngleY".into(), Property::Float(2.2)),
722            ("PreferredAngleZ".into(), Property::Float(2.3)),
723            ("Show".into(), Property::Bool(false)),
724            ("LODBox".into(), Property::Bool(true)),
725            ("Freeze".into(), Property::Bool(true)),
726        ]);
727
728        let o = OwnedObject {
729            object_index: 600,
730            name: "Model::Props".into(),
731            type_name: MODEL_TYPE_NAME.into(),
732            class_name: "Mesh".into(),
733            properties: props,
734            attributes: HashMap::new(),
735            connected_object_ids: vec![],
736            object_property_targets: vec![],
737            pp_property_targets: HashMap::new(),
738        };
739
740        let m = Model::try_from(o).unwrap();
741        assert_eq!(m.inner().object_index, 600);
742        assert!(m.property("QuaternionInterpolate").is_some());
743        assert!(m.property("missing").is_none());
744        assert_eq!(m.properties().len(), 62);
745
746        assert_eq!(m.quaternion_interpolate(), 1);
747        assert_eq!(m.rotation_offset(), [0.1, 0.2, 0.3]);
748        assert_eq!(m.rotation_pivot(), [0.4, 0.5, 0.6]);
749        assert_eq!(m.scaling_offset(), [0.7, 0.8, 0.9]);
750        assert_eq!(m.scaling_pivot(), [1.0, 1.1, 1.2]);
751        assert_eq!(m.translation_active(), true);
752        assert_eq!(m.translation_min(), [1.0, 2.0, 3.0]);
753        assert_eq!(m.translation_max(), [4.0, 5.0, 6.0]);
754        assert_eq!(m.translation_min_x(), true);
755        assert_eq!(m.translation_max_x(), false);
756        assert_eq!(m.translation_min_y(), false);
757        assert_eq!(m.translation_max_y(), true);
758        assert_eq!(m.translation_min_z(), true);
759        assert_eq!(m.translation_max_z(), false);
760        assert_eq!(m.rotation_order(), ModelRotationOrder::SphericXYZ);
761        assert_eq!(m.rotation_space_for_limit_only(), true);
762        assert_eq!(m.rotation_stiffness_x(), 0.11);
763        assert_eq!(m.rotation_stiffness_y(), 0.22);
764        assert_eq!(m.rotation_stiffness_z(), 0.33);
765        assert_eq!(m.axis_len(), 9.5);
766        assert_eq!(m.pre_rotation(), [10.0, 20.0, 30.0]);
767        assert_eq!(m.post_rotation(), [40.0, 50.0, 60.0]);
768        assert_eq!(m.rotation_active(), true);
769        assert_eq!(m.rotation_min(), [-1.0, -2.0, -3.0]);
770        assert_eq!(m.rotation_max(), [1.0, 2.0, 3.0]);
771        assert_eq!(m.rotation_min_x(), true);
772        assert_eq!(m.rotation_max_x(), false);
773        assert_eq!(m.rotation_min_y(), false);
774        assert_eq!(m.rotation_max_y(), true);
775        assert_eq!(m.rotation_min_z(), true);
776        assert_eq!(m.rotation_max_z(), false);
777        assert_eq!(m.inherit_type(), ModelTransformInheritance::RSrs);
778        assert_eq!(m.scaling_active(), true);
779        assert_eq!(m.scaling_min(), [0.5, 0.6, 0.7]);
780        assert_eq!(m.scaling_max(), [2.0, 2.5, 3.0]);
781        assert_eq!(m.scaling_min_x(), true);
782        assert_eq!(m.scaling_max_x(), false);
783        assert_eq!(m.scaling_min_y(), false);
784        assert_eq!(m.scaling_max_y(), true);
785        assert_eq!(m.scaling_min_z(), true);
786        assert_eq!(m.scaling_max_z(), false);
787        assert_eq!(m.geometric_translation(), [7.0, 8.0, 9.0]);
788        assert_eq!(m.geometric_rotation(), [0.01, 0.02, 0.03]);
789        assert_eq!(m.geometric_scaling(), [1.5, 2.5, 3.5]);
790        assert_eq!(m.min_damp_range_x(), 0.1);
791        assert_eq!(m.min_damp_range_y(), 0.2);
792        assert_eq!(m.min_damp_range_z(), 0.3);
793        assert_eq!(m.max_damp_range_x(), 0.4);
794        assert_eq!(m.max_damp_range_y(), 0.5);
795        assert_eq!(m.max_damp_range_z(), 0.6);
796        assert_eq!(m.min_damp_strength_x(), 0.7);
797        assert_eq!(m.min_damp_strength_y(), 0.8);
798        assert_eq!(m.min_damp_strength_z(), 0.9);
799        assert_eq!(m.max_damp_strength_x(), 1.1);
800        assert_eq!(m.max_damp_strength_y(), 1.2);
801        assert_eq!(m.max_damp_strength_z(), 1.3);
802        assert_eq!(m.preferred_angle_x(), 2.1);
803        assert_eq!(m.preferred_angle_y(), 2.2);
804        assert_eq!(m.preferred_angle_z(), 2.3);
805        assert_eq!(m.show(), false);
806        assert_eq!(m.lod_box(), true);
807        assert_eq!(m.freeze(), true);
808
809        let inner = m.into_inner();
810        assert_eq!(inner.object_index, 600);
811        assert_eq!(inner.name, "Model::Props");
812    }
813
814    #[test]
815    fn resolves_incoming_material_geometry_and_node_attribute_oo_links() {
816        let model = Model::try_from(OwnedObject {
817            object_index: 500,
818            name: "Model::Root".into(),
819            type_name: MODEL_TYPE_NAME.into(),
820            class_name: "Mesh".into(),
821            properties: HashMap::new(),
822            attributes: HashMap::new(),
823            connected_object_ids: vec![],
824            object_property_targets: vec![],
825            pp_property_targets: HashMap::new(),
826        })
827        .unwrap();
828
829        let material = crate::objects::Material::try_from(OwnedObject {
830            object_index: 501,
831            name: "Material::M".into(),
832            type_name: MATERIAL_TYPE_NAME.into(),
833            class_name: MATERIAL_CLASS_NAME.into(),
834            properties: HashMap::new(),
835            attributes: HashMap::from([
836                (
837                    "ShadingModel".to_string(),
838                    ElementAttribute::Leaf(Box::new(LeafAttribute {
839                        key: "ShadingModel".into(),
840                        tokens: vec!["Phong".into()],
841                    })),
842                ),
843                (
844                    "MultiLayer".to_string(),
845                    ElementAttribute::Leaf(Box::new(LeafAttribute {
846                        key: "MultiLayer".into(),
847                        tokens: vec!["0".into()],
848                    })),
849                ),
850            ]),
851            connected_object_ids: vec![500],
852            object_property_targets: vec![],
853            pp_property_targets: HashMap::new(),
854        })
855        .unwrap();
856
857        let unknown_geo = OwnedObject {
858            object_index: 502,
859            name: "Geometry::Custom".into(),
860            type_name: GEOMETRY_TYPE_NAME.into(),
861            class_name: "CustomMesh".into(),
862            properties: HashMap::new(),
863            attributes: HashMap::new(),
864            connected_object_ids: vec![500],
865            object_property_targets: vec![],
866            pp_property_targets: HashMap::new(),
867        };
868
869        let light = crate::objects::Light::try_from(OwnedObject {
870            object_index: 503,
871            name: "NodeAttribute::L".into(),
872            type_name: NODE_ATTRIBUTE_TYPE_NAME.into(),
873            class_name: NODE_ATTRIBUTE_LIGHT_CLASS_NAME.into(),
874            properties: HashMap::new(),
875            attributes: HashMap::new(),
876            connected_object_ids: vec![500],
877            object_property_targets: vec![],
878            pp_property_targets: HashMap::new(),
879        })
880        .unwrap();
881
882        let texture = crate::objects::Texture::try_from(OwnedObject {
883            object_index: 504,
884            name: "Texture::D".into(),
885            type_name: TEXTURE_TYPE_NAME.into(),
886            class_name: TEXTURE_CLASS_NAME.into(),
887            properties: HashMap::new(),
888            attributes: HashMap::new(),
889            connected_object_ids: vec![],
890            object_property_targets: vec![ObjectPropertyConnection {
891                dest: 501,
892                property: "DiffuseColor".into(),
893            }],
894            pp_property_targets: HashMap::new(),
895        })
896        .unwrap();
897
898        let mut doc = OwnedDocument::default();
899        doc.models = vec![model];
900        doc.materials = vec![material];
901        doc.unknown_geometries = vec![unknown_geo];
902        doc.lights = vec![light];
903        doc.textures = vec![texture];
904
905        let model = &doc.models[0];
906        let mats = model.connected_materials(&doc);
907        assert_eq!(mats.len(), 1);
908        assert_eq!(mats[0].inner().object_index, 501);
909
910        let geos = model.connected_geometries(&doc);
911        assert_eq!(geos.len(), 1);
912        assert!(matches!(geos[0], ModelGeometryRef::Unknown(_)));
913        assert_eq!(geos[0].inner().object_index, 502);
914
915        let attrs = model.connected_node_attributes(&doc);
916        assert_eq!(attrs.len(), 1);
917        assert!(matches!(attrs[0], NodeAttributeRef::Light(_)));
918        assert_eq!(attrs[0].inner().object_index, 503);
919
920        let tex = mats[0].get_textures(&doc);
921        assert_eq!(
922            tex.get("DiffuseColor").map(|t| t.inner().object_index),
923            Some(504)
924        );
925    }
926}