1use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::interaction::selection::{NodeId, Selection};
12use crate::{LightKind, LightSource};
13use crate::renderer::{PickId, SceneRenderItem};
14use crate::resources::mesh_store::MeshId;
15use crate::scene::aabb::Aabb;
16use crate::scene::material::Material;
17use crate::scene::spatial_index::SpatialIndex;
18use crate::scene::traits::ViewportObject;
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 skin_instance: Option<u32>,
90 receives_decals: bool,
92 pub light: Option<LightSource>,
96}
97
98impl SceneNode {
99 pub fn id(&self) -> NodeId {
101 self.id
102 }
103
104 pub fn name(&self) -> &str {
106 &self.name
107 }
108
109 pub fn mesh_id(&self) -> Option<MeshId> {
111 self.mesh_id
112 }
113
114 pub fn material(&self) -> &Material {
116 &self.material
117 }
118
119 pub fn appearance(&self) -> &crate::scene::material::ItemSettings {
121 &self.appearance
122 }
123
124 pub fn is_visible(&self) -> bool {
126 self.visible
127 }
128
129 pub fn show_normals(&self) -> bool {
131 self.show_normals
132 }
133
134 pub fn local_transform(&self) -> glam::Mat4 {
136 self.local_transform
137 }
138
139 pub fn world_transform(&self) -> glam::Mat4 {
141 self.world_transform
142 }
143
144 pub fn parent(&self) -> Option<NodeId> {
146 self.parent
147 }
148
149 pub fn children(&self) -> &[NodeId] {
151 &self.children
152 }
153
154 pub fn layer(&self) -> LayerId {
156 self.layer
157 }
158
159 pub fn receives_decals(&self) -> bool {
161 self.receives_decals
162 }
163
164 pub fn set_receives_decals(&mut self, v: bool) {
166 self.receives_decals = v;
167 }
168}
169
170impl ViewportObject for SceneNode {
171 fn id(&self) -> u64 {
172 self.id
173 }
174
175 fn mesh_id(&self) -> Option<u64> {
176 self.mesh_id.map(|m| m.index() as u64)
177 }
178
179 fn model_matrix(&self) -> glam::Mat4 {
180 self.world_transform
181 }
182
183 fn position(&self) -> glam::Vec3 {
184 self.world_transform.col(3).truncate()
185 }
186
187 fn rotation(&self) -> glam::Quat {
188 let (_scale, rotation, _translation) = self.world_transform.to_scale_rotation_translation();
189 rotation
190 }
191
192 fn scale(&self) -> glam::Vec3 {
193 let (scale, _rotation, _translation) = self.world_transform.to_scale_rotation_translation();
194 scale
195 }
196
197 fn is_visible(&self) -> bool {
198 self.visible
199 }
200
201 fn colour(&self) -> glam::Vec3 {
202 glam::Vec3::from(self.material.base_colour)
203 }
204
205 fn show_normals(&self) -> bool {
206 self.show_normals
207 }
208
209 fn material(&self) -> Material {
210 self.material
211 }
212}
213
214#[derive(Debug, Clone, Copy, Default)]
220pub struct SceneStats {
221 pub spatial_index_nodes: u32,
224 pub frustum_cull_traversal_ms: f32,
226}
227
228fn resolve_light_to_world(src: &LightSource, world: glam::Mat4) -> LightSource {
238 let translation = world.col(3).truncate();
239 let kind = match &src.kind {
240 LightKind::Directional { direction } => {
241 let rotated = world.transform_vector3(glam::Vec3::from(*direction)).normalize();
242 LightKind::Directional { direction: rotated.into() }
243 }
244 LightKind::Point { range, .. } => {
245 LightKind::Point { position: translation.into(), range: *range }
246 }
247 LightKind::Spot { direction, range, inner_angle, outer_angle, .. } => {
248 let rotated = world.transform_vector3(glam::Vec3::from(*direction)).normalize();
249 LightKind::Spot {
250 position: translation.into(),
251 direction: rotated.into(),
252 range: *range,
253 inner_angle: *inner_angle,
254 outer_angle: *outer_angle,
255 }
256 }
257 };
258 LightSource { kind, colour: src.colour, intensity: src.intensity }
259}
260
261const DEFAULT_LAYER: LayerId = LayerId(0);
263
264pub struct Scene {
266 nodes: HashMap<NodeId, SceneNode>,
267 roots: Vec<NodeId>,
268 layers: Vec<Layer>,
269 next_id: u64,
270 next_layer_id: u32,
271 groups: Vec<Group>,
272 next_group_id: u32,
273 version: u64,
276 spatial: SpatialIndex,
277 spatial_built: bool,
279 last_scene_stats: SceneStats,
280 live_decals: Vec<LiveDecal>,
282 next_decal_id: u64,
283}
284
285static SCENE_VERSION_CLOCK: AtomicU64 = AtomicU64::new(0);
292
293impl Scene {
294 pub fn new() -> Self {
296 let base = SCENE_VERSION_CLOCK.fetch_add(1 << 20, Ordering::Relaxed);
299 Self {
300 nodes: HashMap::new(),
301 roots: Vec::new(),
302 layers: vec![Layer {
303 id: DEFAULT_LAYER,
304 name: "Default".to_string(),
305 visible: true,
306 locked: false,
307 colour: [1.0, 1.0, 1.0, 1.0],
308 order: 0,
309 }],
310 next_id: 1,
311 next_layer_id: 1,
312 groups: Vec::new(),
313 next_group_id: 0,
314 version: base,
315 spatial: SpatialIndex::new(),
316 spatial_built: false,
317 last_scene_stats: SceneStats::default(),
318 live_decals: Vec::new(),
319 next_decal_id: 0,
320 }
321 }
322
323 pub fn version(&self) -> u64 {
328 self.version
329 }
330
331 pub fn add(
335 &mut self,
336 mesh_id: Option<MeshId>,
337 transform: glam::Mat4,
338 material: Material,
339 ) -> NodeId {
340 self.add_named("", mesh_id, transform, material)
341 }
342
343 pub fn add_named(
345 &mut self,
346 name: &str,
347 mesh_id: Option<MeshId>,
348 transform: glam::Mat4,
349 material: Material,
350 ) -> NodeId {
351 let id = self.next_id;
352 self.next_id += 1;
353 let node = SceneNode {
354 id,
355 name: name.to_string(),
356 mesh_id,
357 material,
358 appearance: crate::scene::material::ItemSettings::default(),
359 visible: true,
360 show_normals: false,
361 local_transform: transform,
362 world_transform: transform,
363 parent: None,
364 children: Vec::new(),
365 layer: DEFAULT_LAYER,
366 dirty: true,
367 skin_instance: None,
368 receives_decals: true,
369 light: None,
370 };
371 self.nodes.insert(id, node);
372 self.roots.push(id);
373 self.version = self.version.wrapping_add(1);
374 if self.spatial_built {
375 self.spatial.mark_dirty(id);
376 }
377 id
378 }
379
380 pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
383 let mut removed = Vec::new();
384 self.remove_recursive(id, &mut removed);
385
386 if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
388 if let Some(parent) = self.nodes.get_mut(&parent_id) {
389 parent.children.retain(|c| *c != id);
390 }
391 } else {
392 self.roots.retain(|r| *r != id);
393 }
394
395 if self.spatial_built {
397 for &rid in &removed {
398 self.spatial.remove_node(rid);
399 }
400 }
401
402 for &rid in &removed {
404 self.nodes.remove(&rid);
405 }
406 self.roots.retain(|r| !removed.contains(r));
408
409 for group in &mut self.groups {
411 for &rid in &removed {
412 group.members.remove(&rid);
413 }
414 }
415
416 self.version = self.version.wrapping_add(1);
417 removed
418 }
419
420 fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
421 out.push(id);
422 if let Some(node) = self.nodes.get(&id) {
423 for &child in &node.children {
424 self.remove_recursive(child, out);
425 }
426 }
427 }
428
429 pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
433 let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
435 if let Some(old_pid) = old_parent {
436 if let Some(old_p) = self.nodes.get_mut(&old_pid) {
437 old_p.children.retain(|c| *c != child_id);
438 }
439 } else {
440 self.roots.retain(|r| *r != child_id);
441 }
442
443 if let Some(new_pid) = new_parent {
445 if let Some(new_p) = self.nodes.get_mut(&new_pid) {
446 new_p.children.push(child_id);
447 }
448 } else {
449 self.roots.push(child_id);
450 }
451
452 if let Some(node) = self.nodes.get_mut(&child_id) {
453 node.parent = new_parent;
454 node.dirty = true;
455 }
456 self.version = self.version.wrapping_add(1);
457 if self.spatial_built {
458 self.mark_spatial_dirty_subtree(child_id);
459 }
460 }
461
462 pub fn children(&self, id: NodeId) -> &[NodeId] {
464 self.nodes
465 .get(&id)
466 .map(|n| n.children.as_slice())
467 .unwrap_or(&[])
468 }
469
470 pub fn parent(&self, id: NodeId) -> Option<NodeId> {
472 self.nodes.get(&id).and_then(|n| n.parent)
473 }
474
475 pub fn roots(&self) -> &[NodeId] {
477 &self.roots
478 }
479
480 pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
484 if let Some(node) = self.nodes.get_mut(&id) {
485 node.local_transform = transform;
486 node.dirty = true;
487 }
488 self.mark_descendants_dirty(id);
489 self.version = self.version.wrapping_add(1);
490 if self.spatial_built {
491 self.mark_spatial_dirty_subtree(id);
492 }
493 }
494
495 pub fn set_visible(&mut self, id: NodeId, visible: bool) {
497 if let Some(node) = self.nodes.get_mut(&id) {
498 node.visible = visible;
499 }
500 self.version = self.version.wrapping_add(1);
501 if self.spatial_built {
502 self.spatial.mark_dirty(id);
503 }
504 }
505
506 pub fn set_material(&mut self, id: NodeId, material: Material) {
508 if let Some(node) = self.nodes.get_mut(&id) {
509 node.material = material;
510 }
511 self.version = self.version.wrapping_add(1);
512 }
513
514 pub fn set_appearance(&mut self, id: NodeId, appearance: crate::scene::material::ItemSettings) {
516 if let Some(node) = self.nodes.get_mut(&id) {
517 node.appearance = appearance;
518 }
519 self.version = self.version.wrapping_add(1);
520 }
521
522 pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
524 if let Some(node) = self.nodes.get_mut(&id) {
525 node.mesh_id = mesh_id;
526 }
527 self.version = self.version.wrapping_add(1);
528 if self.spatial_built {
529 self.spatial.mark_dirty(id);
530 }
531 }
532
533 pub fn set_name(&mut self, id: NodeId, name: &str) {
535 if let Some(node) = self.nodes.get_mut(&id) {
536 node.name = name.to_string();
537 }
538 self.version = self.version.wrapping_add(1);
539 }
540
541 pub fn set_skin_instance(&mut self, id: NodeId, instance: Option<u32>) {
548 if let Some(node) = self.nodes.get_mut(&id) {
549 node.skin_instance = instance;
550 }
551 self.version = self.version.wrapping_add(1);
552 }
553
554 pub fn set_receives_decals(&mut self, id: NodeId, v: bool) {
556 if let Some(node) = self.nodes.get_mut(&id) {
557 node.receives_decals = v;
558 }
559 self.version = self.version.wrapping_add(1);
560 }
561
562 pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
564 if let Some(node) = self.nodes.get_mut(&id) {
565 node.show_normals = show;
566 }
567 self.version = self.version.wrapping_add(1);
568 }
569
570 pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
572 if let Some(node) = self.nodes.get_mut(&id) {
573 node.layer = layer;
574 }
575 self.version = self.version.wrapping_add(1);
576 }
577
578 pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
580 self.nodes.get(&id)
581 }
582
583 pub fn node_count(&self) -> usize {
585 self.nodes.len()
586 }
587
588 pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
590 self.nodes.values()
591 }
592
593 pub fn add_layer(&mut self, name: &str) -> LayerId {
597 let id = LayerId(self.next_layer_id);
598 let order = self.next_layer_id;
599 self.next_layer_id += 1;
600 self.layers.push(Layer {
601 id,
602 name: name.to_string(),
603 visible: true,
604 locked: false,
605 colour: [1.0, 1.0, 1.0, 1.0],
606 order,
607 });
608 self.version = self.version.wrapping_add(1);
609 id
610 }
611
612 pub fn remove_layer(&mut self, id: LayerId) {
615 if id == DEFAULT_LAYER {
616 return;
617 }
618 for node in self.nodes.values_mut() {
620 if node.layer == id {
621 node.layer = DEFAULT_LAYER;
622 }
623 }
624 self.layers.retain(|l| l.id != id);
625 self.version = self.version.wrapping_add(1);
626 }
627
628 pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
630 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
631 layer.visible = visible;
632 }
633 self.version = self.version.wrapping_add(1);
634 }
635
636 pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
638 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
639 layer.locked = locked;
640 }
641 self.version = self.version.wrapping_add(1);
642 }
643
644 pub fn set_layer_colour(&mut self, id: LayerId, colour: [f32; 4]) {
646 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
647 layer.colour = colour;
648 }
649 self.version = self.version.wrapping_add(1);
650 }
651
652 pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
654 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
655 layer.order = order;
656 }
657 self.version = self.version.wrapping_add(1);
658 }
659
660 pub fn is_layer_locked(&self, id: LayerId) -> bool {
662 self.layers
663 .iter()
664 .find(|l| l.id == id)
665 .map(|l| l.locked)
666 .unwrap_or(false)
667 }
668
669 pub fn layers(&self) -> Vec<&Layer> {
671 let mut sorted: Vec<&Layer> = self.layers.iter().collect();
672 sorted.sort_by_key(|l| l.order);
673 sorted
674 }
675
676 pub fn create_group(&mut self, name: &str) -> GroupId {
680 let id = GroupId(self.next_group_id);
681 self.next_group_id += 1;
682 self.groups.push(Group {
683 id,
684 name: name.to_string(),
685 members: HashSet::new(),
686 });
687 self.version = self.version.wrapping_add(1);
688 id
689 }
690
691 pub fn remove_group(&mut self, id: GroupId) {
693 self.groups.retain(|g| g.id != id);
694 self.version = self.version.wrapping_add(1);
695 }
696
697 pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
699 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
700 g.members.insert(node);
701 }
702 self.version = self.version.wrapping_add(1);
703 }
704
705 pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
707 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
708 g.members.remove(&node);
709 }
710 self.version = self.version.wrapping_add(1);
711 }
712
713 pub fn get_group(&self, id: GroupId) -> Option<&Group> {
715 self.groups.iter().find(|g| g.id == id)
716 }
717
718 pub fn groups(&self) -> &[Group] {
720 &self.groups
721 }
722
723 pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
725 self.groups
726 .iter()
727 .filter(|g| g.members.contains(&node))
728 .map(|g| g.id)
729 .collect()
730 }
731
732 pub fn update_transforms(&mut self) {
736 let roots: Vec<NodeId> = self.roots.clone();
739 for &root_id in &roots {
740 self.propagate_transform(root_id, glam::Mat4::IDENTITY);
741 }
742 }
743
744 fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
745 let (dirty, local, children) = {
746 let Some(node) = self.nodes.get(&id) else {
747 return;
748 };
749 (node.dirty, node.local_transform, node.children.clone())
750 };
751
752 if dirty {
753 let world = parent_world * local;
754 let node = self.nodes.get_mut(&id).unwrap();
755 node.world_transform = world;
756 node.dirty = false;
757 for &child_id in &children {
759 self.mark_dirty(child_id);
760 self.propagate_transform(child_id, world);
761 }
762 } else {
763 let world = self.nodes[&id].world_transform;
764 for &child_id in &children {
765 self.propagate_transform(child_id, world);
766 }
767 }
768 }
769
770 fn mark_dirty(&mut self, id: NodeId) {
771 if let Some(node) = self.nodes.get_mut(&id) {
772 node.dirty = true;
773 }
774 }
775
776 fn mark_descendants_dirty(&mut self, id: NodeId) {
777 let children = self
778 .nodes
779 .get(&id)
780 .map(|n| n.children.clone())
781 .unwrap_or_default();
782 for child_id in children {
783 self.mark_dirty(child_id);
784 self.mark_descendants_dirty(child_id);
785 }
786 }
787
788 fn mark_spatial_dirty_subtree(&mut self, id: NodeId) {
790 self.spatial.mark_dirty(id);
791 let children = self
792 .nodes
793 .get(&id)
794 .map(|n| n.children.clone())
795 .unwrap_or_default();
796 for child_id in children {
797 self.mark_spatial_dirty_subtree(child_id);
798 }
799 }
800
801 fn build_spatial_index_from(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
804 let entries: Vec<(NodeId, Aabb)> = self
805 .nodes
806 .values()
807 .filter(|n| n.visible && n.mesh_id.is_some())
808 .filter_map(|n| {
809 let local_aabb = mesh_aabb_fn(n.mesh_id.unwrap())?;
810 let world_aabb = local_aabb.transformed(&n.world_transform);
811 Some((n.id, world_aabb))
812 })
813 .collect();
814 self.spatial.rebuild(&entries);
815 self.spatial_built = true;
816 }
817
818 fn flush_spatial_dirty(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
821 let dirty = self.spatial.drain_dirty();
822 for id in dirty {
823 self.spatial.remove_entry(id);
824 let new_aabb = self.nodes.get(&id).and_then(|node| {
825 if !node.visible {
826 return None;
827 }
828 let mesh_id = node.mesh_id?;
829 let local_aabb = mesh_aabb_fn(mesh_id)?;
830 Some(local_aabb.transformed(&node.world_transform))
831 });
832 if let Some(aabb) = new_aabb {
833 self.spatial.insert_entry(id, aabb);
834 }
835 }
836 }
837
838 pub fn rebuild_spatial_index(&mut self, mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>) {
846 self.update_transforms();
847 self.build_spatial_index_from(&mesh_aabb_fn);
848 }
849
850 pub fn scene_stats(&self) -> SceneStats {
852 self.last_scene_stats
853 }
854
855 pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
862 self.update_transforms();
863
864 let layer_visible: HashMap<LayerId, bool> =
865 self.layers.iter().map(|l| (l.id, l.visible)).collect();
866
867 let layer_locked: HashMap<LayerId, bool> =
868 self.layers.iter().map(|l| (l.id, l.locked)).collect();
869
870 let mut items = Vec::new();
871 for node in self.nodes.values() {
872 if !node.visible {
873 continue;
874 }
875 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
876 continue;
877 }
878 let Some(mesh_id) = node.mesh_id else {
879 continue;
880 };
881 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
882 let mut item_settings = node.appearance;
883 item_settings.pick_id = PickId(node.id);
884 item_settings.selected = if locked { false } else { selection.contains(node.id) };
885 items.push(SceneRenderItem {
886 mesh_id,
887 model: node.world_transform.to_cols_array_2d(),
888 settings: item_settings,
889 show_normals: node.show_normals,
890 material: node.material,
891 active_attribute: None,
892 scalar_range: None,
893 colourmap_id: None,
894 nan_colour: None,
895 warp_attribute: None,
896 warp_scale: 1.0,
897 skin_instance: node.skin_instance,
898 receives_decals: node.receives_decals,
899 lic: None,
900 });
901 }
902 items
903 }
904
905 pub fn collect_render_items_culled(
915 &mut self,
916 selection: &Selection,
917 frustum: &crate::camera::frustum::Frustum,
918 mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>,
919 ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
920 self.update_transforms();
921
922 let layer_visible: HashMap<LayerId, bool> =
923 self.layers.iter().map(|l| (l.id, l.visible)).collect();
924 let layer_locked: HashMap<LayerId, bool> =
925 self.layers.iter().map(|l| (l.id, l.locked)).collect();
926
927 let mut items = Vec::new();
928 let mut stats = crate::camera::frustum::CullStats::default();
929
930 if self.nodes.len() >= SPATIAL_THRESHOLD {
931 if !self.spatial_built {
933 self.build_spatial_index_from(&mesh_aabb_fn);
934 } else {
935 self.flush_spatial_dirty(&mesh_aabb_fn);
936 }
937
938 let t0 = std::time::Instant::now();
939 let mut candidate_ids = Vec::new();
940 self.spatial.collect_visible(frustum, &mut candidate_ids, &mut stats);
941 let traversal_ms = t0.elapsed().as_secs_f32() * 1000.0;
942
943 for id in candidate_ids {
944 let Some(node) = self.nodes.get(&id) else {
945 continue;
946 };
947 if !node.visible {
948 continue;
949 }
950 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
951 continue;
952 }
953 let Some(mesh_id) = node.mesh_id else {
954 continue;
955 };
956 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
957 let mut item_settings = node.appearance;
958 item_settings.pick_id = PickId(node.id);
959 item_settings.selected = if locked { false } else { selection.contains(node.id) };
960 items.push(SceneRenderItem {
961 mesh_id,
962 model: node.world_transform.to_cols_array_2d(),
963 settings: item_settings,
964 show_normals: node.show_normals,
965 material: node.material,
966 active_attribute: None,
967 scalar_range: None,
968 colourmap_id: None,
969 nan_colour: None,
970 warp_attribute: None,
971 warp_scale: 1.0,
972 skin_instance: node.skin_instance,
973 receives_decals: node.receives_decals,
974 lic: None,
975 });
976 }
977
978 self.last_scene_stats = SceneStats {
979 spatial_index_nodes: self.spatial.node_count(),
980 frustum_cull_traversal_ms: traversal_ms,
981 };
982 } else {
983 let t0 = std::time::Instant::now();
985
986 for node in self.nodes.values() {
987 if !node.visible {
988 continue;
989 }
990 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
991 continue;
992 }
993 let Some(mesh_id) = node.mesh_id else {
994 continue;
995 };
996
997 stats.total += 1;
998
999 if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
1001 let world_aabb = local_aabb.transformed(&node.world_transform);
1002 if frustum.cull_aabb(&world_aabb) {
1003 stats.culled += 1;
1004 continue;
1005 }
1006 }
1007
1008 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
1009 stats.visible += 1;
1010 let mut item_settings = node.appearance;
1011 item_settings.pick_id = PickId(node.id);
1012 item_settings.selected = if locked { false } else { selection.contains(node.id) };
1013 items.push(SceneRenderItem {
1014 mesh_id,
1015 model: node.world_transform.to_cols_array_2d(),
1016 settings: item_settings,
1017 show_normals: node.show_normals,
1018 material: node.material,
1019 active_attribute: None,
1020 scalar_range: None,
1021 colourmap_id: None,
1022 nan_colour: None,
1023 warp_attribute: None,
1024 warp_scale: 1.0,
1025 skin_instance: node.skin_instance,
1026 receives_decals: node.receives_decals,
1027 lic: None,
1028 });
1029 }
1030
1031 self.last_scene_stats = SceneStats {
1032 spatial_index_nodes: 0,
1033 frustum_cull_traversal_ms: t0.elapsed().as_secs_f32() * 1000.0,
1034 };
1035 }
1036
1037 (items, stats)
1038 }
1039
1040 pub fn add_light(&mut self, light: LightSource) -> NodeId {
1051 let id = self.next_id;
1052 self.next_id += 1;
1053 let node = SceneNode {
1054 id,
1055 name: String::new(),
1056 mesh_id: None,
1057 material: crate::scene::material::Material::default(),
1058 appearance: crate::scene::material::ItemSettings::default(),
1059 visible: true,
1060 show_normals: false,
1061 local_transform: glam::Mat4::IDENTITY,
1062 world_transform: glam::Mat4::IDENTITY,
1063 parent: None,
1064 children: Vec::new(),
1065 layer: DEFAULT_LAYER,
1066 dirty: true,
1067 skin_instance: None,
1068 receives_decals: false,
1069 light: Some(light),
1070 };
1071 self.nodes.insert(id, node);
1072 self.roots.push(id);
1073 self.version = self.version.wrapping_add(1);
1074 id
1075 }
1076
1077 pub fn collect_lights(&mut self) -> Vec<LightSource> {
1087 self.update_transforms();
1088
1089 let layer_visible: HashMap<LayerId, bool> =
1090 self.layers.iter().map(|l| (l.id, l.visible)).collect();
1091
1092 let mut out = Vec::new();
1093 for node in self.nodes.values() {
1094 if !node.visible {
1095 continue;
1096 }
1097 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1098 continue;
1099 }
1100 let Some(src) = &node.light else {
1101 continue;
1102 };
1103 out.push(resolve_light_to_world(src, node.world_transform));
1104 }
1105 out
1106 }
1107
1108 pub fn set_light(&mut self, id: NodeId, light: Option<LightSource>) {
1110 if let Some(node) = self.nodes.get_mut(&id) {
1111 node.light = light;
1112 }
1113 self.version = self.version.wrapping_add(1);
1114 }
1115
1116 pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
1124 self.nodes
1125 .values()
1126 .filter(|n| n.mesh_id == Some(mesh_id))
1127 .count()
1128 }
1129
1130 pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
1134 let mut result = Vec::new();
1135 for &root_id in &self.roots {
1136 self.walk_recursive(root_id, 0, &mut result);
1137 }
1138 result
1139 }
1140
1141 fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
1142 out.push((id, depth));
1143 if let Some(node) = self.nodes.get(&id) {
1144 for &child_id in &node.children {
1145 self.walk_recursive(child_id, depth + 1, out);
1146 }
1147 }
1148 }
1149}
1150
1151impl Default for Scene {
1152 fn default() -> Self {
1153 Self::new()
1154 }
1155}
1156
1157#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1165pub struct DecalHandle(u64);
1166
1167pub struct LiveDecal {
1174 id: u64,
1175 pub item: crate::renderer::DecalItem,
1178 pub animation: Option<crate::renderer::DecalAnimation>,
1180 pub lifetime: Option<f32>,
1182 pub fade_duration: f32,
1185 pub age: f32,
1187}
1188
1189impl Scene {
1190 pub fn add_decal(&mut self, item: crate::renderer::DecalItem) -> DecalHandle {
1192 self.add_live_decal(item, None, None, 0.0)
1193 }
1194
1195 pub fn add_decal_with_lifetime(
1200 &mut self,
1201 item: crate::renderer::DecalItem,
1202 lifetime: f32,
1203 fade_duration: f32,
1204 ) {
1205 self.add_live_decal(item, Some(lifetime), None, fade_duration);
1206 }
1207
1208 pub fn add_decal_animated(
1210 &mut self,
1211 item: crate::renderer::DecalItem,
1212 animation: crate::renderer::DecalAnimation,
1213 lifetime: Option<f32>,
1214 ) -> DecalHandle {
1215 self.add_live_decal(item, lifetime, Some(animation), 0.0)
1216 }
1217
1218 pub fn remove_decal(&mut self, handle: DecalHandle) {
1220 self.live_decals.retain(|d| d.id != handle.0);
1221 }
1222
1223 pub fn update_decals(&mut self, dt: f32) {
1227 for ld in &mut self.live_decals {
1228 ld.age += dt;
1229 }
1230 self.live_decals
1231 .retain(|ld| ld.lifetime.map_or(true, |lt| ld.age < lt));
1232 }
1233
1234 pub fn collect_decal_items(&self) -> Vec<crate::renderer::DecalItem> {
1239 self.live_decals
1240 .iter()
1241 .map(|ld| {
1242 let mut item = ld.item.clone();
1243
1244 if let Some(lt) = ld.lifetime {
1246 let fade = if ld.fade_duration > 0.0 {
1247 ld.fade_duration.min(lt)
1248 } else {
1249 lt * 0.2
1250 };
1251 let time_left = lt - ld.age;
1252 if time_left < fade {
1253 item.alpha *= (time_left / fade).clamp(0.0, 1.0);
1254 }
1255 }
1256
1257 if let Some(anim) = &ld.animation {
1259 match anim {
1260 crate::renderer::DecalAnimation::UvScroll { vx, vy } => {
1261 item.uv_offset[0] =
1263 (ld.item.uv_offset[0] + vx * ld.age).rem_euclid(1.0);
1264 item.uv_offset[1] =
1265 (ld.item.uv_offset[1] + vy * ld.age).rem_euclid(1.0);
1266 }
1267 crate::renderer::DecalAnimation::SpriteSheet { cols, rows, fps } => {
1268 let total = cols * rows;
1269 let frame = ((ld.age * fps) as u32).rem_euclid(total.max(1));
1270 let col = frame % cols;
1271 let row = frame / cols;
1272 item.uv_scale = [1.0 / *cols as f32, 1.0 / *rows as f32];
1273 item.uv_offset = [
1274 col as f32 / *cols as f32,
1275 row as f32 / *rows as f32,
1276 ];
1277 }
1278 }
1279 }
1280
1281 item
1282 })
1283 .collect()
1284 }
1285
1286 fn add_live_decal(
1287 &mut self,
1288 item: crate::renderer::DecalItem,
1289 lifetime: Option<f32>,
1290 animation: Option<crate::renderer::DecalAnimation>,
1291 fade_duration: f32,
1292 ) -> DecalHandle {
1293 let id = self.next_decal_id;
1294 self.next_decal_id += 1;
1295 self.live_decals.push(LiveDecal {
1296 id,
1297 item,
1298 animation,
1299 lifetime,
1300 fade_duration,
1301 age: 0.0,
1302 });
1303 DecalHandle(id)
1304 }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use super::*;
1310
1311 #[test]
1312 fn test_add_and_remove() {
1313 let mut scene = Scene::new();
1314 let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1315 assert!(scene.node(id).is_some());
1316 assert_eq!(scene.node_count(), 1);
1317
1318 let removed = scene.remove(id);
1319 assert_eq!(removed, vec![id]);
1320 assert!(scene.node(id).is_none());
1321 assert_eq!(scene.node_count(), 0);
1322 }
1323
1324 #[test]
1325 fn test_remove_cascades_to_children() {
1326 let mut scene = Scene::new();
1327 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1328 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1329 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1330 scene.set_parent(child1, Some(parent));
1331 scene.set_parent(child2, Some(parent));
1332
1333 let removed = scene.remove(parent);
1334 assert_eq!(removed.len(), 3);
1335 assert!(removed.contains(&parent));
1336 assert!(removed.contains(&child1));
1337 assert!(removed.contains(&child2));
1338 assert_eq!(scene.node_count(), 0);
1339 }
1340
1341 #[test]
1342 fn test_set_parent_updates_world_transform() {
1343 let mut scene = Scene::new();
1344 let parent = scene.add(
1345 None,
1346 glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
1347 Material::default(),
1348 );
1349 let child = scene.add(
1350 None,
1351 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1352 Material::default(),
1353 );
1354 scene.set_parent(child, Some(parent));
1355 scene.update_transforms();
1356
1357 let world = scene.node(child).unwrap().world_transform();
1358 let pos = world.col(3).truncate();
1359 assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
1360 }
1361
1362 #[test]
1363 fn test_dirty_propagation() {
1364 let mut scene = Scene::new();
1365 let parent = scene.add(
1366 None,
1367 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1368 Material::default(),
1369 );
1370 let child = scene.add(
1371 None,
1372 glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
1373 Material::default(),
1374 );
1375 scene.set_parent(child, Some(parent));
1376 scene.update_transforms();
1377
1378 scene.set_local_transform(
1380 parent,
1381 glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
1382 );
1383 scene.update_transforms();
1384
1385 let child_pos = scene
1386 .node(child)
1387 .unwrap()
1388 .world_transform()
1389 .col(3)
1390 .truncate();
1391 assert!(
1392 (child_pos.x - 12.0).abs() < 1e-5,
1393 "expected x=12.0, got {}",
1394 child_pos.x
1395 );
1396 }
1397
1398 #[test]
1399 fn test_layer_visibility_hides_nodes() {
1400 let mut scene = Scene::new();
1401 let layer = scene.add_layer("Hidden");
1402 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1403 scene.set_layer(id, layer);
1404 scene.set_layer_visible(layer, false);
1405
1406 let items = scene.collect_render_items(&Selection::new());
1407 assert!(items.is_empty());
1408 }
1409
1410 #[test]
1411 fn test_collect_skips_invisible_nodes() {
1412 let mut scene = Scene::new();
1413 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1414 scene.set_visible(id, false);
1415
1416 let items = scene.collect_render_items(&Selection::new());
1417 assert!(items.is_empty());
1418 }
1419
1420 #[test]
1421 fn test_collect_skips_meshless_nodes() {
1422 let mut scene = Scene::new();
1423 scene.add(None, glam::Mat4::IDENTITY, Material::default());
1424
1425 let items = scene.collect_render_items(&Selection::new());
1426 assert!(items.is_empty());
1427 }
1428
1429 #[test]
1430 fn test_collect_marks_selected() {
1431 let mut scene = Scene::new();
1432 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1433
1434 let mut sel = Selection::new();
1435 sel.select_one(id);
1436
1437 let items = scene.collect_render_items(&sel);
1438 assert_eq!(items.len(), 1);
1439 assert!(items[0].settings.selected);
1440 }
1441
1442 #[test]
1443 fn test_unparent_makes_root() {
1444 let mut scene = Scene::new();
1445 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1446 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1447 scene.set_parent(child, Some(parent));
1448 assert!(!scene.roots().contains(&child));
1449
1450 scene.set_parent(child, None);
1451 assert!(scene.roots().contains(&child));
1452 assert!(scene.node(child).unwrap().parent().is_none());
1453 }
1454
1455 #[test]
1456 fn test_collect_culled_filters_offscreen() {
1457 let mut scene = Scene::new();
1458 let visible_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1460 let _behind = scene.add(
1462 Some(MeshId(1)),
1463 glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
1464 Material::default(),
1465 );
1466
1467 let sel = Selection::new();
1468 let view = glam::Mat4::look_at_rh(
1470 glam::Vec3::new(0.0, 0.0, 5.0),
1471 glam::Vec3::ZERO,
1472 glam::Vec3::Y,
1473 );
1474 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
1475 let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
1476
1477 let unit_aabb = crate::scene::aabb::Aabb {
1479 min: glam::Vec3::splat(-0.5),
1480 max: glam::Vec3::splat(0.5),
1481 };
1482
1483 let (items, stats) =
1484 scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
1485
1486 assert_eq!(stats.total, 2);
1487 assert_eq!(stats.visible, 1);
1488 assert_eq!(stats.culled, 1);
1489 assert_eq!(items.len(), 1);
1490 assert_eq!(items[0].mesh_id.index(), visible_id as usize - 1); let _ = visible_id; }
1494
1495 #[test]
1498 fn test_layer_locked_field_default_false() {
1499 let scene = Scene::new();
1500 let layers = scene.layers();
1501 let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
1502 assert!(!default_layer.locked);
1503 }
1504
1505 #[test]
1506 fn test_add_layer_has_locked_false_colour_white_and_order() {
1507 let mut scene = Scene::new();
1508 let layer_id = scene.add_layer("Test");
1509 let layers = scene.layers();
1510 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1511 assert!(!layer.locked);
1512 assert_eq!(layer.colour, [1.0, 1.0, 1.0, 1.0]);
1513 assert!(layer.order > 0); }
1515
1516 #[test]
1517 fn test_set_layer_locked() {
1518 let mut scene = Scene::new();
1519 let layer_id = scene.add_layer("Locked");
1520 scene.set_layer_locked(layer_id, true);
1521 assert!(scene.is_layer_locked(layer_id));
1522 scene.set_layer_locked(layer_id, false);
1523 assert!(!scene.is_layer_locked(layer_id));
1524 }
1525
1526 #[test]
1527 fn test_set_layer_colour() {
1528 let mut scene = Scene::new();
1529 let layer_id = scene.add_layer("Coloured");
1530 scene.set_layer_colour(layer_id, [1.0, 0.0, 0.0, 1.0]);
1531 let layers = scene.layers();
1532 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1533 assert_eq!(layer.colour, [1.0, 0.0, 0.0, 1.0]);
1534 }
1535
1536 #[test]
1537 fn test_set_layer_order() {
1538 let mut scene = Scene::new();
1539 let layer_id = scene.add_layer("Orderly");
1540 scene.set_layer_order(layer_id, 99);
1541 let layers = scene.layers();
1542 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1543 assert_eq!(layer.order, 99);
1544 }
1545
1546 #[test]
1547 fn test_locked_layer_suppresses_selection_in_render_items() {
1548 let mut scene = Scene::new();
1549 let layer_id = scene.add_layer("Locked");
1550 let node_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1551 scene.set_layer(node_id, layer_id);
1552 scene.set_layer_locked(layer_id, true);
1553
1554 let mut sel = Selection::new();
1555 sel.select_one(node_id);
1556
1557 let items = scene.collect_render_items(&sel);
1558 assert_eq!(items.len(), 1, "locked layer nodes still render");
1559 assert!(
1560 !items[0].settings.selected,
1561 "locked layer nodes must not appear selected"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_layers_sorted_by_order() {
1567 let mut scene = Scene::new();
1568 let a = scene.add_layer("A");
1569 let b = scene.add_layer("B");
1570 scene.set_layer_order(a, 10);
1572 scene.set_layer_order(b, 5);
1573 let layers = scene.layers();
1574 let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1576 let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1577 assert!(
1578 pos_b < pos_a,
1579 "layer B (order=5) should appear before A (order=10)"
1580 );
1581 }
1582
1583 #[test]
1586 fn test_create_group_returns_id() {
1587 let mut scene = Scene::new();
1588 let gid = scene.create_group("MyGroup");
1589 let group = scene.get_group(gid).unwrap();
1590 assert_eq!(group.name, "MyGroup");
1591 assert!(group.members.is_empty());
1592 }
1593
1594 #[test]
1595 fn test_add_to_group_and_remove_from_group() {
1596 let mut scene = Scene::new();
1597 let gid = scene.create_group("G");
1598 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1599 scene.add_to_group(node_id, gid);
1600 assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1601 scene.remove_from_group(node_id, gid);
1602 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1603 }
1604
1605 #[test]
1606 fn test_groups_returns_all_groups() {
1607 let mut scene = Scene::new();
1608 scene.create_group("G1");
1609 scene.create_group("G2");
1610 assert_eq!(scene.groups().len(), 2);
1611 }
1612
1613 #[test]
1614 fn test_node_groups_returns_containing_groups() {
1615 let mut scene = Scene::new();
1616 let g1 = scene.create_group("G1");
1617 let g2 = scene.create_group("G2");
1618 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1619 scene.add_to_group(node_id, g1);
1620 scene.add_to_group(node_id, g2);
1621 let groups = scene.node_groups(node_id);
1622 assert_eq!(groups.len(), 2);
1623 assert!(groups.contains(&g1));
1624 assert!(groups.contains(&g2));
1625 }
1626
1627 #[test]
1628 fn test_remove_node_cleans_up_group_membership() {
1629 let mut scene = Scene::new();
1630 let gid = scene.create_group("G");
1631 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1632 scene.add_to_group(node_id, gid);
1633 scene.remove(node_id);
1634 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1635 }
1636
1637 #[test]
1640 fn test_mesh_ref_count_zero_for_unused_mesh() {
1641 let scene = Scene::new();
1642 assert_eq!(scene.mesh_ref_count(MeshId(42)), 0);
1643 }
1644
1645 #[test]
1646 fn test_mesh_ref_count_correct_for_nodes() {
1647 let mut scene = Scene::new();
1648 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1649 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1650 scene.add(Some(MeshId(1)), glam::Mat4::IDENTITY, Material::default());
1651 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1652 assert_eq!(scene.mesh_ref_count(MeshId(1)), 1);
1653 }
1654
1655 #[test]
1656 fn test_mesh_ref_count_decreases_after_remove() {
1657 let mut scene = Scene::new();
1658 let node_a = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1659 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1660 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1661 scene.remove(node_a);
1662 assert_eq!(scene.mesh_ref_count(MeshId(0)), 1);
1663 }
1664
1665 #[test]
1666 fn test_remove_group() {
1667 let mut scene = Scene::new();
1668 let gid = scene.create_group("G");
1669 scene.remove_group(gid);
1670 assert!(scene.get_group(gid).is_none());
1671 assert!(scene.groups().is_empty());
1672 }
1673
1674 #[test]
1675 fn test_walk_depth_first_order() {
1676 let mut scene = Scene::new();
1677 let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1678 let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1679 let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1680 let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1681 scene.set_parent(child_a, Some(root));
1682 scene.set_parent(child_b, Some(root));
1683 scene.set_parent(grandchild, Some(child_a));
1684
1685 let walk = scene.walk_depth_first();
1686 assert_eq!(walk.len(), 4);
1687 assert_eq!(walk[0], (root, 0));
1688 assert_eq!(walk[1], (child_a, 1));
1689 assert_eq!(walk[2], (grandchild, 2));
1690 assert_eq!(walk[3], (child_b, 1));
1691 }
1692
1693 fn make_test_frustum() -> crate::camera::frustum::Frustum {
1696 let view = glam::Mat4::look_at_rh(
1697 glam::Vec3::new(0.0, 0.0, 300.0),
1698 glam::Vec3::ZERO,
1699 glam::Vec3::Y,
1700 );
1701 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 1.0, 400.0);
1702 crate::camera::frustum::Frustum::from_view_proj(&(proj * view))
1703 }
1704
1705 fn unit_aabb() -> crate::scene::aabb::Aabb {
1706 crate::scene::aabb::Aabb {
1707 min: glam::Vec3::splat(-0.5),
1708 max: glam::Vec3::splat(0.5),
1709 }
1710 }
1711
1712 #[test]
1717 fn test_octree_parity_with_known_counts() {
1718 let mut scene = Scene::new();
1719 let frustum = make_test_frustum();
1720
1721 for _ in 0..100 {
1723 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1724 }
1725 for _ in 0..500 {
1727 let mat = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
1728 scene.add(Some(MeshId(0)), mat, Material::default());
1729 }
1730
1731 assert!(scene.node_count() >= SPATIAL_THRESHOLD);
1732
1733 let sel = Selection::new();
1734 let (items, stats) =
1735 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1736
1737 assert_eq!(items.len(), 100, "expected 100 visible items, got {}", items.len());
1738 assert_eq!(stats.visible, 100);
1739 let scene_stats = scene.scene_stats();
1740 assert!(scene_stats.spatial_index_nodes > 0, "spatial index should be active");
1741 }
1742
1743 #[test]
1746 fn test_octree_incremental_transform_update() {
1747 let mut scene = Scene::new();
1748 let frustum = make_test_frustum();
1749
1750 let mut ids = Vec::new();
1751 for _ in 0..600 {
1753 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1754 ids.push(id);
1755 }
1756
1757 let sel = Selection::new();
1758 let (items, _) =
1759 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1760 assert_eq!(items.len(), 600, "all 600 should be visible initially");
1761
1762 let behind = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
1764 for &id in &ids[..300] {
1765 scene.set_local_transform(id, behind);
1766 }
1767
1768 let (items2, _) =
1769 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1770 assert_eq!(
1771 items2.len(),
1772 300,
1773 "300 nodes should be visible after moving the others out"
1774 );
1775 }
1776
1777 #[test]
1779 fn test_octree_node_removal() {
1780 let mut scene = Scene::new();
1781 let frustum = make_test_frustum();
1782
1783 let mut ids = Vec::new();
1784 for _ in 0..600 {
1785 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1786 ids.push(id);
1787 }
1788
1789 let sel = Selection::new();
1790 let (items, _) =
1791 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1792 assert_eq!(items.len(), 600);
1793
1794 for &id in &ids[..150] {
1796 scene.remove(id);
1797 }
1798
1799 let (items2, _) =
1800 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1801 assert_eq!(items2.len(), 450, "should have 450 after removing 150");
1802 }
1803}