1use std::collections::BTreeMap;
19
20use crate::prelude_::*;
21
22#[derive(Debug, Clone, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub enum DynamicObject {
29 Cube {
31 size: [f64; 3],
32 },
33 Sphere {
34 radius: f64,
35 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
36 segments: usize,
37 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
38 stacks: usize,
39 },
40 Cylinder {
41 radius: f64,
42 height: f64,
43 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
44 segments: usize,
45 },
46 Cone {
47 radius: f64,
48 height: f64,
49 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
50 segments: usize,
51 },
52
53 Circle {
55 radius: f64,
56 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
57 segments: usize,
58 },
59 Square {
60 size: [f64; 2],
61 },
62 Polygon {
63 points: Vec<[f64; 2]>,
64 },
65 Sketch {
68 data: crate::sketch::SketchData,
69 },
70
71 Union {
74 children: Vec<DynamicObject>,
75 },
76 Difference {
78 children: Vec<DynamicObject>,
79 },
80 Intersection {
82 children: Vec<DynamicObject>,
83 },
84
85 Translate {
87 offset: [f64; 3],
88 child: Box<DynamicObject>,
89 },
90 Rotate {
92 angles: [f64; 3],
93 child: Box<DynamicObject>,
94 },
95 Scale {
96 factors: [f64; 3],
97 child: Box<DynamicObject>,
98 },
99 Mirror {
100 axis: [f64; 3],
101 child: Box<DynamicObject>,
102 },
103 Color {
105 color: [f64; 4],
106 child: Box<DynamicObject>,
107 },
108 Material {
110 material: crate::material::MaterialData,
111 child: Box<DynamicObject>,
112 },
113
114 LinearExtrude {
116 height: f64,
117 #[cfg_attr(feature = "serde", serde(default))]
118 twist: f64,
119 #[cfg_attr(feature = "serde", serde(default = "default_scale"))]
120 scale: f64,
121 #[cfg_attr(feature = "serde", serde(default = "default_slices"))]
122 slices: usize,
123 child: Box<DynamicObject>,
124 },
125 Revolve {
128 #[cfg_attr(feature = "serde", serde(default = "default_angle"))]
129 angle_degrees: f64,
130 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
131 segments: usize,
132 child: Box<DynamicObject>,
133 },
134 Offset {
136 delta: f64,
137 #[cfg_attr(feature = "serde", serde(default))]
139 round: bool,
140 #[cfg_attr(feature = "serde", serde(default))]
142 chamfer: bool,
143 #[cfg_attr(feature = "serde", serde(default = "default_segments"))]
145 segments: usize,
146 child: Box<DynamicObject>,
147 },
148 EdgeFeature {
153 data: crate::feature::EdgeFeatureData,
154 child: Box<DynamicObject>,
155 },
156
157 Group {
160 children: Vec<DynamicObject>,
161 },
162 Empty,
164}
165
166#[cfg(feature = "serde")]
167fn default_segments() -> usize {
168 32
169}
170
171#[cfg(feature = "serde")]
172fn default_scale() -> f64 {
173 1.0
174}
175
176#[cfg(feature = "serde")]
177fn default_slices() -> usize {
178 1
179}
180
181#[cfg(feature = "serde")]
182fn default_angle() -> f64 {
183 360.0
184}
185
186pub struct ExtractedTransforms<'a> {
194 pub transform: DAffine3,
196 pub color: Option<[f64; 4]>,
198 pub material: Option<crate::material::MaterialData>,
200 pub inner: &'a DynamicObject,
202}
203
204impl DynamicObject {
205 pub fn extract_top_transforms(&self) -> ExtractedTransforms<'_> {
214 let mut transform = DAffine3::IDENTITY;
215 let mut color: Option<[f64; 4]> = None;
216 let mut material: Option<crate::material::MaterialData> = None;
217 let mut current = self;
218
219 loop {
220 match current {
221 DynamicObject::Translate { offset, child } => {
222 transform *= DAffine3::from_translation(DVec3::from_array(*offset));
223 current = child;
224 }
225 DynamicObject::Rotate { angles, child } => {
226 let q = DQuat::from_euler(
229 EulerRot::ZYX,
230 angles[2].to_radians(),
231 angles[1].to_radians(),
232 angles[0].to_radians(),
233 );
234 transform *= DAffine3::from_quat(q);
235 current = child;
236 }
237 DynamicObject::Scale { factors, child } => {
238 transform *= DAffine3::from_scale(DVec3::from_array(*factors));
239 current = child;
240 }
241 DynamicObject::Color { color: c, child } => {
242 if color.is_none() {
243 color = Some(*c);
244 }
245 current = child;
246 }
247 DynamicObject::Material { material: m, child } => {
248 if material.is_none() {
249 material = Some(m.clone());
250 }
251 current = child;
252 }
253 _ => break,
254 }
255 }
256
257 ExtractedTransforms {
258 transform,
259 color,
260 material,
261 inner: current,
262 }
263 }
264}
265
266#[derive(Debug, Clone, PartialEq)]
276#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
277pub struct SceneFile {
278 pub trees: BTreeMap<String, DynamicObject>,
279}
280
281impl SceneFile {
282 pub fn from_single(obj: DynamicObject) -> Self {
284 Self {
285 trees: BTreeMap::from([("Scene".into(), obj)]),
286 }
287 }
288}
289
290impl Object for DynamicObject {
291 fn name(&self) -> Option<&'static str> {
292 Some(match self {
293 Self::Cube { .. } => "Cube",
294 Self::Sphere { .. } => "Sphere",
295 Self::Cylinder { .. } => "Cylinder",
296 Self::Cone { .. } => "Cone",
297 Self::Circle { .. } => "Circle",
298 Self::Square { .. } => "Square",
299 Self::Polygon { .. } => "Polygon",
300 Self::Sketch { .. } => "Sketch",
301 Self::Union { .. } => "Union",
302 Self::Difference { .. } => "Difference",
303 Self::Intersection { .. } => "Intersection",
304 Self::Translate { .. } => "Translate",
305 Self::Rotate { .. } => "Rotate",
306 Self::Scale { .. } => "Scale",
307 Self::Mirror { .. } => "Mirror",
308 Self::Color { .. } => "Color",
309 Self::Material { .. } => "Material",
310 Self::LinearExtrude { .. } => "LinearExtrude",
311 Self::Revolve { .. } => "Revolve",
312 Self::Offset { .. } => "Offset",
313 Self::EdgeFeature { data, .. } => data.kind.display_name(),
314 Self::Group { .. } => "Group",
315 Self::Empty => "Empty",
316 })
317 }
318}
319
320pub trait ToDynamic {
324 fn to_dynamic(&self) -> DynamicObject;
325}
326
327impl ToDynamic for Cube {
328 fn to_dynamic(&self) -> DynamicObject {
329 DynamicObject::Cube {
330 size: [self.x, self.y, self.z],
331 }
332 }
333}
334
335impl ToDynamic for Sphere {
336 fn to_dynamic(&self) -> DynamicObject {
337 DynamicObject::Sphere {
338 radius: self.radius,
339 segments: self.fn_,
340 stacks: self.stacks,
341 }
342 }
343}
344
345impl ToDynamic for Cylinder {
346 fn to_dynamic(&self) -> DynamicObject {
347 DynamicObject::Cylinder {
348 radius: self.radius,
349 height: self.height,
350 segments: self.fn_,
351 }
352 }
353}
354
355impl ToDynamic for crate::Circle {
356 fn to_dynamic(&self) -> DynamicObject {
357 DynamicObject::Circle {
358 radius: self.radius,
359 segments: self.fn_,
360 }
361 }
362}
363
364impl ToDynamic for crate::Square {
365 fn to_dynamic(&self) -> DynamicObject {
366 DynamicObject::Square {
367 size: [self.x, self.y],
368 }
369 }
370}
371
372impl<T: Object + ToDynamic> ToDynamic for Translate<T> {
373 fn to_dynamic(&self) -> DynamicObject {
374 DynamicObject::Translate {
375 offset: self.translation.into(),
376 child: Box::new(self.object.to_dynamic()),
377 }
378 }
379}
380
381impl<T: Object + ToDynamic> ToDynamic for Rotate<T> {
382 fn to_dynamic(&self) -> DynamicObject {
383 DynamicObject::Rotate {
384 angles: self.rotation.into(),
385 child: Box::new(self.object.to_dynamic()),
386 }
387 }
388}
389
390impl<T: Object + ToDynamic> ToDynamic for Scale<T> {
391 fn to_dynamic(&self) -> DynamicObject {
392 DynamicObject::Scale {
393 factors: self.scale.into(),
394 child: Box::new(self.object.to_dynamic()),
395 }
396 }
397}
398
399impl<T: Object + ToDynamic> ToDynamic for Mirror<T> {
400 fn to_dynamic(&self) -> DynamicObject {
401 DynamicObject::Mirror {
402 axis: self.mirror.into(),
403 child: Box::new(self.object.to_dynamic()),
404 }
405 }
406}
407
408impl<T: Object + ToDynamic> ToDynamic for Colored<T> {
409 fn to_dynamic(&self) -> DynamicObject {
410 DynamicObject::Color {
411 color: [
412 self.color.r as f64 / 255.0,
413 self.color.g as f64 / 255.0,
414 self.color.b as f64 / 255.0,
415 self.color.a as f64 / 255.0,
416 ],
417 child: Box::new(self.object.to_dynamic()),
418 }
419 }
420}
421
422impl<A: Object + ToDynamic, B: Object + ToDynamic> ToDynamic for Union<A, B> {
423 fn to_dynamic(&self) -> DynamicObject {
424 let mut children = Vec::new();
425 flatten_union(&self.this.to_dynamic(), &mut children);
426 flatten_union(&self.other.to_dynamic(), &mut children);
427 DynamicObject::Union { children }
428 }
429}
430
431impl<A: Object + ToDynamic, B: Object + ToDynamic> ToDynamic for Difference<A, B> {
432 fn to_dynamic(&self) -> DynamicObject {
433 DynamicObject::Difference {
434 children: vec![self.this.to_dynamic(), self.other.to_dynamic()],
435 }
436 }
437}
438
439impl<A: Object + ToDynamic, B: Object + ToDynamic> ToDynamic for Intersection<A, B> {
440 fn to_dynamic(&self) -> DynamicObject {
441 DynamicObject::Intersection {
442 children: vec![self.this.to_dynamic(), self.other.to_dynamic()],
443 }
444 }
445}
446
447fn flatten_union(node: &DynamicObject, out: &mut Vec<DynamicObject>) {
449 match node {
450 DynamicObject::Union { children } => {
451 for child in children {
452 flatten_union(child, out);
453 }
454 }
455 other => out.push(other.clone()),
456 }
457}
458
459impl<O: Object + ToDynamic> ToDynamic for crate::extrude::LinearExtrude<O> {
460 fn to_dynamic(&self) -> DynamicObject {
461 DynamicObject::LinearExtrude {
462 height: self.height,
463 twist: self.twist,
464 scale: self.scale,
465 slices: self.slices,
466 child: Box::new(self.object.to_dynamic()),
467 }
468 }
469}
470
471impl<O: Object + ToDynamic> ToDynamic for crate::extrude::RotateExtrude<O> {
472 fn to_dynamic(&self) -> DynamicObject {
473 DynamicObject::Revolve {
474 angle_degrees: self.angle_degrees,
475 segments: self.fn_,
476 child: Box::new(self.object.to_dynamic()),
477 }
478 }
479}
480
481impl<A: Object + ToDynamic, B: Object + ToDynamic> ToDynamic for TwoContainer<A, B> {
482 fn to_dynamic(&self) -> DynamicObject {
483 DynamicObject::Group {
484 children: vec![self.a.to_dynamic(), self.b.to_dynamic()],
485 }
486 }
487}
488
489impl ToDynamic for Empty {
490 fn to_dynamic(&self) -> DynamicObject {
491 DynamicObject::Empty
492 }
493}
494
495impl ToDynamic for DynamicObject {
496 fn to_dynamic(&self) -> DynamicObject {
497 self.clone()
498 }
499}
500
501impl<T: ToDynamic> ToDynamic for Box<T> {
502 fn to_dynamic(&self) -> DynamicObject {
503 (**self).to_dynamic()
504 }
505}
506
507impl<T: ToDynamic> ToDynamic for Option<T> {
508 fn to_dynamic(&self) -> DynamicObject {
509 match self {
510 Some(inner) => inner.to_dynamic(),
511 None => DynamicObject::Empty,
512 }
513 }
514}
515
516impl<T: ToDynamic> ToDynamic for Vec<T> {
517 fn to_dynamic(&self) -> DynamicObject {
518 DynamicObject::Group {
519 children: self.iter().map(ToDynamic::to_dynamic).collect(),
520 }
521 }
522}
523
524impl<T: ToDynamic, const N: usize> ToDynamic for [T; N] {
525 fn to_dynamic(&self) -> DynamicObject {
526 DynamicObject::Group {
527 children: self.iter().map(ToDynamic::to_dynamic).collect(),
528 }
529 }
530}
531
532impl ToDynamic for () {
533 fn to_dynamic(&self) -> DynamicObject {
534 DynamicObject::Empty
535 }
536}
537
538macro_rules! impl_to_dynamic_tuple {
539 ($($name:ident),+) => {
540 impl<$($name: ToDynamic),+> ToDynamic for ($($name,)+) {
541 fn to_dynamic(&self) -> DynamicObject {
542 #[allow(non_snake_case)]
543 let ($($name,)+) = self;
544 DynamicObject::Group {
545 children: vec![$($name.to_dynamic()),+],
546 }
547 }
548 }
549 };
550}
551
552impl_to_dynamic_tuple!(A);
553impl_to_dynamic_tuple!(A, B);
554impl_to_dynamic_tuple!(A, B, C);
555impl_to_dynamic_tuple!(A, B, C, D);
556impl_to_dynamic_tuple!(A, B, C, D, E);
557impl_to_dynamic_tuple!(A, B, C, D, E, F);
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use crate::extrude::LinearExtrudeObject;
563
564 #[test]
565 fn cube_converts() {
566 let cube = Cube {
567 x: 10.0,
568 y: 20.0,
569 z: 30.0,
570 };
571 let node = cube.to_dynamic();
572 assert!(matches!(
573 node,
574 DynamicObject::Cube {
575 size: [10.0, 20.0, 30.0]
576 }
577 ));
578 }
579
580 #[test]
581 fn translated_cube_converts() {
582 let obj = Cube {
583 x: 1.0,
584 y: 1.0,
585 z: 1.0,
586 }
587 .translate(5.0, 0.0, 0.0);
588 let node = obj.to_dynamic();
589 assert!(matches!(node, DynamicObject::Translate { .. }));
590 }
591
592 #[test]
593 fn union_flattens() {
594 let a = Cube {
595 x: 1.0,
596 y: 1.0,
597 z: 1.0,
598 };
599 let b = Cube {
600 x: 2.0,
601 y: 2.0,
602 z: 2.0,
603 };
604 let c = Cube {
605 x: 3.0,
606 y: 3.0,
607 z: 3.0,
608 };
609 let obj = a.union(b).union(c);
610 let node = obj.to_dynamic();
611 match node {
612 DynamicObject::Union { children } => assert_eq!(children.len(), 3),
613 other => panic!("expected Union, got {other:?}"),
614 }
615 }
616
617 #[test]
618 fn colored_converts_u8_to_f64() {
619 let obj = Cube {
620 x: 1.0,
621 y: 1.0,
622 z: 1.0,
623 }
624 .colored(Color::RED);
625 let node = obj.to_dynamic();
626 match node {
627 DynamicObject::Color { color, .. } => {
628 assert!((color[0] - 1.0).abs() < f64::EPSILON);
629 assert!((color[1]).abs() < f64::EPSILON);
630 assert!((color[2]).abs() < f64::EPSILON);
631 assert!((color[3] - 1.0).abs() < f64::EPSILON);
632 }
633 other => panic!("expected Color, got {other:?}"),
634 }
635 }
636
637 #[test]
638 fn linear_extrude_converts() {
639 let obj = crate::Circle {
640 radius: 5.0,
641 fa: 0.0,
642 fs: 0.0,
643 fn_: 32,
644 }
645 .linear_extrude(10.0);
646 let node = obj.to_dynamic();
647 match node {
648 DynamicObject::LinearExtrude { height, child, .. } => {
649 assert!((height - 10.0).abs() < f64::EPSILON);
650 assert!(matches!(*child, DynamicObject::Circle { .. }));
651 }
652 other => panic!("expected LinearExtrude, got {other:?}"),
653 }
654 }
655
656 #[test]
657 fn rotate_extrude_converts() {
658 use crate::extrude::RotateExtrudeObject;
659 let obj = crate::Circle {
660 radius: 5.0,
661 fa: 0.0,
662 fs: 0.0,
663 fn_: 32,
664 }
665 .rotate_extrude()
666 .with_angle(180.0);
667 let node = obj.to_dynamic();
668 match node {
669 DynamicObject::Revolve {
670 angle_degrees,
671 child,
672 ..
673 } => {
674 assert!((angle_degrees - 180.0).abs() < f64::EPSILON);
675 assert!(matches!(*child, DynamicObject::Circle { .. }));
676 }
677 other => panic!("expected Revolve, got {other:?}"),
678 }
679 }
680
681 #[test]
682 fn empty_converts() {
683 assert!(matches!(Empty.to_dynamic(), DynamicObject::Empty));
684 }
685
686 #[test]
687 fn option_none_is_empty() {
688 let obj: Option<Cube> = None;
689 assert!(matches!(obj.to_dynamic(), DynamicObject::Empty));
690 }
691
692 #[test]
693 fn two_container_becomes_group() {
694 let obj = Cube {
695 x: 1.0,
696 y: 1.0,
697 z: 1.0,
698 }
699 .and(Sphere {
700 radius: 2.0,
701 stacks: 16,
702 fn_: 16,
703 });
704 let node = obj.to_dynamic();
705 match node {
706 DynamicObject::Group { children } => assert_eq!(children.len(), 2),
707 other => panic!("expected Group, got {other:?}"),
708 }
709 }
710
711 #[test]
712 fn dynamic_object_implements_object() {
713 let obj = DynamicObject::Cube {
714 size: [1.0, 2.0, 3.0],
715 };
716 assert_eq!(obj.name(), Some("Cube"));
717 }
718
719 #[test]
722 fn extract_identity_from_primitive() {
723 let obj = DynamicObject::Cube {
724 size: [1.0, 2.0, 3.0],
725 };
726 let ext = obj.extract_top_transforms();
727 assert_eq!(ext.transform, DAffine3::IDENTITY);
728 assert!(ext.color.is_none());
729 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
730 }
731
732 #[test]
733 fn extract_single_translate() {
734 let obj = DynamicObject::Translate {
735 offset: [1.0, 2.0, 3.0],
736 child: Box::new(DynamicObject::Cube {
737 size: [1.0, 1.0, 1.0],
738 }),
739 };
740 let ext = obj.extract_top_transforms();
741 let expected = DAffine3::from_translation(DVec3::new(1.0, 2.0, 3.0));
742 assert!((ext.transform.translation - expected.translation).length() < 1e-10);
743 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
744 }
745
746 #[test]
747 fn extract_nested_transforms() {
748 let obj = DynamicObject::Translate {
750 offset: [10.0, 0.0, 0.0],
751 child: Box::new(DynamicObject::Rotate {
752 angles: [0.0, 0.0, 90.0],
753 child: Box::new(DynamicObject::Scale {
754 factors: [2.0, 2.0, 2.0],
755 child: Box::new(DynamicObject::Cube {
756 size: [1.0, 1.0, 1.0],
757 }),
758 }),
759 }),
760 };
761 let ext = obj.extract_top_transforms();
762 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
763 assert!((ext.transform.translation.x - 10.0).abs() < 1e-10);
765 }
766
767 #[test]
768 fn extract_stops_at_boolean() {
769 let obj = DynamicObject::Translate {
770 offset: [5.0, 0.0, 0.0],
771 child: Box::new(DynamicObject::Union {
772 children: vec![
773 DynamicObject::Cube {
774 size: [1.0, 1.0, 1.0],
775 },
776 DynamicObject::Sphere {
777 radius: 1.0,
778 segments: 16,
779 stacks: 8,
780 },
781 ],
782 }),
783 };
784 let ext = obj.extract_top_transforms();
785 assert!((ext.transform.translation.x - 5.0).abs() < 1e-10);
786 assert!(matches!(ext.inner, DynamicObject::Union { .. }));
787 }
788
789 #[test]
790 fn extract_stops_at_mirror() {
791 let obj = DynamicObject::Translate {
792 offset: [5.0, 0.0, 0.0],
793 child: Box::new(DynamicObject::Mirror {
794 axis: [1.0, 0.0, 0.0],
795 child: Box::new(DynamicObject::Cube {
796 size: [1.0, 1.0, 1.0],
797 }),
798 }),
799 };
800 let ext = obj.extract_top_transforms();
801 assert!((ext.transform.translation.x - 5.0).abs() < 1e-10);
802 assert!(matches!(ext.inner, DynamicObject::Mirror { .. }));
803 }
804
805 #[test]
806 fn extract_color() {
807 let obj = DynamicObject::Color {
808 color: [1.0, 0.0, 0.0, 1.0],
809 child: Box::new(DynamicObject::Cube {
810 size: [1.0, 1.0, 1.0],
811 }),
812 };
813 let ext = obj.extract_top_transforms();
814 assert_eq!(ext.color, Some([1.0, 0.0, 0.0, 1.0]));
815 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
816 }
817
818 #[test]
819 fn extract_color_interleaved_with_transform() {
820 let obj = DynamicObject::Translate {
822 offset: [5.0, 0.0, 0.0],
823 child: Box::new(DynamicObject::Color {
824 color: [0.0, 1.0, 0.0, 1.0],
825 child: Box::new(DynamicObject::Rotate {
826 angles: [90.0, 0.0, 0.0],
827 child: Box::new(DynamicObject::Cube {
828 size: [1.0, 1.0, 1.0],
829 }),
830 }),
831 }),
832 };
833 let ext = obj.extract_top_transforms();
834 assert!((ext.transform.translation.x - 5.0).abs() < 1e-10);
835 assert_eq!(ext.color, Some([0.0, 1.0, 0.0, 1.0]));
836 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
837 }
838
839 #[test]
840 fn extract_boolean_at_root() {
841 let obj = DynamicObject::Union {
842 children: vec![DynamicObject::Cube {
843 size: [1.0, 1.0, 1.0],
844 }],
845 };
846 let ext = obj.extract_top_transforms();
847 assert_eq!(ext.transform, DAffine3::IDENTITY);
848 assert!(ext.color.is_none());
849 assert!(matches!(ext.inner, DynamicObject::Union { .. }));
850 }
851
852 fn gold() -> crate::material::MaterialData {
853 crate::material::MaterialData {
854 metallic: 1.0,
855 roughness: 0.25,
856 preset: Some("Gold".into()),
857 }
858 }
859
860 #[test]
861 fn extract_material() {
862 let obj = DynamicObject::Material {
863 material: gold(),
864 child: Box::new(DynamicObject::Cube {
865 size: [1.0, 1.0, 1.0],
866 }),
867 };
868 let ext = obj.extract_top_transforms();
869 assert_eq!(ext.material, Some(gold()));
870 assert!(ext.color.is_none());
871 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
872 }
873
874 #[test]
875 fn extract_material_interleaved_with_transform_and_color() {
876 let obj = DynamicObject::Translate {
878 offset: [5.0, 0.0, 0.0],
879 child: Box::new(DynamicObject::Material {
880 material: gold(),
881 child: Box::new(DynamicObject::Color {
882 color: [0.0, 1.0, 0.0, 1.0],
883 child: Box::new(DynamicObject::Cube {
884 size: [1.0, 1.0, 1.0],
885 }),
886 }),
887 }),
888 };
889 let ext = obj.extract_top_transforms();
890 assert!((ext.transform.translation.x - 5.0).abs() < 1e-10);
891 assert_eq!(ext.color, Some([0.0, 1.0, 0.0, 1.0]));
892 assert_eq!(ext.material, Some(gold()));
893 assert!(matches!(ext.inner, DynamicObject::Cube { .. }));
894 }
895
896 #[test]
897 fn extract_outermost_material_wins() {
898 let obj = DynamicObject::Material {
899 material: gold(),
900 child: Box::new(DynamicObject::Material {
901 material: crate::material::MaterialData::default(),
902 child: Box::new(DynamicObject::Cube {
903 size: [1.0, 1.0, 1.0],
904 }),
905 }),
906 };
907 let ext = obj.extract_top_transforms();
908 assert_eq!(ext.material, Some(gold()));
909 }
910}