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 pick_id: node.id,
676 });
677 }
678 items
679 }
680
681 pub fn collect_render_items_culled(
687 &mut self,
688 selection: &Selection,
689 frustum: &crate::camera::frustum::Frustum,
690 mesh_aabb_fn: impl Fn(MeshId) -> Option<crate::scene::aabb::Aabb>,
691 ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
692 self.update_transforms();
693
694 let layer_visible: HashMap<LayerId, bool> =
695 self.layers.iter().map(|l| (l.id, l.visible)).collect();
696
697 let layer_locked: HashMap<LayerId, bool> =
698 self.layers.iter().map(|l| (l.id, l.locked)).collect();
699
700 let mut items = Vec::new();
701 let mut stats = crate::camera::frustum::CullStats::default();
702
703 for node in self.nodes.values() {
704 if !node.visible {
705 continue;
706 }
707 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
708 continue;
709 }
710 let Some(mesh_id) = node.mesh_id else {
711 continue;
712 };
713
714 stats.total += 1;
715
716 if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
718 let world_aabb = local_aabb.transformed(&node.world_transform);
719 if frustum.cull_aabb(&world_aabb) {
720 stats.culled += 1;
721 continue;
722 }
723 }
724
725 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
726 stats.visible += 1;
727 items.push(SceneRenderItem {
728 mesh_index: mesh_id.index(),
729 model: node.world_transform.to_cols_array_2d(),
730 selected: if locked {
731 false
732 } else {
733 selection.contains(node.id)
734 },
735 visible: true,
736 show_normals: node.show_normals,
737 material: node.material,
738 active_attribute: None,
739 scalar_range: None,
740 colormap_id: None,
741 nan_color: None,
742 two_sided: false,
743 pick_id: node.id,
744 });
745 }
746 (items, stats)
747 }
748
749 pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
757 self.nodes
758 .values()
759 .filter(|n| n.mesh_id == Some(mesh_id))
760 .count()
761 }
762
763 pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
767 let mut result = Vec::new();
768 for &root_id in &self.roots {
769 self.walk_recursive(root_id, 0, &mut result);
770 }
771 result
772 }
773
774 fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
775 out.push((id, depth));
776 if let Some(node) = self.nodes.get(&id) {
777 for &child_id in &node.children {
778 self.walk_recursive(child_id, depth + 1, out);
779 }
780 }
781 }
782}
783
784impl Default for Scene {
785 fn default() -> Self {
786 Self::new()
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 #[test]
795 fn test_add_and_remove() {
796 let mut scene = Scene::new();
797 let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
798 assert!(scene.node(id).is_some());
799 assert_eq!(scene.node_count(), 1);
800
801 let removed = scene.remove(id);
802 assert_eq!(removed, vec![id]);
803 assert!(scene.node(id).is_none());
804 assert_eq!(scene.node_count(), 0);
805 }
806
807 #[test]
808 fn test_remove_cascades_to_children() {
809 let mut scene = Scene::new();
810 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
811 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
812 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
813 scene.set_parent(child1, Some(parent));
814 scene.set_parent(child2, Some(parent));
815
816 let removed = scene.remove(parent);
817 assert_eq!(removed.len(), 3);
818 assert!(removed.contains(&parent));
819 assert!(removed.contains(&child1));
820 assert!(removed.contains(&child2));
821 assert_eq!(scene.node_count(), 0);
822 }
823
824 #[test]
825 fn test_set_parent_updates_world_transform() {
826 let mut scene = Scene::new();
827 let parent = scene.add(
828 None,
829 glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
830 Material::default(),
831 );
832 let child = scene.add(
833 None,
834 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
835 Material::default(),
836 );
837 scene.set_parent(child, Some(parent));
838 scene.update_transforms();
839
840 let world = scene.node(child).unwrap().world_transform();
841 let pos = world.col(3).truncate();
842 assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
843 }
844
845 #[test]
846 fn test_dirty_propagation() {
847 let mut scene = Scene::new();
848 let parent = scene.add(
849 None,
850 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
851 Material::default(),
852 );
853 let child = scene.add(
854 None,
855 glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
856 Material::default(),
857 );
858 scene.set_parent(child, Some(parent));
859 scene.update_transforms();
860
861 scene.set_local_transform(
863 parent,
864 glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
865 );
866 scene.update_transforms();
867
868 let child_pos = scene
869 .node(child)
870 .unwrap()
871 .world_transform()
872 .col(3)
873 .truncate();
874 assert!(
875 (child_pos.x - 12.0).abs() < 1e-5,
876 "expected x=12.0, got {}",
877 child_pos.x
878 );
879 }
880
881 #[test]
882 fn test_layer_visibility_hides_nodes() {
883 let mut scene = Scene::new();
884 let layer = scene.add_layer("Hidden");
885 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
886 scene.set_layer(id, layer);
887 scene.set_layer_visible(layer, false);
888
889 let items = scene.collect_render_items(&Selection::new());
890 assert!(items.is_empty());
891 }
892
893 #[test]
894 fn test_collect_skips_invisible_nodes() {
895 let mut scene = Scene::new();
896 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
897 scene.set_visible(id, false);
898
899 let items = scene.collect_render_items(&Selection::new());
900 assert!(items.is_empty());
901 }
902
903 #[test]
904 fn test_collect_skips_meshless_nodes() {
905 let mut scene = Scene::new();
906 scene.add(None, glam::Mat4::IDENTITY, Material::default());
907
908 let items = scene.collect_render_items(&Selection::new());
909 assert!(items.is_empty());
910 }
911
912 #[test]
913 fn test_collect_marks_selected() {
914 let mut scene = Scene::new();
915 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
916
917 let mut sel = Selection::new();
918 sel.select_one(id);
919
920 let items = scene.collect_render_items(&sel);
921 assert_eq!(items.len(), 1);
922 assert!(items[0].selected);
923 }
924
925 #[test]
926 fn test_unparent_makes_root() {
927 let mut scene = Scene::new();
928 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
929 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
930 scene.set_parent(child, Some(parent));
931 assert!(!scene.roots().contains(&child));
932
933 scene.set_parent(child, None);
934 assert!(scene.roots().contains(&child));
935 assert!(scene.node(child).unwrap().parent().is_none());
936 }
937
938 #[test]
939 fn test_collect_culled_filters_offscreen() {
940 let mut scene = Scene::new();
941 let visible_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
943 let _behind = scene.add(
945 Some(MeshId(1)),
946 glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
947 Material::default(),
948 );
949
950 let sel = Selection::new();
951 let view = glam::Mat4::look_at_rh(
953 glam::Vec3::new(0.0, 0.0, 5.0),
954 glam::Vec3::ZERO,
955 glam::Vec3::Y,
956 );
957 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
958 let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
959
960 let unit_aabb = crate::scene::aabb::Aabb {
962 min: glam::Vec3::splat(-0.5),
963 max: glam::Vec3::splat(0.5),
964 };
965
966 let (items, stats) =
967 scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
968
969 assert_eq!(stats.total, 2);
970 assert_eq!(stats.visible, 1);
971 assert_eq!(stats.culled, 1);
972 assert_eq!(items.len(), 1);
973 assert_eq!(items[0].mesh_index, visible_id as usize - 1); let _ = visible_id; }
977
978 #[test]
981 fn test_layer_locked_field_default_false() {
982 let scene = Scene::new();
983 let layers = scene.layers();
984 let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
985 assert!(!default_layer.locked);
986 }
987
988 #[test]
989 fn test_add_layer_has_locked_false_color_white_and_order() {
990 let mut scene = Scene::new();
991 let layer_id = scene.add_layer("Test");
992 let layers = scene.layers();
993 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
994 assert!(!layer.locked);
995 assert_eq!(layer.color, [1.0, 1.0, 1.0, 1.0]);
996 assert!(layer.order > 0); }
998
999 #[test]
1000 fn test_set_layer_locked() {
1001 let mut scene = Scene::new();
1002 let layer_id = scene.add_layer("Locked");
1003 scene.set_layer_locked(layer_id, true);
1004 assert!(scene.is_layer_locked(layer_id));
1005 scene.set_layer_locked(layer_id, false);
1006 assert!(!scene.is_layer_locked(layer_id));
1007 }
1008
1009 #[test]
1010 fn test_set_layer_color() {
1011 let mut scene = Scene::new();
1012 let layer_id = scene.add_layer("Colored");
1013 scene.set_layer_color(layer_id, [1.0, 0.0, 0.0, 1.0]);
1014 let layers = scene.layers();
1015 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1016 assert_eq!(layer.color, [1.0, 0.0, 0.0, 1.0]);
1017 }
1018
1019 #[test]
1020 fn test_set_layer_order() {
1021 let mut scene = Scene::new();
1022 let layer_id = scene.add_layer("Orderly");
1023 scene.set_layer_order(layer_id, 99);
1024 let layers = scene.layers();
1025 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1026 assert_eq!(layer.order, 99);
1027 }
1028
1029 #[test]
1030 fn test_locked_layer_suppresses_selection_in_render_items() {
1031 let mut scene = Scene::new();
1032 let layer_id = scene.add_layer("Locked");
1033 let node_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1034 scene.set_layer(node_id, layer_id);
1035 scene.set_layer_locked(layer_id, true);
1036
1037 let mut sel = Selection::new();
1038 sel.select_one(node_id);
1039
1040 let items = scene.collect_render_items(&sel);
1041 assert_eq!(items.len(), 1, "locked layer nodes still render");
1042 assert!(
1043 !items[0].selected,
1044 "locked layer nodes must not appear selected"
1045 );
1046 }
1047
1048 #[test]
1049 fn test_layers_sorted_by_order() {
1050 let mut scene = Scene::new();
1051 let a = scene.add_layer("A");
1052 let b = scene.add_layer("B");
1053 scene.set_layer_order(a, 10);
1055 scene.set_layer_order(b, 5);
1056 let layers = scene.layers();
1057 let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1059 let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1060 assert!(
1061 pos_b < pos_a,
1062 "layer B (order=5) should appear before A (order=10)"
1063 );
1064 }
1065
1066 #[test]
1069 fn test_create_group_returns_id() {
1070 let mut scene = Scene::new();
1071 let gid = scene.create_group("MyGroup");
1072 let group = scene.get_group(gid).unwrap();
1073 assert_eq!(group.name, "MyGroup");
1074 assert!(group.members.is_empty());
1075 }
1076
1077 #[test]
1078 fn test_add_to_group_and_remove_from_group() {
1079 let mut scene = Scene::new();
1080 let gid = scene.create_group("G");
1081 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1082 scene.add_to_group(node_id, gid);
1083 assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1084 scene.remove_from_group(node_id, gid);
1085 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1086 }
1087
1088 #[test]
1089 fn test_groups_returns_all_groups() {
1090 let mut scene = Scene::new();
1091 scene.create_group("G1");
1092 scene.create_group("G2");
1093 assert_eq!(scene.groups().len(), 2);
1094 }
1095
1096 #[test]
1097 fn test_node_groups_returns_containing_groups() {
1098 let mut scene = Scene::new();
1099 let g1 = scene.create_group("G1");
1100 let g2 = scene.create_group("G2");
1101 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1102 scene.add_to_group(node_id, g1);
1103 scene.add_to_group(node_id, g2);
1104 let groups = scene.node_groups(node_id);
1105 assert_eq!(groups.len(), 2);
1106 assert!(groups.contains(&g1));
1107 assert!(groups.contains(&g2));
1108 }
1109
1110 #[test]
1111 fn test_remove_node_cleans_up_group_membership() {
1112 let mut scene = Scene::new();
1113 let gid = scene.create_group("G");
1114 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1115 scene.add_to_group(node_id, gid);
1116 scene.remove(node_id);
1117 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1118 }
1119
1120 #[test]
1123 fn test_mesh_ref_count_zero_for_unused_mesh() {
1124 let scene = Scene::new();
1125 assert_eq!(scene.mesh_ref_count(MeshId(42)), 0);
1126 }
1127
1128 #[test]
1129 fn test_mesh_ref_count_correct_for_nodes() {
1130 let mut scene = Scene::new();
1131 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1132 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1133 scene.add(Some(MeshId(1)), glam::Mat4::IDENTITY, Material::default());
1134 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1135 assert_eq!(scene.mesh_ref_count(MeshId(1)), 1);
1136 }
1137
1138 #[test]
1139 fn test_mesh_ref_count_decreases_after_remove() {
1140 let mut scene = Scene::new();
1141 let node_a = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1142 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1143 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1144 scene.remove(node_a);
1145 assert_eq!(scene.mesh_ref_count(MeshId(0)), 1);
1146 }
1147
1148 #[test]
1149 fn test_remove_group() {
1150 let mut scene = Scene::new();
1151 let gid = scene.create_group("G");
1152 scene.remove_group(gid);
1153 assert!(scene.get_group(gid).is_none());
1154 assert!(scene.groups().is_empty());
1155 }
1156
1157 #[test]
1158 fn test_walk_depth_first_order() {
1159 let mut scene = Scene::new();
1160 let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1161 let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1162 let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1163 let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1164 scene.set_parent(child_a, Some(root));
1165 scene.set_parent(child_b, Some(root));
1166 scene.set_parent(grandchild, Some(child_a));
1167
1168 let walk = scene.walk_depth_first();
1169 assert_eq!(walk.len(), 4);
1170 assert_eq!(walk[0], (root, 0));
1171 assert_eq!(walk[1], (child_a, 1));
1172 assert_eq!(walk[2], (grandchild, 2));
1173 assert_eq!(walk[3], (child_b, 1));
1174 }
1175}