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