1use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::interaction::selection::{NodeId, Selection};
12use crate::renderer::SceneRenderItem;
13use crate::resources::mesh_store::MeshId;
14use crate::scene::material::Material;
15use crate::scene::traits::ViewportObject;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct LayerId(pub u32);
24
25pub struct Layer {
27 pub id: LayerId,
29 pub name: String,
31 pub visible: bool,
33 pub locked: bool,
35 pub color: [f32; 4],
37 pub order: u32,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub struct GroupId(pub u32);
48
49pub struct Group {
51 pub id: GroupId,
53 pub name: String,
55 pub members: HashSet<NodeId>,
57}
58
59pub struct SceneNode {
65 id: NodeId,
66 name: String,
67 mesh_id: Option<MeshId>,
68 material: Material,
69 visible: bool,
70 show_normals: bool,
71 local_transform: glam::Mat4,
72 world_transform: glam::Mat4,
73 parent: Option<NodeId>,
74 children: Vec<NodeId>,
75 layer: LayerId,
76 dirty: bool,
77}
78
79impl SceneNode {
80 pub fn id(&self) -> NodeId {
82 self.id
83 }
84
85 pub fn name(&self) -> &str {
87 &self.name
88 }
89
90 pub fn mesh_id(&self) -> Option<MeshId> {
92 self.mesh_id
93 }
94
95 pub fn material(&self) -> &Material {
97 &self.material
98 }
99
100 pub fn is_visible(&self) -> bool {
102 self.visible
103 }
104
105 pub fn show_normals(&self) -> bool {
107 self.show_normals
108 }
109
110 pub fn local_transform(&self) -> glam::Mat4 {
112 self.local_transform
113 }
114
115 pub fn world_transform(&self) -> glam::Mat4 {
117 self.world_transform
118 }
119
120 pub fn parent(&self) -> Option<NodeId> {
122 self.parent
123 }
124
125 pub fn children(&self) -> &[NodeId] {
127 &self.children
128 }
129
130 pub fn layer(&self) -> LayerId {
132 self.layer
133 }
134}
135
136impl ViewportObject for SceneNode {
137 fn id(&self) -> u64 {
138 self.id
139 }
140
141 fn mesh_id(&self) -> Option<u64> {
142 self.mesh_id.map(|m| m.index() as u64)
143 }
144
145 fn model_matrix(&self) -> glam::Mat4 {
146 self.world_transform
147 }
148
149 fn position(&self) -> glam::Vec3 {
150 self.world_transform.col(3).truncate()
151 }
152
153 fn rotation(&self) -> glam::Quat {
154 let (_scale, rotation, _translation) = self.world_transform.to_scale_rotation_translation();
155 rotation
156 }
157
158 fn scale(&self) -> glam::Vec3 {
159 let (scale, _rotation, _translation) = self.world_transform.to_scale_rotation_translation();
160 scale
161 }
162
163 fn is_visible(&self) -> bool {
164 self.visible
165 }
166
167 fn color(&self) -> glam::Vec3 {
168 glam::Vec3::from(self.material.base_color)
169 }
170
171 fn show_normals(&self) -> bool {
172 self.show_normals
173 }
174
175 fn material(&self) -> Material {
176 self.material
177 }
178}
179
180const DEFAULT_LAYER: LayerId = LayerId(0);
186
187pub struct Scene {
189 nodes: HashMap<NodeId, SceneNode>,
190 roots: Vec<NodeId>,
191 layers: Vec<Layer>,
192 next_id: u64,
193 next_layer_id: u32,
194 groups: Vec<Group>,
195 next_group_id: u32,
196 version: u64,
199}
200
201static SCENE_VERSION_CLOCK: AtomicU64 = AtomicU64::new(0);
208
209impl Scene {
210 pub fn new() -> Self {
212 let base = SCENE_VERSION_CLOCK.fetch_add(1 << 20, Ordering::Relaxed);
215 Self {
216 nodes: HashMap::new(),
217 roots: Vec::new(),
218 layers: vec![Layer {
219 id: DEFAULT_LAYER,
220 name: "Default".to_string(),
221 visible: true,
222 locked: false,
223 color: [1.0, 1.0, 1.0, 1.0],
224 order: 0,
225 }],
226 next_id: 1,
227 next_layer_id: 1,
228 groups: Vec::new(),
229 next_group_id: 0,
230 version: base,
231 }
232 }
233
234 pub fn version(&self) -> u64 {
239 self.version
240 }
241
242 pub fn add(
246 &mut self,
247 mesh_id: Option<MeshId>,
248 transform: glam::Mat4,
249 material: Material,
250 ) -> NodeId {
251 self.add_named("", mesh_id, transform, material)
252 }
253
254 pub fn add_named(
256 &mut self,
257 name: &str,
258 mesh_id: Option<MeshId>,
259 transform: glam::Mat4,
260 material: Material,
261 ) -> NodeId {
262 let id = self.next_id;
263 self.next_id += 1;
264 let node = SceneNode {
265 id,
266 name: name.to_string(),
267 mesh_id,
268 material,
269 visible: true,
270 show_normals: false,
271 local_transform: transform,
272 world_transform: transform,
273 parent: None,
274 children: Vec::new(),
275 layer: DEFAULT_LAYER,
276 dirty: true,
277 };
278 self.nodes.insert(id, node);
279 self.roots.push(id);
280 self.version = self.version.wrapping_add(1);
281 id
282 }
283
284 pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
287 let mut removed = Vec::new();
288 self.remove_recursive(id, &mut removed);
289
290 if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
292 if let Some(parent) = self.nodes.get_mut(&parent_id) {
293 parent.children.retain(|c| *c != id);
294 }
295 } else {
296 self.roots.retain(|r| *r != id);
297 }
298
299 for &rid in &removed {
301 self.nodes.remove(&rid);
302 }
303 self.roots.retain(|r| !removed.contains(r));
305
306 for group in &mut self.groups {
308 for &rid in &removed {
309 group.members.remove(&rid);
310 }
311 }
312
313 self.version = self.version.wrapping_add(1);
314 removed
315 }
316
317 fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
318 out.push(id);
319 if let Some(node) = self.nodes.get(&id) {
320 for &child in &node.children {
321 self.remove_recursive(child, out);
322 }
323 }
324 }
325
326 pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
330 let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
332 if let Some(old_pid) = old_parent {
333 if let Some(old_p) = self.nodes.get_mut(&old_pid) {
334 old_p.children.retain(|c| *c != child_id);
335 }
336 } else {
337 self.roots.retain(|r| *r != child_id);
338 }
339
340 if let Some(new_pid) = new_parent {
342 if let Some(new_p) = self.nodes.get_mut(&new_pid) {
343 new_p.children.push(child_id);
344 }
345 } else {
346 self.roots.push(child_id);
347 }
348
349 if let Some(node) = self.nodes.get_mut(&child_id) {
350 node.parent = new_parent;
351 node.dirty = true;
352 }
353 self.version = self.version.wrapping_add(1);
354 }
355
356 pub fn children(&self, id: NodeId) -> &[NodeId] {
358 self.nodes
359 .get(&id)
360 .map(|n| n.children.as_slice())
361 .unwrap_or(&[])
362 }
363
364 pub fn parent(&self, id: NodeId) -> Option<NodeId> {
366 self.nodes.get(&id).and_then(|n| n.parent)
367 }
368
369 pub fn roots(&self) -> &[NodeId] {
371 &self.roots
372 }
373
374 pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
378 if let Some(node) = self.nodes.get_mut(&id) {
379 node.local_transform = transform;
380 node.dirty = true;
381 }
382 self.mark_descendants_dirty(id);
383 self.version = self.version.wrapping_add(1);
384 }
385
386 pub fn set_visible(&mut self, id: NodeId, visible: bool) {
388 if let Some(node) = self.nodes.get_mut(&id) {
389 node.visible = visible;
390 }
391 self.version = self.version.wrapping_add(1);
392 }
393
394 pub fn set_material(&mut self, id: NodeId, material: Material) {
396 if let Some(node) = self.nodes.get_mut(&id) {
397 node.material = material;
398 }
399 self.version = self.version.wrapping_add(1);
400 }
401
402 pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
404 if let Some(node) = self.nodes.get_mut(&id) {
405 node.mesh_id = mesh_id;
406 }
407 self.version = self.version.wrapping_add(1);
408 }
409
410 pub fn set_name(&mut self, id: NodeId, name: &str) {
412 if let Some(node) = self.nodes.get_mut(&id) {
413 node.name = name.to_string();
414 }
415 self.version = self.version.wrapping_add(1);
416 }
417
418 pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
420 if let Some(node) = self.nodes.get_mut(&id) {
421 node.show_normals = show;
422 }
423 self.version = self.version.wrapping_add(1);
424 }
425
426 pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
428 if let Some(node) = self.nodes.get_mut(&id) {
429 node.layer = layer;
430 }
431 self.version = self.version.wrapping_add(1);
432 }
433
434 pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
436 self.nodes.get(&id)
437 }
438
439 pub fn node_count(&self) -> usize {
441 self.nodes.len()
442 }
443
444 pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
446 self.nodes.values()
447 }
448
449 pub fn add_layer(&mut self, name: &str) -> LayerId {
453 let id = LayerId(self.next_layer_id);
454 let order = self.next_layer_id;
455 self.next_layer_id += 1;
456 self.layers.push(Layer {
457 id,
458 name: name.to_string(),
459 visible: true,
460 locked: false,
461 color: [1.0, 1.0, 1.0, 1.0],
462 order,
463 });
464 self.version = self.version.wrapping_add(1);
465 id
466 }
467
468 pub fn remove_layer(&mut self, id: LayerId) {
471 if id == DEFAULT_LAYER {
472 return;
473 }
474 for node in self.nodes.values_mut() {
476 if node.layer == id {
477 node.layer = DEFAULT_LAYER;
478 }
479 }
480 self.layers.retain(|l| l.id != id);
481 self.version = self.version.wrapping_add(1);
482 }
483
484 pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
486 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
487 layer.visible = visible;
488 }
489 self.version = self.version.wrapping_add(1);
490 }
491
492 pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
494 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
495 layer.locked = locked;
496 }
497 self.version = self.version.wrapping_add(1);
498 }
499
500 pub fn set_layer_color(&mut self, id: LayerId, color: [f32; 4]) {
502 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
503 layer.color = color;
504 }
505 self.version = self.version.wrapping_add(1);
506 }
507
508 pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
510 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
511 layer.order = order;
512 }
513 self.version = self.version.wrapping_add(1);
514 }
515
516 pub fn is_layer_locked(&self, id: LayerId) -> bool {
518 self.layers
519 .iter()
520 .find(|l| l.id == id)
521 .map(|l| l.locked)
522 .unwrap_or(false)
523 }
524
525 pub fn layers(&self) -> Vec<&Layer> {
527 let mut sorted: Vec<&Layer> = self.layers.iter().collect();
528 sorted.sort_by_key(|l| l.order);
529 sorted
530 }
531
532 pub fn create_group(&mut self, name: &str) -> GroupId {
536 let id = GroupId(self.next_group_id);
537 self.next_group_id += 1;
538 self.groups.push(Group {
539 id,
540 name: name.to_string(),
541 members: HashSet::new(),
542 });
543 self.version = self.version.wrapping_add(1);
544 id
545 }
546
547 pub fn remove_group(&mut self, id: GroupId) {
549 self.groups.retain(|g| g.id != id);
550 self.version = self.version.wrapping_add(1);
551 }
552
553 pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
555 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
556 g.members.insert(node);
557 }
558 self.version = self.version.wrapping_add(1);
559 }
560
561 pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
563 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
564 g.members.remove(&node);
565 }
566 self.version = self.version.wrapping_add(1);
567 }
568
569 pub fn get_group(&self, id: GroupId) -> Option<&Group> {
571 self.groups.iter().find(|g| g.id == id)
572 }
573
574 pub fn groups(&self) -> &[Group] {
576 &self.groups
577 }
578
579 pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
581 self.groups
582 .iter()
583 .filter(|g| g.members.contains(&node))
584 .map(|g| g.id)
585 .collect()
586 }
587
588 pub fn update_transforms(&mut self) {
592 let roots: Vec<NodeId> = self.roots.clone();
595 for &root_id in &roots {
596 self.propagate_transform(root_id, glam::Mat4::IDENTITY);
597 }
598 }
599
600 fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
601 let (dirty, local, children) = {
602 let Some(node) = self.nodes.get(&id) else {
603 return;
604 };
605 (node.dirty, node.local_transform, node.children.clone())
606 };
607
608 if dirty {
609 let world = parent_world * local;
610 let node = self.nodes.get_mut(&id).unwrap();
611 node.world_transform = world;
612 node.dirty = false;
613 for &child_id in &children {
615 self.mark_dirty(child_id);
616 self.propagate_transform(child_id, world);
617 }
618 } else {
619 let world = self.nodes[&id].world_transform;
620 for &child_id in &children {
621 self.propagate_transform(child_id, world);
622 }
623 }
624 }
625
626 fn mark_dirty(&mut self, id: NodeId) {
627 if let Some(node) = self.nodes.get_mut(&id) {
628 node.dirty = true;
629 }
630 }
631
632 fn mark_descendants_dirty(&mut self, id: NodeId) {
633 let children = self
634 .nodes
635 .get(&id)
636 .map(|n| n.children.clone())
637 .unwrap_or_default();
638 for child_id in children {
639 self.mark_dirty(child_id);
640 self.mark_descendants_dirty(child_id);
641 }
642 }
643
644 pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
651 self.update_transforms();
652
653 let layer_visible: HashMap<LayerId, bool> =
654 self.layers.iter().map(|l| (l.id, l.visible)).collect();
655
656 let layer_locked: HashMap<LayerId, bool> =
657 self.layers.iter().map(|l| (l.id, l.locked)).collect();
658
659 let mut items = Vec::new();
660 for node in self.nodes.values() {
661 if !node.visible {
662 continue;
663 }
664 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
665 continue;
666 }
667 let Some(mesh_id) = node.mesh_id else {
668 continue;
669 };
670 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
671 items.push(SceneRenderItem {
672 mesh_index: mesh_id.index(),
673 model: node.world_transform.to_cols_array_2d(),
674 selected: if locked {
675 false
676 } else {
677 selection.contains(node.id)
678 },
679 visible: true,
680 show_normals: node.show_normals,
681 material: node.material,
682 active_attribute: None,
683 scalar_range: None,
684 colormap_id: None,
685 nan_color: None,
686 two_sided: node.material.is_two_sided(),
687 pick_id: node.id,
688 });
689 }
690 items
691 }
692
693 pub fn collect_render_items_culled(
699 &mut self,
700 selection: &Selection,
701 frustum: &crate::camera::frustum::Frustum,
702 mesh_aabb_fn: impl Fn(MeshId) -> Option<crate::scene::aabb::Aabb>,
703 ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
704 self.update_transforms();
705
706 let layer_visible: HashMap<LayerId, bool> =
707 self.layers.iter().map(|l| (l.id, l.visible)).collect();
708
709 let layer_locked: HashMap<LayerId, bool> =
710 self.layers.iter().map(|l| (l.id, l.locked)).collect();
711
712 let mut items = Vec::new();
713 let mut stats = crate::camera::frustum::CullStats::default();
714
715 for node in self.nodes.values() {
716 if !node.visible {
717 continue;
718 }
719 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
720 continue;
721 }
722 let Some(mesh_id) = node.mesh_id else {
723 continue;
724 };
725
726 stats.total += 1;
727
728 if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
730 let world_aabb = local_aabb.transformed(&node.world_transform);
731 if frustum.cull_aabb(&world_aabb) {
732 stats.culled += 1;
733 continue;
734 }
735 }
736
737 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
738 stats.visible += 1;
739 items.push(SceneRenderItem {
740 mesh_index: mesh_id.index(),
741 model: node.world_transform.to_cols_array_2d(),
742 selected: if locked {
743 false
744 } else {
745 selection.contains(node.id)
746 },
747 visible: true,
748 show_normals: node.show_normals,
749 material: node.material,
750 active_attribute: None,
751 scalar_range: None,
752 colormap_id: None,
753 nan_color: None,
754 two_sided: node.material.is_two_sided(),
755 pick_id: node.id,
756 });
757 }
758 (items, stats)
759 }
760
761 pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
769 self.nodes
770 .values()
771 .filter(|n| n.mesh_id == Some(mesh_id))
772 .count()
773 }
774
775 pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
779 let mut result = Vec::new();
780 for &root_id in &self.roots {
781 self.walk_recursive(root_id, 0, &mut result);
782 }
783 result
784 }
785
786 fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
787 out.push((id, depth));
788 if let Some(node) = self.nodes.get(&id) {
789 for &child_id in &node.children {
790 self.walk_recursive(child_id, depth + 1, out);
791 }
792 }
793 }
794}
795
796impl Default for Scene {
797 fn default() -> Self {
798 Self::new()
799 }
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn test_add_and_remove() {
808 let mut scene = Scene::new();
809 let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
810 assert!(scene.node(id).is_some());
811 assert_eq!(scene.node_count(), 1);
812
813 let removed = scene.remove(id);
814 assert_eq!(removed, vec![id]);
815 assert!(scene.node(id).is_none());
816 assert_eq!(scene.node_count(), 0);
817 }
818
819 #[test]
820 fn test_remove_cascades_to_children() {
821 let mut scene = Scene::new();
822 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
823 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
824 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
825 scene.set_parent(child1, Some(parent));
826 scene.set_parent(child2, Some(parent));
827
828 let removed = scene.remove(parent);
829 assert_eq!(removed.len(), 3);
830 assert!(removed.contains(&parent));
831 assert!(removed.contains(&child1));
832 assert!(removed.contains(&child2));
833 assert_eq!(scene.node_count(), 0);
834 }
835
836 #[test]
837 fn test_set_parent_updates_world_transform() {
838 let mut scene = Scene::new();
839 let parent = scene.add(
840 None,
841 glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
842 Material::default(),
843 );
844 let child = scene.add(
845 None,
846 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
847 Material::default(),
848 );
849 scene.set_parent(child, Some(parent));
850 scene.update_transforms();
851
852 let world = scene.node(child).unwrap().world_transform();
853 let pos = world.col(3).truncate();
854 assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
855 }
856
857 #[test]
858 fn test_dirty_propagation() {
859 let mut scene = Scene::new();
860 let parent = scene.add(
861 None,
862 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
863 Material::default(),
864 );
865 let child = scene.add(
866 None,
867 glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
868 Material::default(),
869 );
870 scene.set_parent(child, Some(parent));
871 scene.update_transforms();
872
873 scene.set_local_transform(
875 parent,
876 glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
877 );
878 scene.update_transforms();
879
880 let child_pos = scene
881 .node(child)
882 .unwrap()
883 .world_transform()
884 .col(3)
885 .truncate();
886 assert!(
887 (child_pos.x - 12.0).abs() < 1e-5,
888 "expected x=12.0, got {}",
889 child_pos.x
890 );
891 }
892
893 #[test]
894 fn test_layer_visibility_hides_nodes() {
895 let mut scene = Scene::new();
896 let layer = scene.add_layer("Hidden");
897 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
898 scene.set_layer(id, layer);
899 scene.set_layer_visible(layer, false);
900
901 let items = scene.collect_render_items(&Selection::new());
902 assert!(items.is_empty());
903 }
904
905 #[test]
906 fn test_collect_skips_invisible_nodes() {
907 let mut scene = Scene::new();
908 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
909 scene.set_visible(id, false);
910
911 let items = scene.collect_render_items(&Selection::new());
912 assert!(items.is_empty());
913 }
914
915 #[test]
916 fn test_collect_skips_meshless_nodes() {
917 let mut scene = Scene::new();
918 scene.add(None, glam::Mat4::IDENTITY, Material::default());
919
920 let items = scene.collect_render_items(&Selection::new());
921 assert!(items.is_empty());
922 }
923
924 #[test]
925 fn test_collect_marks_selected() {
926 let mut scene = Scene::new();
927 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
928
929 let mut sel = Selection::new();
930 sel.select_one(id);
931
932 let items = scene.collect_render_items(&sel);
933 assert_eq!(items.len(), 1);
934 assert!(items[0].selected);
935 }
936
937 #[test]
938 fn test_unparent_makes_root() {
939 let mut scene = Scene::new();
940 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
941 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
942 scene.set_parent(child, Some(parent));
943 assert!(!scene.roots().contains(&child));
944
945 scene.set_parent(child, None);
946 assert!(scene.roots().contains(&child));
947 assert!(scene.node(child).unwrap().parent().is_none());
948 }
949
950 #[test]
951 fn test_collect_culled_filters_offscreen() {
952 let mut scene = Scene::new();
953 let visible_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
955 let _behind = scene.add(
957 Some(MeshId(1)),
958 glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
959 Material::default(),
960 );
961
962 let sel = Selection::new();
963 let view = glam::Mat4::look_at_rh(
965 glam::Vec3::new(0.0, 0.0, 5.0),
966 glam::Vec3::ZERO,
967 glam::Vec3::Y,
968 );
969 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
970 let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
971
972 let unit_aabb = crate::scene::aabb::Aabb {
974 min: glam::Vec3::splat(-0.5),
975 max: glam::Vec3::splat(0.5),
976 };
977
978 let (items, stats) =
979 scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
980
981 assert_eq!(stats.total, 2);
982 assert_eq!(stats.visible, 1);
983 assert_eq!(stats.culled, 1);
984 assert_eq!(items.len(), 1);
985 assert_eq!(items[0].mesh_index, visible_id as usize - 1); let _ = visible_id; }
989
990 #[test]
993 fn test_layer_locked_field_default_false() {
994 let scene = Scene::new();
995 let layers = scene.layers();
996 let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
997 assert!(!default_layer.locked);
998 }
999
1000 #[test]
1001 fn test_add_layer_has_locked_false_color_white_and_order() {
1002 let mut scene = Scene::new();
1003 let layer_id = scene.add_layer("Test");
1004 let layers = scene.layers();
1005 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1006 assert!(!layer.locked);
1007 assert_eq!(layer.color, [1.0, 1.0, 1.0, 1.0]);
1008 assert!(layer.order > 0); }
1010
1011 #[test]
1012 fn test_set_layer_locked() {
1013 let mut scene = Scene::new();
1014 let layer_id = scene.add_layer("Locked");
1015 scene.set_layer_locked(layer_id, true);
1016 assert!(scene.is_layer_locked(layer_id));
1017 scene.set_layer_locked(layer_id, false);
1018 assert!(!scene.is_layer_locked(layer_id));
1019 }
1020
1021 #[test]
1022 fn test_set_layer_color() {
1023 let mut scene = Scene::new();
1024 let layer_id = scene.add_layer("Colored");
1025 scene.set_layer_color(layer_id, [1.0, 0.0, 0.0, 1.0]);
1026 let layers = scene.layers();
1027 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1028 assert_eq!(layer.color, [1.0, 0.0, 0.0, 1.0]);
1029 }
1030
1031 #[test]
1032 fn test_set_layer_order() {
1033 let mut scene = Scene::new();
1034 let layer_id = scene.add_layer("Orderly");
1035 scene.set_layer_order(layer_id, 99);
1036 let layers = scene.layers();
1037 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1038 assert_eq!(layer.order, 99);
1039 }
1040
1041 #[test]
1042 fn test_locked_layer_suppresses_selection_in_render_items() {
1043 let mut scene = Scene::new();
1044 let layer_id = scene.add_layer("Locked");
1045 let node_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1046 scene.set_layer(node_id, layer_id);
1047 scene.set_layer_locked(layer_id, true);
1048
1049 let mut sel = Selection::new();
1050 sel.select_one(node_id);
1051
1052 let items = scene.collect_render_items(&sel);
1053 assert_eq!(items.len(), 1, "locked layer nodes still render");
1054 assert!(
1055 !items[0].selected,
1056 "locked layer nodes must not appear selected"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_layers_sorted_by_order() {
1062 let mut scene = Scene::new();
1063 let a = scene.add_layer("A");
1064 let b = scene.add_layer("B");
1065 scene.set_layer_order(a, 10);
1067 scene.set_layer_order(b, 5);
1068 let layers = scene.layers();
1069 let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1071 let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1072 assert!(
1073 pos_b < pos_a,
1074 "layer B (order=5) should appear before A (order=10)"
1075 );
1076 }
1077
1078 #[test]
1081 fn test_create_group_returns_id() {
1082 let mut scene = Scene::new();
1083 let gid = scene.create_group("MyGroup");
1084 let group = scene.get_group(gid).unwrap();
1085 assert_eq!(group.name, "MyGroup");
1086 assert!(group.members.is_empty());
1087 }
1088
1089 #[test]
1090 fn test_add_to_group_and_remove_from_group() {
1091 let mut scene = Scene::new();
1092 let gid = scene.create_group("G");
1093 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1094 scene.add_to_group(node_id, gid);
1095 assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1096 scene.remove_from_group(node_id, gid);
1097 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1098 }
1099
1100 #[test]
1101 fn test_groups_returns_all_groups() {
1102 let mut scene = Scene::new();
1103 scene.create_group("G1");
1104 scene.create_group("G2");
1105 assert_eq!(scene.groups().len(), 2);
1106 }
1107
1108 #[test]
1109 fn test_node_groups_returns_containing_groups() {
1110 let mut scene = Scene::new();
1111 let g1 = scene.create_group("G1");
1112 let g2 = scene.create_group("G2");
1113 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1114 scene.add_to_group(node_id, g1);
1115 scene.add_to_group(node_id, g2);
1116 let groups = scene.node_groups(node_id);
1117 assert_eq!(groups.len(), 2);
1118 assert!(groups.contains(&g1));
1119 assert!(groups.contains(&g2));
1120 }
1121
1122 #[test]
1123 fn test_remove_node_cleans_up_group_membership() {
1124 let mut scene = Scene::new();
1125 let gid = scene.create_group("G");
1126 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1127 scene.add_to_group(node_id, gid);
1128 scene.remove(node_id);
1129 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1130 }
1131
1132 #[test]
1135 fn test_mesh_ref_count_zero_for_unused_mesh() {
1136 let scene = Scene::new();
1137 assert_eq!(scene.mesh_ref_count(MeshId(42)), 0);
1138 }
1139
1140 #[test]
1141 fn test_mesh_ref_count_correct_for_nodes() {
1142 let mut scene = Scene::new();
1143 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1144 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1145 scene.add(Some(MeshId(1)), glam::Mat4::IDENTITY, Material::default());
1146 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1147 assert_eq!(scene.mesh_ref_count(MeshId(1)), 1);
1148 }
1149
1150 #[test]
1151 fn test_mesh_ref_count_decreases_after_remove() {
1152 let mut scene = Scene::new();
1153 let node_a = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1154 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1155 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1156 scene.remove(node_a);
1157 assert_eq!(scene.mesh_ref_count(MeshId(0)), 1);
1158 }
1159
1160 #[test]
1161 fn test_remove_group() {
1162 let mut scene = Scene::new();
1163 let gid = scene.create_group("G");
1164 scene.remove_group(gid);
1165 assert!(scene.get_group(gid).is_none());
1166 assert!(scene.groups().is_empty());
1167 }
1168
1169 #[test]
1170 fn test_walk_depth_first_order() {
1171 let mut scene = Scene::new();
1172 let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1173 let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1174 let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1175 let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1176 scene.set_parent(child_a, Some(root));
1177 scene.set_parent(child_b, Some(root));
1178 scene.set_parent(grandchild, Some(child_a));
1179
1180 let walk = scene.walk_depth_first();
1181 assert_eq!(walk.len(), 4);
1182 assert_eq!(walk[0], (root, 0));
1183 assert_eq!(walk[1], (child_a, 1));
1184 assert_eq!(walk[2], (grandchild, 2));
1185 assert_eq!(walk[3], (child_b, 1));
1186 }
1187}