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