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