1use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::interaction::select::selection::{NodeId, Selection};
12use crate::renderer::{PickId, SceneRenderItem};
13use crate::resources::mesh::mesh_store::MeshId;
14use crate::scene::aabb::Aabb;
15use crate::scene::material::Material;
16use crate::scene::spatial_index::SpatialIndex;
17use crate::scene::traits::ViewportObject;
18use crate::{LightKind, LightSource};
19
20const SPATIAL_THRESHOLD: usize = 500;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub struct LayerId(pub u32);
32
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35pub struct Layer {
36 pub id: LayerId,
38 pub name: String,
40 pub visible: bool,
42 pub locked: bool,
44 pub colour: [f32; 4],
46 pub order: u32,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub struct GroupId(pub u32);
57
58pub struct Group {
60 pub id: GroupId,
62 pub name: String,
64 pub members: HashSet<NodeId>,
66}
67
68pub struct SceneNode {
74 id: NodeId,
75 name: String,
76 mesh_id: Option<MeshId>,
77 material: Material,
78 appearance: crate::scene::material::ItemSettings,
79 visible: bool,
80 show_normals: bool,
81 local_transform: glam::Mat4,
82 world_transform: glam::Mat4,
83 parent: Option<NodeId>,
84 children: Vec<NodeId>,
85 layer: LayerId,
86 dirty: bool,
87 deform_instance: Option<u32>,
90 receives_decals: bool,
92 lod_group: Option<crate::resources::LodGroupId>,
97 pub light: Option<LightSource>,
101}
102
103impl SceneNode {
104 pub fn id(&self) -> NodeId {
106 self.id
107 }
108
109 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 pub fn mesh_id(&self) -> Option<MeshId> {
116 self.mesh_id
117 }
118
119 pub fn lod_group(&self) -> Option<crate::resources::LodGroupId> {
121 self.lod_group
122 }
123
124 pub fn material(&self) -> &Material {
126 &self.material
127 }
128
129 pub fn appearance(&self) -> &crate::scene::material::ItemSettings {
131 &self.appearance
132 }
133
134 pub fn is_visible(&self) -> bool {
136 self.visible
137 }
138
139 pub fn show_normals(&self) -> bool {
141 self.show_normals
142 }
143
144 pub fn local_transform(&self) -> glam::Mat4 {
146 self.local_transform
147 }
148
149 pub fn world_transform(&self) -> glam::Mat4 {
151 self.world_transform
152 }
153
154 pub fn parent(&self) -> Option<NodeId> {
156 self.parent
157 }
158
159 pub fn children(&self) -> &[NodeId] {
161 &self.children
162 }
163
164 pub fn layer(&self) -> LayerId {
166 self.layer
167 }
168
169 pub fn receives_decals(&self) -> bool {
171 self.receives_decals
172 }
173
174 pub fn set_receives_decals(&mut self, v: bool) {
176 self.receives_decals = v;
177 }
178}
179
180impl ViewportObject for SceneNode {
181 fn id(&self) -> u64 {
182 self.id
183 }
184
185 fn mesh_id(&self) -> Option<u64> {
186 self.mesh_id.map(|m| m.index() as u64)
187 }
188
189 fn model_matrix(&self) -> glam::Mat4 {
190 self.world_transform
191 }
192
193 fn position(&self) -> glam::Vec3 {
194 self.world_transform.col(3).truncate()
195 }
196
197 fn rotation(&self) -> glam::Quat {
198 let (_scale, rotation, _translation) = self.world_transform.to_scale_rotation_translation();
199 rotation
200 }
201
202 fn scale(&self) -> glam::Vec3 {
203 let (scale, _rotation, _translation) = self.world_transform.to_scale_rotation_translation();
204 scale
205 }
206
207 fn is_visible(&self) -> bool {
208 self.visible
209 }
210
211 fn colour(&self) -> glam::Vec3 {
212 glam::Vec3::from(self.material.base_colour)
213 }
214
215 fn show_normals(&self) -> bool {
216 self.show_normals
217 }
218
219 fn material(&self) -> Material {
220 self.material
221 }
222}
223
224#[derive(Debug, Clone, Copy, Default)]
230pub struct SceneStats {
231 pub spatial_index_nodes: u32,
234 pub frustum_cull_traversal_ms: f32,
236}
237
238fn resolve_light_to_world(src: &LightSource, world: glam::Mat4) -> LightSource {
248 let translation = world.col(3).truncate();
249 let kind = match &src.kind {
250 LightKind::Directional { direction } => {
251 let rotated = world
252 .transform_vector3(glam::Vec3::from(*direction))
253 .normalize();
254 LightKind::Directional {
255 direction: rotated.into(),
256 }
257 }
258 LightKind::Point { range, .. } => LightKind::Point {
259 position: translation.into(),
260 range: *range,
261 },
262 LightKind::Spot {
263 direction,
264 range,
265 inner_angle,
266 outer_angle,
267 ..
268 } => {
269 let rotated = world
270 .transform_vector3(glam::Vec3::from(*direction))
271 .normalize();
272 LightKind::Spot {
273 position: translation.into(),
274 direction: rotated.into(),
275 range: *range,
276 inner_angle: *inner_angle,
277 outer_angle: *outer_angle,
278 }
279 }
280 };
281 LightSource {
282 kind,
283 colour: src.colour,
284 intensity: src.intensity,
285 importance: src.importance,
286 cast_shadows: src.cast_shadows,
287 }
288}
289
290const DEFAULT_LAYER: LayerId = LayerId(0);
292
293pub struct Scene {
295 nodes: HashMap<NodeId, SceneNode>,
296 roots: Vec<NodeId>,
297 layers: Vec<Layer>,
298 next_id: u64,
299 next_layer_id: u32,
300 groups: Vec<Group>,
301 next_group_id: u32,
302 version: u64,
305 spatial: SpatialIndex,
306 spatial_built: bool,
308 last_scene_stats: SceneStats,
309 live_decals: Vec<LiveDecal>,
311 next_decal_id: u64,
312}
313
314static SCENE_VERSION_CLOCK: AtomicU64 = AtomicU64::new(0);
321
322impl Scene {
323 pub fn new() -> Self {
325 let base = SCENE_VERSION_CLOCK.fetch_add(1 << 20, Ordering::Relaxed);
328 Self {
329 nodes: HashMap::new(),
330 roots: Vec::new(),
331 layers: vec![Layer {
332 id: DEFAULT_LAYER,
333 name: "Default".to_string(),
334 visible: true,
335 locked: false,
336 colour: [1.0, 1.0, 1.0, 1.0],
337 order: 0,
338 }],
339 next_id: 1,
340 next_layer_id: 1,
341 groups: Vec::new(),
342 next_group_id: 0,
343 version: base,
344 spatial: SpatialIndex::new(),
345 spatial_built: false,
346 last_scene_stats: SceneStats::default(),
347 live_decals: Vec::new(),
348 next_decal_id: 0,
349 }
350 }
351
352 pub fn version(&self) -> u64 {
357 self.version
358 }
359
360 pub fn add(
364 &mut self,
365 mesh_id: Option<MeshId>,
366 transform: glam::Mat4,
367 material: Material,
368 ) -> NodeId {
369 self.add_named("", mesh_id, transform, material)
370 }
371
372 pub fn add_named(
374 &mut self,
375 name: &str,
376 mesh_id: Option<MeshId>,
377 transform: glam::Mat4,
378 material: Material,
379 ) -> NodeId {
380 let id = self.next_id;
381 self.next_id += 1;
382 let node = SceneNode {
383 id,
384 name: name.to_string(),
385 mesh_id,
386 material,
387 appearance: crate::scene::material::ItemSettings::default(),
388 visible: true,
389 show_normals: false,
390 local_transform: transform,
391 world_transform: transform,
392 parent: None,
393 children: Vec::new(),
394 layer: DEFAULT_LAYER,
395 dirty: true,
396 deform_instance: None,
397 receives_decals: true,
398 lod_group: None,
399 light: None,
400 };
401 self.nodes.insert(id, node);
402 self.roots.push(id);
403 self.version = self.version.wrapping_add(1);
404 if self.spatial_built {
405 self.spatial.mark_dirty(id);
406 }
407 id
408 }
409
410 pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
413 let mut removed = Vec::new();
414 self.remove_recursive(id, &mut removed);
415
416 if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
418 if let Some(parent) = self.nodes.get_mut(&parent_id) {
419 parent.children.retain(|c| *c != id);
420 }
421 } else {
422 self.roots.retain(|r| *r != id);
423 }
424
425 if self.spatial_built {
427 for &rid in &removed {
428 self.spatial.remove_node(rid);
429 }
430 }
431
432 for &rid in &removed {
434 self.nodes.remove(&rid);
435 }
436 self.roots.retain(|r| !removed.contains(r));
438
439 for group in &mut self.groups {
441 for &rid in &removed {
442 group.members.remove(&rid);
443 }
444 }
445
446 self.version = self.version.wrapping_add(1);
447 removed
448 }
449
450 fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
451 out.push(id);
452 if let Some(node) = self.nodes.get(&id) {
453 for &child in &node.children {
454 self.remove_recursive(child, out);
455 }
456 }
457 }
458
459 pub fn remove_many(&mut self, ids: &[NodeId]) -> Vec<NodeId> {
472 let mut removed = Vec::new();
475 let mut removed_set = HashSet::new();
476 for &id in ids {
477 if self.nodes.contains_key(&id) {
478 self.collect_subtree(id, &mut removed_set, &mut removed);
479 }
480 }
481 if removed.is_empty() {
482 return removed;
483 }
484
485 let mut parents_to_fix = HashSet::new();
490 for &id in ids {
491 if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
492 if !removed_set.contains(&parent_id) {
493 parents_to_fix.insert(parent_id);
494 }
495 }
496 }
497 for parent_id in parents_to_fix {
498 if let Some(parent) = self.nodes.get_mut(&parent_id) {
499 parent.children.retain(|c| !removed_set.contains(c));
500 }
501 }
502
503 if self.spatial_built {
506 for &rid in &removed {
507 self.spatial.remove_node(rid);
508 }
509 }
510
511 for &rid in &removed {
512 self.nodes.remove(&rid);
513 }
514
515 self.roots.retain(|r| !removed_set.contains(r));
517 for group in &mut self.groups {
518 group.members.retain(|m| !removed_set.contains(m));
519 }
520
521 self.version = self.version.wrapping_add(1);
522 removed
523 }
524
525 fn collect_subtree(&self, id: NodeId, seen: &mut HashSet<NodeId>, out: &mut Vec<NodeId>) {
528 if !seen.insert(id) {
529 return;
530 }
531 out.push(id);
532 if let Some(node) = self.nodes.get(&id) {
533 for &child in &node.children {
534 self.collect_subtree(child, seen, out);
535 }
536 }
537 }
538
539 pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
543 let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
545 if let Some(old_pid) = old_parent {
546 if let Some(old_p) = self.nodes.get_mut(&old_pid) {
547 old_p.children.retain(|c| *c != child_id);
548 }
549 } else {
550 self.roots.retain(|r| *r != child_id);
551 }
552
553 if let Some(new_pid) = new_parent {
555 if let Some(new_p) = self.nodes.get_mut(&new_pid) {
556 new_p.children.push(child_id);
557 }
558 } else {
559 self.roots.push(child_id);
560 }
561
562 if let Some(node) = self.nodes.get_mut(&child_id) {
563 node.parent = new_parent;
564 node.dirty = true;
565 }
566 self.version = self.version.wrapping_add(1);
567 if self.spatial_built {
568 self.mark_spatial_dirty_subtree(child_id);
569 }
570 }
571
572 pub fn children(&self, id: NodeId) -> &[NodeId] {
574 self.nodes
575 .get(&id)
576 .map(|n| n.children.as_slice())
577 .unwrap_or(&[])
578 }
579
580 pub fn parent(&self, id: NodeId) -> Option<NodeId> {
582 self.nodes.get(&id).and_then(|n| n.parent)
583 }
584
585 pub fn roots(&self) -> &[NodeId] {
587 &self.roots
588 }
589
590 pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
594 if let Some(node) = self.nodes.get_mut(&id) {
595 node.local_transform = transform;
596 node.dirty = true;
597 }
598 self.mark_descendants_dirty(id);
599 self.version = self.version.wrapping_add(1);
600 if self.spatial_built {
601 self.mark_spatial_dirty_subtree(id);
602 }
603 }
604
605 pub fn set_visible(&mut self, id: NodeId, visible: bool) {
607 if let Some(node) = self.nodes.get_mut(&id) {
608 node.visible = visible;
609 }
610 self.version = self.version.wrapping_add(1);
611 if self.spatial_built {
612 self.spatial.mark_dirty(id);
613 }
614 }
615
616 pub fn set_material(&mut self, id: NodeId, material: Material) {
618 if let Some(node) = self.nodes.get_mut(&id) {
619 node.material = material;
620 }
621 self.version = self.version.wrapping_add(1);
622 }
623
624 pub fn set_appearance(&mut self, id: NodeId, appearance: crate::scene::material::ItemSettings) {
626 if let Some(node) = self.nodes.get_mut(&id) {
627 node.appearance = appearance;
628 }
629 self.version = self.version.wrapping_add(1);
630 }
631
632 pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
634 if let Some(node) = self.nodes.get_mut(&id) {
635 node.mesh_id = mesh_id;
636 }
637 self.version = self.version.wrapping_add(1);
638 if self.spatial_built {
639 self.spatial.mark_dirty(id);
640 }
641 }
642
643 pub fn set_lod_group(&mut self, id: NodeId, lod_group: Option<crate::resources::LodGroupId>) {
650 if let Some(node) = self.nodes.get_mut(&id) {
651 node.lod_group = lod_group;
652 }
653 self.version = self.version.wrapping_add(1);
654 }
655
656 pub fn set_name(&mut self, id: NodeId, name: &str) {
658 if let Some(node) = self.nodes.get_mut(&id) {
659 node.name = name.to_string();
660 }
661 self.version = self.version.wrapping_add(1);
662 }
663
664 pub fn set_deform_instance(&mut self, id: NodeId, instance: Option<u32>) {
670 if let Some(node) = self.nodes.get_mut(&id) {
671 node.deform_instance = instance;
672 }
673 self.version = self.version.wrapping_add(1);
674 }
675
676 pub fn set_receives_decals(&mut self, id: NodeId, v: bool) {
678 if let Some(node) = self.nodes.get_mut(&id) {
679 node.receives_decals = v;
680 }
681 self.version = self.version.wrapping_add(1);
682 }
683
684 pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
686 if let Some(node) = self.nodes.get_mut(&id) {
687 node.show_normals = show;
688 }
689 self.version = self.version.wrapping_add(1);
690 }
691
692 pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
694 if let Some(node) = self.nodes.get_mut(&id) {
695 node.layer = layer;
696 }
697 self.version = self.version.wrapping_add(1);
698 }
699
700 pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
702 self.nodes.get(&id)
703 }
704
705 pub fn node_count(&self) -> usize {
707 self.nodes.len()
708 }
709
710 pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
712 self.nodes.values()
713 }
714
715 pub fn add_layer(&mut self, name: &str) -> LayerId {
719 let id = LayerId(self.next_layer_id);
720 let order = self.next_layer_id;
721 self.next_layer_id += 1;
722 self.layers.push(Layer {
723 id,
724 name: name.to_string(),
725 visible: true,
726 locked: false,
727 colour: [1.0, 1.0, 1.0, 1.0],
728 order,
729 });
730 self.version = self.version.wrapping_add(1);
731 id
732 }
733
734 pub fn remove_layer(&mut self, id: LayerId) {
737 if id == DEFAULT_LAYER {
738 return;
739 }
740 for node in self.nodes.values_mut() {
742 if node.layer == id {
743 node.layer = DEFAULT_LAYER;
744 }
745 }
746 self.layers.retain(|l| l.id != id);
747 self.version = self.version.wrapping_add(1);
748 }
749
750 pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
752 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
753 layer.visible = visible;
754 }
755 self.version = self.version.wrapping_add(1);
756 }
757
758 pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
760 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
761 layer.locked = locked;
762 }
763 self.version = self.version.wrapping_add(1);
764 }
765
766 pub fn set_layer_colour(&mut self, id: LayerId, colour: [f32; 4]) {
768 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
769 layer.colour = colour;
770 }
771 self.version = self.version.wrapping_add(1);
772 }
773
774 pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
776 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
777 layer.order = order;
778 }
779 self.version = self.version.wrapping_add(1);
780 }
781
782 pub fn is_layer_locked(&self, id: LayerId) -> bool {
784 self.layers
785 .iter()
786 .find(|l| l.id == id)
787 .map(|l| l.locked)
788 .unwrap_or(false)
789 }
790
791 pub fn layers(&self) -> Vec<&Layer> {
793 let mut sorted: Vec<&Layer> = self.layers.iter().collect();
794 sorted.sort_by_key(|l| l.order);
795 sorted
796 }
797
798 pub fn create_group(&mut self, name: &str) -> GroupId {
802 let id = GroupId(self.next_group_id);
803 self.next_group_id += 1;
804 self.groups.push(Group {
805 id,
806 name: name.to_string(),
807 members: HashSet::new(),
808 });
809 self.version = self.version.wrapping_add(1);
810 id
811 }
812
813 pub fn remove_group(&mut self, id: GroupId) {
815 self.groups.retain(|g| g.id != id);
816 self.version = self.version.wrapping_add(1);
817 }
818
819 pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
821 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
822 g.members.insert(node);
823 }
824 self.version = self.version.wrapping_add(1);
825 }
826
827 pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
829 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
830 g.members.remove(&node);
831 }
832 self.version = self.version.wrapping_add(1);
833 }
834
835 pub fn get_group(&self, id: GroupId) -> Option<&Group> {
837 self.groups.iter().find(|g| g.id == id)
838 }
839
840 pub fn groups(&self) -> &[Group] {
842 &self.groups
843 }
844
845 pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
847 self.groups
848 .iter()
849 .filter(|g| g.members.contains(&node))
850 .map(|g| g.id)
851 .collect()
852 }
853
854 pub fn update_transforms(&mut self) {
858 let roots: Vec<NodeId> = self.roots.clone();
861 for &root_id in &roots {
862 self.propagate_transform(root_id, glam::Mat4::IDENTITY);
863 }
864 }
865
866 fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
867 let (dirty, local, children) = {
868 let Some(node) = self.nodes.get(&id) else {
869 return;
870 };
871 (node.dirty, node.local_transform, node.children.clone())
872 };
873
874 if dirty {
875 let world = parent_world * local;
876 let node = self.nodes.get_mut(&id).unwrap();
877 node.world_transform = world;
878 node.dirty = false;
879 for &child_id in &children {
881 self.mark_dirty(child_id);
882 self.propagate_transform(child_id, world);
883 }
884 } else {
885 let world = self.nodes[&id].world_transform;
886 for &child_id in &children {
887 self.propagate_transform(child_id, world);
888 }
889 }
890 }
891
892 fn mark_dirty(&mut self, id: NodeId) {
893 if let Some(node) = self.nodes.get_mut(&id) {
894 node.dirty = true;
895 }
896 }
897
898 fn mark_descendants_dirty(&mut self, id: NodeId) {
899 let children = self
900 .nodes
901 .get(&id)
902 .map(|n| n.children.clone())
903 .unwrap_or_default();
904 for child_id in children {
905 self.mark_dirty(child_id);
906 self.mark_descendants_dirty(child_id);
907 }
908 }
909
910 fn mark_spatial_dirty_subtree(&mut self, id: NodeId) {
912 self.spatial.mark_dirty(id);
913 let children = self
914 .nodes
915 .get(&id)
916 .map(|n| n.children.clone())
917 .unwrap_or_default();
918 for child_id in children {
919 self.mark_spatial_dirty_subtree(child_id);
920 }
921 }
922
923 fn build_spatial_index_from(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
926 let entries: Vec<(NodeId, Aabb)> = self
927 .nodes
928 .values()
929 .filter(|n| n.visible && n.mesh_id.is_some())
930 .filter_map(|n| {
931 let local_aabb = mesh_aabb_fn(n.mesh_id.unwrap())?;
932 let world_aabb = local_aabb.transformed(&n.world_transform);
933 Some((n.id, world_aabb))
934 })
935 .collect();
936 self.spatial.rebuild(&entries);
937 self.spatial_built = true;
938 }
939
940 fn flush_spatial_dirty(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
943 let dirty = self.spatial.drain_dirty();
944 for id in dirty {
945 self.spatial.remove_entry(id);
946 let new_aabb = self.nodes.get(&id).and_then(|node| {
947 if !node.visible {
948 return None;
949 }
950 let mesh_id = node.mesh_id?;
951 let local_aabb = mesh_aabb_fn(mesh_id)?;
952 Some(local_aabb.transformed(&node.world_transform))
953 });
954 if let Some(aabb) = new_aabb {
955 self.spatial.insert_entry(id, aabb);
956 }
957 }
958 }
959
960 pub fn rebuild_spatial_index(&mut self, mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>) {
968 self.update_transforms();
969 self.build_spatial_index_from(&mesh_aabb_fn);
970 }
971
972 pub fn scene_stats(&self) -> SceneStats {
974 self.last_scene_stats
975 }
976
977 pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
984 self.update_transforms();
985
986 let layer_visible: HashMap<LayerId, bool> =
987 self.layers.iter().map(|l| (l.id, l.visible)).collect();
988
989 let layer_locked: HashMap<LayerId, bool> =
990 self.layers.iter().map(|l| (l.id, l.locked)).collect();
991
992 let mut items = Vec::new();
993 for node in self.nodes.values() {
994 if !node.visible {
995 continue;
996 }
997 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
998 continue;
999 }
1000 let Some(mesh_id) = node.mesh_id else {
1001 continue;
1002 };
1003 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
1004 let mut item_settings = node.appearance;
1005 item_settings.pick_id = PickId(node.id);
1006 item_settings.selected = if locked {
1007 false
1008 } else {
1009 selection.contains(node.id)
1010 };
1011 items.push(SceneRenderItem {
1012 mesh_id,
1013 model: node.world_transform.to_cols_array_2d(),
1014 settings: item_settings,
1015 show_normals: node.show_normals,
1016 material: node.material,
1017 active_attribute: None,
1018 scalar_range: None,
1019 colourmap_id: None,
1020 nan_colour: None,
1021 warp_attribute: None,
1022 warp_scale: 1.0,
1023 deform_instance: node.deform_instance,
1024 receives_decals: node.receives_decals,
1025 lic: None,
1026 lod_group: node.lod_group,
1027 });
1028 }
1029 items
1030 }
1031
1032 pub fn collect_render_items_culled(
1042 &mut self,
1043 selection: &Selection,
1044 frustum: &crate::camera::frustum::Frustum,
1045 mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>,
1046 ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
1047 self.update_transforms();
1048
1049 let layer_visible: HashMap<LayerId, bool> =
1050 self.layers.iter().map(|l| (l.id, l.visible)).collect();
1051 let layer_locked: HashMap<LayerId, bool> =
1052 self.layers.iter().map(|l| (l.id, l.locked)).collect();
1053
1054 let mut items = Vec::new();
1055 let mut stats = crate::camera::frustum::CullStats::default();
1056
1057 if self.nodes.len() >= SPATIAL_THRESHOLD {
1058 if !self.spatial_built {
1060 self.build_spatial_index_from(&mesh_aabb_fn);
1061 } else {
1062 self.flush_spatial_dirty(&mesh_aabb_fn);
1063 }
1064
1065 let t0 = std::time::Instant::now();
1066 let mut candidate_ids = Vec::new();
1067 self.spatial
1068 .collect_visible(frustum, &mut candidate_ids, &mut stats);
1069 let traversal_ms = t0.elapsed().as_secs_f32() * 1000.0;
1070
1071 for id in candidate_ids {
1072 let Some(node) = self.nodes.get(&id) else {
1073 continue;
1074 };
1075 if !node.visible {
1076 continue;
1077 }
1078 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1079 continue;
1080 }
1081 let Some(mesh_id) = node.mesh_id else {
1082 continue;
1083 };
1084 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
1085 let mut item_settings = node.appearance;
1086 item_settings.pick_id = PickId(node.id);
1087 item_settings.selected = if locked {
1088 false
1089 } else {
1090 selection.contains(node.id)
1091 };
1092 items.push(SceneRenderItem {
1093 mesh_id,
1094 model: node.world_transform.to_cols_array_2d(),
1095 settings: item_settings,
1096 show_normals: node.show_normals,
1097 material: node.material,
1098 active_attribute: None,
1099 scalar_range: None,
1100 colourmap_id: None,
1101 nan_colour: None,
1102 warp_attribute: None,
1103 warp_scale: 1.0,
1104 deform_instance: node.deform_instance,
1105 receives_decals: node.receives_decals,
1106 lic: None,
1107 lod_group: node.lod_group,
1108 });
1109 }
1110
1111 self.last_scene_stats = SceneStats {
1112 spatial_index_nodes: self.spatial.node_count(),
1113 frustum_cull_traversal_ms: traversal_ms,
1114 };
1115 } else {
1116 let t0 = std::time::Instant::now();
1118
1119 for node in self.nodes.values() {
1120 if !node.visible {
1121 continue;
1122 }
1123 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1124 continue;
1125 }
1126 let Some(mesh_id) = node.mesh_id else {
1127 continue;
1128 };
1129
1130 stats.total += 1;
1131
1132 if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
1134 let world_aabb = local_aabb.transformed(&node.world_transform);
1135 if frustum.cull_aabb(&world_aabb) {
1136 stats.culled += 1;
1137 continue;
1138 }
1139 }
1140
1141 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
1142 stats.visible += 1;
1143 let mut item_settings = node.appearance;
1144 item_settings.pick_id = PickId(node.id);
1145 item_settings.selected = if locked {
1146 false
1147 } else {
1148 selection.contains(node.id)
1149 };
1150 items.push(SceneRenderItem {
1151 mesh_id,
1152 model: node.world_transform.to_cols_array_2d(),
1153 settings: item_settings,
1154 show_normals: node.show_normals,
1155 material: node.material,
1156 active_attribute: None,
1157 scalar_range: None,
1158 colourmap_id: None,
1159 nan_colour: None,
1160 warp_attribute: None,
1161 warp_scale: 1.0,
1162 deform_instance: node.deform_instance,
1163 receives_decals: node.receives_decals,
1164 lic: None,
1165 lod_group: node.lod_group,
1166 });
1167 }
1168
1169 self.last_scene_stats = SceneStats {
1170 spatial_index_nodes: 0,
1171 frustum_cull_traversal_ms: t0.elapsed().as_secs_f32() * 1000.0,
1172 };
1173 }
1174
1175 (items, stats)
1176 }
1177
1178 pub fn add_light(&mut self, light: LightSource) -> NodeId {
1189 let id = self.next_id;
1190 self.next_id += 1;
1191 let node = SceneNode {
1192 id,
1193 name: String::new(),
1194 mesh_id: None,
1195 material: crate::scene::material::Material::default(),
1196 appearance: crate::scene::material::ItemSettings::default(),
1197 visible: true,
1198 show_normals: false,
1199 local_transform: glam::Mat4::IDENTITY,
1200 world_transform: glam::Mat4::IDENTITY,
1201 parent: None,
1202 children: Vec::new(),
1203 layer: DEFAULT_LAYER,
1204 dirty: true,
1205 deform_instance: None,
1206 receives_decals: false,
1207 lod_group: None,
1208 light: Some(light),
1209 };
1210 self.nodes.insert(id, node);
1211 self.roots.push(id);
1212 self.version = self.version.wrapping_add(1);
1213 id
1214 }
1215
1216 pub fn collect_lights(&mut self) -> Vec<LightSource> {
1226 self.update_transforms();
1227
1228 let layer_visible: HashMap<LayerId, bool> =
1229 self.layers.iter().map(|l| (l.id, l.visible)).collect();
1230
1231 let mut out = Vec::new();
1232 for node in self.nodes.values() {
1233 if !node.visible {
1234 continue;
1235 }
1236 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1237 continue;
1238 }
1239 let Some(src) = &node.light else {
1240 continue;
1241 };
1242 out.push(resolve_light_to_world(src, node.world_transform));
1243 }
1244 out
1245 }
1246
1247 pub fn set_light(&mut self, id: NodeId, light: Option<LightSource>) {
1249 if let Some(node) = self.nodes.get_mut(&id) {
1250 node.light = light;
1251 }
1252 self.version = self.version.wrapping_add(1);
1253 }
1254
1255 pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
1263 self.nodes
1264 .values()
1265 .filter(|n| n.mesh_id == Some(mesh_id))
1266 .count()
1267 }
1268
1269 pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
1273 let mut result = Vec::new();
1274 for &root_id in &self.roots {
1275 self.walk_recursive(root_id, 0, &mut result);
1276 }
1277 result
1278 }
1279
1280 fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
1281 out.push((id, depth));
1282 if let Some(node) = self.nodes.get(&id) {
1283 for &child_id in &node.children {
1284 self.walk_recursive(child_id, depth + 1, out);
1285 }
1286 }
1287 }
1288}
1289
1290impl Default for Scene {
1291 fn default() -> Self {
1292 Self::new()
1293 }
1294}
1295
1296#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1304pub struct DecalHandle(u64);
1305
1306pub struct LiveDecal {
1313 id: u64,
1314 pub item: crate::renderer::DecalItem,
1317 pub animation: Option<crate::renderer::DecalAnimation>,
1319 pub lifetime: Option<f32>,
1321 pub fade_duration: f32,
1324 pub age: f32,
1326}
1327
1328impl Scene {
1329 pub fn add_decal(&mut self, item: crate::renderer::DecalItem) -> DecalHandle {
1331 self.add_live_decal(item, None, None, 0.0)
1332 }
1333
1334 pub fn add_decal_with_lifetime(
1339 &mut self,
1340 item: crate::renderer::DecalItem,
1341 lifetime: f32,
1342 fade_duration: f32,
1343 ) {
1344 self.add_live_decal(item, Some(lifetime), None, fade_duration);
1345 }
1346
1347 pub fn add_decal_animated(
1349 &mut self,
1350 item: crate::renderer::DecalItem,
1351 animation: crate::renderer::DecalAnimation,
1352 lifetime: Option<f32>,
1353 ) -> DecalHandle {
1354 self.add_live_decal(item, lifetime, Some(animation), 0.0)
1355 }
1356
1357 pub fn remove_decal(&mut self, handle: DecalHandle) {
1359 self.live_decals.retain(|d| d.id != handle.0);
1360 }
1361
1362 pub fn update_decals(&mut self, dt: f32) {
1366 for ld in &mut self.live_decals {
1367 ld.age += dt;
1368 }
1369 self.live_decals
1370 .retain(|ld| ld.lifetime.map_or(true, |lt| ld.age < lt));
1371 }
1372
1373 pub fn collect_decal_items(&self) -> Vec<crate::renderer::DecalItem> {
1378 self.live_decals
1379 .iter()
1380 .map(|ld| {
1381 let mut item = ld.item.clone();
1382
1383 if let Some(lt) = ld.lifetime {
1385 let fade = if ld.fade_duration > 0.0 {
1386 ld.fade_duration.min(lt)
1387 } else {
1388 lt * 0.2
1389 };
1390 let time_left = lt - ld.age;
1391 if time_left < fade {
1392 item.alpha *= (time_left / fade).clamp(0.0, 1.0);
1393 }
1394 }
1395
1396 if let Some(anim) = &ld.animation {
1398 match anim {
1399 crate::renderer::DecalAnimation::UvScroll { vx, vy } => {
1400 item.uv_offset[0] =
1402 (ld.item.uv_offset[0] + vx * ld.age).rem_euclid(1.0);
1403 item.uv_offset[1] =
1404 (ld.item.uv_offset[1] + vy * ld.age).rem_euclid(1.0);
1405 }
1406 crate::renderer::DecalAnimation::SpriteSheet { cols, rows, fps } => {
1407 let total = cols * rows;
1408 let frame = ((ld.age * fps) as u32).rem_euclid(total.max(1));
1409 let col = frame % cols;
1410 let row = frame / cols;
1411 item.uv_scale = [1.0 / *cols as f32, 1.0 / *rows as f32];
1412 item.uv_offset = [col as f32 / *cols as f32, row as f32 / *rows as f32];
1413 }
1414 }
1415 }
1416
1417 item
1418 })
1419 .collect()
1420 }
1421
1422 fn add_live_decal(
1423 &mut self,
1424 item: crate::renderer::DecalItem,
1425 lifetime: Option<f32>,
1426 animation: Option<crate::renderer::DecalAnimation>,
1427 fade_duration: f32,
1428 ) -> DecalHandle {
1429 let id = self.next_decal_id;
1430 self.next_decal_id += 1;
1431 self.live_decals.push(LiveDecal {
1432 id,
1433 item,
1434 animation,
1435 lifetime,
1436 fade_duration,
1437 age: 0.0,
1438 });
1439 DecalHandle(id)
1440 }
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446
1447 #[test]
1448 fn lod_group_round_trips_onto_render_items() {
1449 let mut scene = Scene::new();
1450 let id = scene.add(
1452 Some(MeshId::new(0, 0)),
1453 glam::Mat4::IDENTITY,
1454 Material::default(),
1455 );
1456 assert_eq!(scene.node(id).unwrap().lod_group(), None);
1457
1458 let group = crate::resources::LodGroupId::new(7, 0);
1459 scene.set_lod_group(id, Some(group));
1460 assert_eq!(scene.node(id).unwrap().lod_group(), Some(group));
1461
1462 let items = scene.collect_render_items(&Selection::new());
1463 assert_eq!(items.len(), 1);
1464 assert_eq!(items[0].lod_group, Some(group));
1465
1466 scene.set_lod_group(id, None);
1468 let items = scene.collect_render_items(&Selection::new());
1469 assert_eq!(items[0].lod_group, None);
1470 }
1471
1472 #[test]
1473 fn test_add_and_remove() {
1474 let mut scene = Scene::new();
1475 let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1476 assert!(scene.node(id).is_some());
1477 assert_eq!(scene.node_count(), 1);
1478
1479 let removed = scene.remove(id);
1480 assert_eq!(removed, vec![id]);
1481 assert!(scene.node(id).is_none());
1482 assert_eq!(scene.node_count(), 0);
1483 }
1484
1485 #[test]
1486 fn test_remove_cascades_to_children() {
1487 let mut scene = Scene::new();
1488 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1489 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1490 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1491 scene.set_parent(child1, Some(parent));
1492 scene.set_parent(child2, Some(parent));
1493
1494 let removed = scene.remove(parent);
1495 assert_eq!(removed.len(), 3);
1496 assert!(removed.contains(&parent));
1497 assert!(removed.contains(&child1));
1498 assert!(removed.contains(&child2));
1499 assert_eq!(scene.node_count(), 0);
1500 }
1501
1502 #[test]
1503 fn test_remove_many_clears_roots_and_descendants() {
1504 let mut scene = Scene::new();
1505 let a = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1506 let b = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1507 let b_child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1508 scene.set_parent(b_child, Some(b));
1509 let keep = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1511
1512 let removed = scene.remove_many(&[a, b]);
1513 assert_eq!(removed.len(), 3);
1514 assert!(removed.contains(&a));
1515 assert!(removed.contains(&b));
1516 assert!(removed.contains(&b_child));
1517 assert!(scene.node(a).is_none());
1518 assert!(scene.node(b_child).is_none());
1519 assert!(scene.node(keep).is_some());
1520 assert_eq!(scene.node_count(), 1);
1521 }
1522
1523 #[test]
1524 fn test_remove_many_dedups_overlapping_and_missing() {
1525 let mut scene = Scene::new();
1526 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1527 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1528 scene.set_parent(child, Some(parent));
1529
1530 let removed = scene.remove_many(&[parent, child, parent, 9999_u64]);
1533 assert_eq!(removed.len(), 2);
1534 assert!(removed.contains(&parent));
1535 assert!(removed.contains(&child));
1536 assert_eq!(scene.node_count(), 0);
1537 }
1538
1539 #[test]
1540 fn test_remove_many_detaches_surviving_parent() {
1541 let mut scene = Scene::new();
1542 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1543 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1544 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1545 scene.set_parent(child1, Some(parent));
1546 scene.set_parent(child2, Some(parent));
1547
1548 scene.remove_many(&[child1, child2]);
1549 assert!(scene.node(parent).is_some());
1550 assert!(scene.node(parent).unwrap().children().is_empty());
1551 }
1552
1553 #[test]
1554 fn test_remove_many_empty_is_noop() {
1555 let mut scene = Scene::new();
1556 scene.add(None, glam::Mat4::IDENTITY, Material::default());
1557 let before = scene.version();
1558 let removed = scene.remove_many(&[]);
1559 assert!(removed.is_empty());
1560 assert_eq!(scene.version(), before);
1561 assert_eq!(scene.node_count(), 1);
1562 }
1563
1564 #[test]
1565 fn test_set_parent_updates_world_transform() {
1566 let mut scene = Scene::new();
1567 let parent = scene.add(
1568 None,
1569 glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
1570 Material::default(),
1571 );
1572 let child = scene.add(
1573 None,
1574 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1575 Material::default(),
1576 );
1577 scene.set_parent(child, Some(parent));
1578 scene.update_transforms();
1579
1580 let world = scene.node(child).unwrap().world_transform();
1581 let pos = world.col(3).truncate();
1582 assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
1583 }
1584
1585 #[test]
1586 fn test_dirty_propagation() {
1587 let mut scene = Scene::new();
1588 let parent = scene.add(
1589 None,
1590 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1591 Material::default(),
1592 );
1593 let child = scene.add(
1594 None,
1595 glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
1596 Material::default(),
1597 );
1598 scene.set_parent(child, Some(parent));
1599 scene.update_transforms();
1600
1601 scene.set_local_transform(
1603 parent,
1604 glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
1605 );
1606 scene.update_transforms();
1607
1608 let child_pos = scene
1609 .node(child)
1610 .unwrap()
1611 .world_transform()
1612 .col(3)
1613 .truncate();
1614 assert!(
1615 (child_pos.x - 12.0).abs() < 1e-5,
1616 "expected x=12.0, got {}",
1617 child_pos.x
1618 );
1619 }
1620
1621 #[test]
1622 fn test_layer_visibility_hides_nodes() {
1623 let mut scene = Scene::new();
1624 let layer = scene.add_layer("Hidden");
1625 let id = scene.add(
1626 Some(MeshId::new(0, 0)),
1627 glam::Mat4::IDENTITY,
1628 Material::default(),
1629 );
1630 scene.set_layer(id, layer);
1631 scene.set_layer_visible(layer, false);
1632
1633 let items = scene.collect_render_items(&Selection::new());
1634 assert!(items.is_empty());
1635 }
1636
1637 #[test]
1638 fn test_collect_skips_invisible_nodes() {
1639 let mut scene = Scene::new();
1640 let id = scene.add(
1641 Some(MeshId::new(0, 0)),
1642 glam::Mat4::IDENTITY,
1643 Material::default(),
1644 );
1645 scene.set_visible(id, false);
1646
1647 let items = scene.collect_render_items(&Selection::new());
1648 assert!(items.is_empty());
1649 }
1650
1651 #[test]
1652 fn test_collect_skips_meshless_nodes() {
1653 let mut scene = Scene::new();
1654 scene.add(None, glam::Mat4::IDENTITY, Material::default());
1655
1656 let items = scene.collect_render_items(&Selection::new());
1657 assert!(items.is_empty());
1658 }
1659
1660 #[test]
1661 fn test_collect_marks_selected() {
1662 let mut scene = Scene::new();
1663 let id = scene.add(
1664 Some(MeshId::new(0, 0)),
1665 glam::Mat4::IDENTITY,
1666 Material::default(),
1667 );
1668
1669 let mut sel = Selection::new();
1670 sel.select_one(id);
1671
1672 let items = scene.collect_render_items(&sel);
1673 assert_eq!(items.len(), 1);
1674 assert!(items[0].settings.selected);
1675 }
1676
1677 #[test]
1678 fn test_unparent_makes_root() {
1679 let mut scene = Scene::new();
1680 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1681 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1682 scene.set_parent(child, Some(parent));
1683 assert!(!scene.roots().contains(&child));
1684
1685 scene.set_parent(child, None);
1686 assert!(scene.roots().contains(&child));
1687 assert!(scene.node(child).unwrap().parent().is_none());
1688 }
1689
1690 #[test]
1691 fn test_collect_culled_filters_offscreen() {
1692 let mut scene = Scene::new();
1693 let visible_id = scene.add(
1695 Some(MeshId::new(0, 0)),
1696 glam::Mat4::IDENTITY,
1697 Material::default(),
1698 );
1699 let _behind = scene.add(
1701 Some(MeshId::new(1, 0)),
1702 glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
1703 Material::default(),
1704 );
1705
1706 let sel = Selection::new();
1707 let view = glam::Mat4::look_at_rh(
1709 glam::Vec3::new(0.0, 0.0, 5.0),
1710 glam::Vec3::ZERO,
1711 glam::Vec3::Y,
1712 );
1713 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
1714 let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
1715
1716 let unit_aabb = crate::scene::aabb::Aabb {
1718 min: glam::Vec3::splat(-0.5),
1719 max: glam::Vec3::splat(0.5),
1720 };
1721
1722 let (items, stats) =
1723 scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
1724
1725 assert_eq!(stats.total, 2);
1726 assert_eq!(stats.visible, 1);
1727 assert_eq!(stats.culled, 1);
1728 assert_eq!(items.len(), 1);
1729 assert_eq!(items[0].mesh_id.index(), visible_id as usize - 1); let _ = visible_id; }
1733
1734 #[test]
1737 fn test_layer_locked_field_default_false() {
1738 let scene = Scene::new();
1739 let layers = scene.layers();
1740 let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
1741 assert!(!default_layer.locked);
1742 }
1743
1744 #[test]
1745 fn test_add_layer_has_locked_false_colour_white_and_order() {
1746 let mut scene = Scene::new();
1747 let layer_id = scene.add_layer("Test");
1748 let layers = scene.layers();
1749 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1750 assert!(!layer.locked);
1751 assert_eq!(layer.colour, [1.0, 1.0, 1.0, 1.0]);
1752 assert!(layer.order > 0); }
1754
1755 #[test]
1756 fn test_set_layer_locked() {
1757 let mut scene = Scene::new();
1758 let layer_id = scene.add_layer("Locked");
1759 scene.set_layer_locked(layer_id, true);
1760 assert!(scene.is_layer_locked(layer_id));
1761 scene.set_layer_locked(layer_id, false);
1762 assert!(!scene.is_layer_locked(layer_id));
1763 }
1764
1765 #[test]
1766 fn test_set_layer_colour() {
1767 let mut scene = Scene::new();
1768 let layer_id = scene.add_layer("Coloured");
1769 scene.set_layer_colour(layer_id, [1.0, 0.0, 0.0, 1.0]);
1770 let layers = scene.layers();
1771 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1772 assert_eq!(layer.colour, [1.0, 0.0, 0.0, 1.0]);
1773 }
1774
1775 #[test]
1776 fn test_set_layer_order() {
1777 let mut scene = Scene::new();
1778 let layer_id = scene.add_layer("Orderly");
1779 scene.set_layer_order(layer_id, 99);
1780 let layers = scene.layers();
1781 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1782 assert_eq!(layer.order, 99);
1783 }
1784
1785 #[test]
1786 fn test_locked_layer_suppresses_selection_in_render_items() {
1787 let mut scene = Scene::new();
1788 let layer_id = scene.add_layer("Locked");
1789 let node_id = scene.add(
1790 Some(MeshId::new(0, 0)),
1791 glam::Mat4::IDENTITY,
1792 Material::default(),
1793 );
1794 scene.set_layer(node_id, layer_id);
1795 scene.set_layer_locked(layer_id, true);
1796
1797 let mut sel = Selection::new();
1798 sel.select_one(node_id);
1799
1800 let items = scene.collect_render_items(&sel);
1801 assert_eq!(items.len(), 1, "locked layer nodes still render");
1802 assert!(
1803 !items[0].settings.selected,
1804 "locked layer nodes must not appear selected"
1805 );
1806 }
1807
1808 #[test]
1809 fn test_layers_sorted_by_order() {
1810 let mut scene = Scene::new();
1811 let a = scene.add_layer("A");
1812 let b = scene.add_layer("B");
1813 scene.set_layer_order(a, 10);
1815 scene.set_layer_order(b, 5);
1816 let layers = scene.layers();
1817 let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1819 let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1820 assert!(
1821 pos_b < pos_a,
1822 "layer B (order=5) should appear before A (order=10)"
1823 );
1824 }
1825
1826 #[test]
1829 fn test_create_group_returns_id() {
1830 let mut scene = Scene::new();
1831 let gid = scene.create_group("MyGroup");
1832 let group = scene.get_group(gid).unwrap();
1833 assert_eq!(group.name, "MyGroup");
1834 assert!(group.members.is_empty());
1835 }
1836
1837 #[test]
1838 fn test_add_to_group_and_remove_from_group() {
1839 let mut scene = Scene::new();
1840 let gid = scene.create_group("G");
1841 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1842 scene.add_to_group(node_id, gid);
1843 assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1844 scene.remove_from_group(node_id, gid);
1845 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1846 }
1847
1848 #[test]
1849 fn test_groups_returns_all_groups() {
1850 let mut scene = Scene::new();
1851 scene.create_group("G1");
1852 scene.create_group("G2");
1853 assert_eq!(scene.groups().len(), 2);
1854 }
1855
1856 #[test]
1857 fn test_node_groups_returns_containing_groups() {
1858 let mut scene = Scene::new();
1859 let g1 = scene.create_group("G1");
1860 let g2 = scene.create_group("G2");
1861 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1862 scene.add_to_group(node_id, g1);
1863 scene.add_to_group(node_id, g2);
1864 let groups = scene.node_groups(node_id);
1865 assert_eq!(groups.len(), 2);
1866 assert!(groups.contains(&g1));
1867 assert!(groups.contains(&g2));
1868 }
1869
1870 #[test]
1871 fn test_remove_node_cleans_up_group_membership() {
1872 let mut scene = Scene::new();
1873 let gid = scene.create_group("G");
1874 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1875 scene.add_to_group(node_id, gid);
1876 scene.remove(node_id);
1877 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1878 }
1879
1880 #[test]
1883 fn test_mesh_ref_count_zero_for_unused_mesh() {
1884 let scene = Scene::new();
1885 assert_eq!(scene.mesh_ref_count(MeshId::new(42, 0)), 0);
1886 }
1887
1888 #[test]
1889 fn test_mesh_ref_count_correct_for_nodes() {
1890 let mut scene = Scene::new();
1891 scene.add(
1892 Some(MeshId::new(0, 0)),
1893 glam::Mat4::IDENTITY,
1894 Material::default(),
1895 );
1896 scene.add(
1897 Some(MeshId::new(0, 0)),
1898 glam::Mat4::IDENTITY,
1899 Material::default(),
1900 );
1901 scene.add(
1902 Some(MeshId::new(1, 0)),
1903 glam::Mat4::IDENTITY,
1904 Material::default(),
1905 );
1906 assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 2);
1907 assert_eq!(scene.mesh_ref_count(MeshId::new(1, 0)), 1);
1908 }
1909
1910 #[test]
1911 fn test_mesh_ref_count_decreases_after_remove() {
1912 let mut scene = Scene::new();
1913 let node_a = scene.add(
1914 Some(MeshId::new(0, 0)),
1915 glam::Mat4::IDENTITY,
1916 Material::default(),
1917 );
1918 scene.add(
1919 Some(MeshId::new(0, 0)),
1920 glam::Mat4::IDENTITY,
1921 Material::default(),
1922 );
1923 assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 2);
1924 scene.remove(node_a);
1925 assert_eq!(scene.mesh_ref_count(MeshId::new(0, 0)), 1);
1926 }
1927
1928 #[test]
1929 fn test_remove_group() {
1930 let mut scene = Scene::new();
1931 let gid = scene.create_group("G");
1932 scene.remove_group(gid);
1933 assert!(scene.get_group(gid).is_none());
1934 assert!(scene.groups().is_empty());
1935 }
1936
1937 #[test]
1938 fn test_walk_depth_first_order() {
1939 let mut scene = Scene::new();
1940 let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1941 let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1942 let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1943 let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1944 scene.set_parent(child_a, Some(root));
1945 scene.set_parent(child_b, Some(root));
1946 scene.set_parent(grandchild, Some(child_a));
1947
1948 let walk = scene.walk_depth_first();
1949 assert_eq!(walk.len(), 4);
1950 assert_eq!(walk[0], (root, 0));
1951 assert_eq!(walk[1], (child_a, 1));
1952 assert_eq!(walk[2], (grandchild, 2));
1953 assert_eq!(walk[3], (child_b, 1));
1954 }
1955
1956 fn make_test_frustum() -> crate::camera::frustum::Frustum {
1959 let view = glam::Mat4::look_at_rh(
1960 glam::Vec3::new(0.0, 0.0, 300.0),
1961 glam::Vec3::ZERO,
1962 glam::Vec3::Y,
1963 );
1964 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 1.0, 400.0);
1965 crate::camera::frustum::Frustum::from_view_proj(&(proj * view))
1966 }
1967
1968 fn unit_aabb() -> crate::scene::aabb::Aabb {
1969 crate::scene::aabb::Aabb {
1970 min: glam::Vec3::splat(-0.5),
1971 max: glam::Vec3::splat(0.5),
1972 }
1973 }
1974
1975 #[test]
1980 fn test_octree_parity_with_known_counts() {
1981 let mut scene = Scene::new();
1982 let frustum = make_test_frustum();
1983
1984 for _ in 0..100 {
1986 scene.add(
1987 Some(MeshId::new(0, 0)),
1988 glam::Mat4::IDENTITY,
1989 Material::default(),
1990 );
1991 }
1992 for _ in 0..500 {
1994 let mat = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
1995 scene.add(Some(MeshId::new(0, 0)), mat, Material::default());
1996 }
1997
1998 assert!(scene.node_count() >= SPATIAL_THRESHOLD);
1999
2000 let sel = Selection::new();
2001 let (items, stats) =
2002 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
2003
2004 assert_eq!(
2005 items.len(),
2006 100,
2007 "expected 100 visible items, got {}",
2008 items.len()
2009 );
2010 assert_eq!(stats.visible, 100);
2011 let scene_stats = scene.scene_stats();
2012 assert!(
2013 scene_stats.spatial_index_nodes > 0,
2014 "spatial index should be active"
2015 );
2016 }
2017
2018 #[test]
2021 fn test_octree_incremental_transform_update() {
2022 let mut scene = Scene::new();
2023 let frustum = make_test_frustum();
2024
2025 let mut ids = Vec::new();
2026 for _ in 0..600 {
2028 let id = scene.add(
2029 Some(MeshId::new(0, 0)),
2030 glam::Mat4::IDENTITY,
2031 Material::default(),
2032 );
2033 ids.push(id);
2034 }
2035
2036 let sel = Selection::new();
2037 let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
2038 assert_eq!(items.len(), 600, "all 600 should be visible initially");
2039
2040 let behind = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
2042 for &id in &ids[..300] {
2043 scene.set_local_transform(id, behind);
2044 }
2045
2046 let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
2047 assert_eq!(
2048 items2.len(),
2049 300,
2050 "300 nodes should be visible after moving the others out"
2051 );
2052 }
2053
2054 #[test]
2056 fn test_octree_node_removal() {
2057 let mut scene = Scene::new();
2058 let frustum = make_test_frustum();
2059
2060 let mut ids = Vec::new();
2061 for _ in 0..600 {
2062 let id = scene.add(
2063 Some(MeshId::new(0, 0)),
2064 glam::Mat4::IDENTITY,
2065 Material::default(),
2066 );
2067 ids.push(id);
2068 }
2069
2070 let sel = Selection::new();
2071 let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
2072 assert_eq!(items.len(), 600);
2073
2074 for &id in &ids[..150] {
2076 scene.remove(id);
2077 }
2078
2079 let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
2080 assert_eq!(items2.len(), 450, "should have 450 after removing 150");
2081 }
2082}