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