1use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use crate::interaction::selection::{NodeId, Selection};
12use crate::renderer::{PickId, SceneRenderItem};
13use crate::resources::mesh_store::MeshId;
14use crate::scene::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 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
242 .transform_vector3(glam::Vec3::from(*direction))
243 .normalize();
244 LightKind::Directional {
245 direction: rotated.into(),
246 }
247 }
248 LightKind::Point { range, .. } => LightKind::Point {
249 position: translation.into(),
250 range: *range,
251 },
252 LightKind::Spot {
253 direction,
254 range,
255 inner_angle,
256 outer_angle,
257 ..
258 } => {
259 let rotated = world
260 .transform_vector3(glam::Vec3::from(*direction))
261 .normalize();
262 LightKind::Spot {
263 position: translation.into(),
264 direction: rotated.into(),
265 range: *range,
266 inner_angle: *inner_angle,
267 outer_angle: *outer_angle,
268 }
269 }
270 };
271 LightSource {
272 kind,
273 colour: src.colour,
274 intensity: src.intensity,
275 importance: src.importance,
276 }
277}
278
279const DEFAULT_LAYER: LayerId = LayerId(0);
281
282pub struct Scene {
284 nodes: HashMap<NodeId, SceneNode>,
285 roots: Vec<NodeId>,
286 layers: Vec<Layer>,
287 next_id: u64,
288 next_layer_id: u32,
289 groups: Vec<Group>,
290 next_group_id: u32,
291 version: u64,
294 spatial: SpatialIndex,
295 spatial_built: bool,
297 last_scene_stats: SceneStats,
298 live_decals: Vec<LiveDecal>,
300 next_decal_id: u64,
301}
302
303static SCENE_VERSION_CLOCK: AtomicU64 = AtomicU64::new(0);
310
311impl Scene {
312 pub fn new() -> Self {
314 let base = SCENE_VERSION_CLOCK.fetch_add(1 << 20, Ordering::Relaxed);
317 Self {
318 nodes: HashMap::new(),
319 roots: Vec::new(),
320 layers: vec![Layer {
321 id: DEFAULT_LAYER,
322 name: "Default".to_string(),
323 visible: true,
324 locked: false,
325 colour: [1.0, 1.0, 1.0, 1.0],
326 order: 0,
327 }],
328 next_id: 1,
329 next_layer_id: 1,
330 groups: Vec::new(),
331 next_group_id: 0,
332 version: base,
333 spatial: SpatialIndex::new(),
334 spatial_built: false,
335 last_scene_stats: SceneStats::default(),
336 live_decals: Vec::new(),
337 next_decal_id: 0,
338 }
339 }
340
341 pub fn version(&self) -> u64 {
346 self.version
347 }
348
349 pub fn add(
353 &mut self,
354 mesh_id: Option<MeshId>,
355 transform: glam::Mat4,
356 material: Material,
357 ) -> NodeId {
358 self.add_named("", mesh_id, transform, material)
359 }
360
361 pub fn add_named(
363 &mut self,
364 name: &str,
365 mesh_id: Option<MeshId>,
366 transform: glam::Mat4,
367 material: Material,
368 ) -> NodeId {
369 let id = self.next_id;
370 self.next_id += 1;
371 let node = SceneNode {
372 id,
373 name: name.to_string(),
374 mesh_id,
375 material,
376 appearance: crate::scene::material::ItemSettings::default(),
377 visible: true,
378 show_normals: false,
379 local_transform: transform,
380 world_transform: transform,
381 parent: None,
382 children: Vec::new(),
383 layer: DEFAULT_LAYER,
384 dirty: true,
385 deform_instance: None,
386 receives_decals: true,
387 light: None,
388 };
389 self.nodes.insert(id, node);
390 self.roots.push(id);
391 self.version = self.version.wrapping_add(1);
392 if self.spatial_built {
393 self.spatial.mark_dirty(id);
394 }
395 id
396 }
397
398 pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
401 let mut removed = Vec::new();
402 self.remove_recursive(id, &mut removed);
403
404 if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
406 if let Some(parent) = self.nodes.get_mut(&parent_id) {
407 parent.children.retain(|c| *c != id);
408 }
409 } else {
410 self.roots.retain(|r| *r != id);
411 }
412
413 if self.spatial_built {
415 for &rid in &removed {
416 self.spatial.remove_node(rid);
417 }
418 }
419
420 for &rid in &removed {
422 self.nodes.remove(&rid);
423 }
424 self.roots.retain(|r| !removed.contains(r));
426
427 for group in &mut self.groups {
429 for &rid in &removed {
430 group.members.remove(&rid);
431 }
432 }
433
434 self.version = self.version.wrapping_add(1);
435 removed
436 }
437
438 fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
439 out.push(id);
440 if let Some(node) = self.nodes.get(&id) {
441 for &child in &node.children {
442 self.remove_recursive(child, out);
443 }
444 }
445 }
446
447 pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
451 let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
453 if let Some(old_pid) = old_parent {
454 if let Some(old_p) = self.nodes.get_mut(&old_pid) {
455 old_p.children.retain(|c| *c != child_id);
456 }
457 } else {
458 self.roots.retain(|r| *r != child_id);
459 }
460
461 if let Some(new_pid) = new_parent {
463 if let Some(new_p) = self.nodes.get_mut(&new_pid) {
464 new_p.children.push(child_id);
465 }
466 } else {
467 self.roots.push(child_id);
468 }
469
470 if let Some(node) = self.nodes.get_mut(&child_id) {
471 node.parent = new_parent;
472 node.dirty = true;
473 }
474 self.version = self.version.wrapping_add(1);
475 if self.spatial_built {
476 self.mark_spatial_dirty_subtree(child_id);
477 }
478 }
479
480 pub fn children(&self, id: NodeId) -> &[NodeId] {
482 self.nodes
483 .get(&id)
484 .map(|n| n.children.as_slice())
485 .unwrap_or(&[])
486 }
487
488 pub fn parent(&self, id: NodeId) -> Option<NodeId> {
490 self.nodes.get(&id).and_then(|n| n.parent)
491 }
492
493 pub fn roots(&self) -> &[NodeId] {
495 &self.roots
496 }
497
498 pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
502 if let Some(node) = self.nodes.get_mut(&id) {
503 node.local_transform = transform;
504 node.dirty = true;
505 }
506 self.mark_descendants_dirty(id);
507 self.version = self.version.wrapping_add(1);
508 if self.spatial_built {
509 self.mark_spatial_dirty_subtree(id);
510 }
511 }
512
513 pub fn set_visible(&mut self, id: NodeId, visible: bool) {
515 if let Some(node) = self.nodes.get_mut(&id) {
516 node.visible = visible;
517 }
518 self.version = self.version.wrapping_add(1);
519 if self.spatial_built {
520 self.spatial.mark_dirty(id);
521 }
522 }
523
524 pub fn set_material(&mut self, id: NodeId, material: Material) {
526 if let Some(node) = self.nodes.get_mut(&id) {
527 node.material = material;
528 }
529 self.version = self.version.wrapping_add(1);
530 }
531
532 pub fn set_appearance(&mut self, id: NodeId, appearance: crate::scene::material::ItemSettings) {
534 if let Some(node) = self.nodes.get_mut(&id) {
535 node.appearance = appearance;
536 }
537 self.version = self.version.wrapping_add(1);
538 }
539
540 pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
542 if let Some(node) = self.nodes.get_mut(&id) {
543 node.mesh_id = mesh_id;
544 }
545 self.version = self.version.wrapping_add(1);
546 if self.spatial_built {
547 self.spatial.mark_dirty(id);
548 }
549 }
550
551 pub fn set_name(&mut self, id: NodeId, name: &str) {
553 if let Some(node) = self.nodes.get_mut(&id) {
554 node.name = name.to_string();
555 }
556 self.version = self.version.wrapping_add(1);
557 }
558
559 pub fn set_deform_instance(&mut self, id: NodeId, instance: Option<u32>) {
565 if let Some(node) = self.nodes.get_mut(&id) {
566 node.deform_instance = instance;
567 }
568 self.version = self.version.wrapping_add(1);
569 }
570
571 pub fn set_receives_decals(&mut self, id: NodeId, v: bool) {
573 if let Some(node) = self.nodes.get_mut(&id) {
574 node.receives_decals = v;
575 }
576 self.version = self.version.wrapping_add(1);
577 }
578
579 pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
581 if let Some(node) = self.nodes.get_mut(&id) {
582 node.show_normals = show;
583 }
584 self.version = self.version.wrapping_add(1);
585 }
586
587 pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
589 if let Some(node) = self.nodes.get_mut(&id) {
590 node.layer = layer;
591 }
592 self.version = self.version.wrapping_add(1);
593 }
594
595 pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
597 self.nodes.get(&id)
598 }
599
600 pub fn node_count(&self) -> usize {
602 self.nodes.len()
603 }
604
605 pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
607 self.nodes.values()
608 }
609
610 pub fn add_layer(&mut self, name: &str) -> LayerId {
614 let id = LayerId(self.next_layer_id);
615 let order = self.next_layer_id;
616 self.next_layer_id += 1;
617 self.layers.push(Layer {
618 id,
619 name: name.to_string(),
620 visible: true,
621 locked: false,
622 colour: [1.0, 1.0, 1.0, 1.0],
623 order,
624 });
625 self.version = self.version.wrapping_add(1);
626 id
627 }
628
629 pub fn remove_layer(&mut self, id: LayerId) {
632 if id == DEFAULT_LAYER {
633 return;
634 }
635 for node in self.nodes.values_mut() {
637 if node.layer == id {
638 node.layer = DEFAULT_LAYER;
639 }
640 }
641 self.layers.retain(|l| l.id != id);
642 self.version = self.version.wrapping_add(1);
643 }
644
645 pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
647 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
648 layer.visible = visible;
649 }
650 self.version = self.version.wrapping_add(1);
651 }
652
653 pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
655 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
656 layer.locked = locked;
657 }
658 self.version = self.version.wrapping_add(1);
659 }
660
661 pub fn set_layer_colour(&mut self, id: LayerId, colour: [f32; 4]) {
663 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
664 layer.colour = colour;
665 }
666 self.version = self.version.wrapping_add(1);
667 }
668
669 pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
671 if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
672 layer.order = order;
673 }
674 self.version = self.version.wrapping_add(1);
675 }
676
677 pub fn is_layer_locked(&self, id: LayerId) -> bool {
679 self.layers
680 .iter()
681 .find(|l| l.id == id)
682 .map(|l| l.locked)
683 .unwrap_or(false)
684 }
685
686 pub fn layers(&self) -> Vec<&Layer> {
688 let mut sorted: Vec<&Layer> = self.layers.iter().collect();
689 sorted.sort_by_key(|l| l.order);
690 sorted
691 }
692
693 pub fn create_group(&mut self, name: &str) -> GroupId {
697 let id = GroupId(self.next_group_id);
698 self.next_group_id += 1;
699 self.groups.push(Group {
700 id,
701 name: name.to_string(),
702 members: HashSet::new(),
703 });
704 self.version = self.version.wrapping_add(1);
705 id
706 }
707
708 pub fn remove_group(&mut self, id: GroupId) {
710 self.groups.retain(|g| g.id != id);
711 self.version = self.version.wrapping_add(1);
712 }
713
714 pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
716 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
717 g.members.insert(node);
718 }
719 self.version = self.version.wrapping_add(1);
720 }
721
722 pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
724 if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
725 g.members.remove(&node);
726 }
727 self.version = self.version.wrapping_add(1);
728 }
729
730 pub fn get_group(&self, id: GroupId) -> Option<&Group> {
732 self.groups.iter().find(|g| g.id == id)
733 }
734
735 pub fn groups(&self) -> &[Group] {
737 &self.groups
738 }
739
740 pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
742 self.groups
743 .iter()
744 .filter(|g| g.members.contains(&node))
745 .map(|g| g.id)
746 .collect()
747 }
748
749 pub fn update_transforms(&mut self) {
753 let roots: Vec<NodeId> = self.roots.clone();
756 for &root_id in &roots {
757 self.propagate_transform(root_id, glam::Mat4::IDENTITY);
758 }
759 }
760
761 fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
762 let (dirty, local, children) = {
763 let Some(node) = self.nodes.get(&id) else {
764 return;
765 };
766 (node.dirty, node.local_transform, node.children.clone())
767 };
768
769 if dirty {
770 let world = parent_world * local;
771 let node = self.nodes.get_mut(&id).unwrap();
772 node.world_transform = world;
773 node.dirty = false;
774 for &child_id in &children {
776 self.mark_dirty(child_id);
777 self.propagate_transform(child_id, world);
778 }
779 } else {
780 let world = self.nodes[&id].world_transform;
781 for &child_id in &children {
782 self.propagate_transform(child_id, world);
783 }
784 }
785 }
786
787 fn mark_dirty(&mut self, id: NodeId) {
788 if let Some(node) = self.nodes.get_mut(&id) {
789 node.dirty = true;
790 }
791 }
792
793 fn mark_descendants_dirty(&mut self, id: NodeId) {
794 let children = self
795 .nodes
796 .get(&id)
797 .map(|n| n.children.clone())
798 .unwrap_or_default();
799 for child_id in children {
800 self.mark_dirty(child_id);
801 self.mark_descendants_dirty(child_id);
802 }
803 }
804
805 fn mark_spatial_dirty_subtree(&mut self, id: NodeId) {
807 self.spatial.mark_dirty(id);
808 let children = self
809 .nodes
810 .get(&id)
811 .map(|n| n.children.clone())
812 .unwrap_or_default();
813 for child_id in children {
814 self.mark_spatial_dirty_subtree(child_id);
815 }
816 }
817
818 fn build_spatial_index_from(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
821 let entries: Vec<(NodeId, Aabb)> = self
822 .nodes
823 .values()
824 .filter(|n| n.visible && n.mesh_id.is_some())
825 .filter_map(|n| {
826 let local_aabb = mesh_aabb_fn(n.mesh_id.unwrap())?;
827 let world_aabb = local_aabb.transformed(&n.world_transform);
828 Some((n.id, world_aabb))
829 })
830 .collect();
831 self.spatial.rebuild(&entries);
832 self.spatial_built = true;
833 }
834
835 fn flush_spatial_dirty(&mut self, mesh_aabb_fn: &impl Fn(MeshId) -> Option<Aabb>) {
838 let dirty = self.spatial.drain_dirty();
839 for id in dirty {
840 self.spatial.remove_entry(id);
841 let new_aabb = self.nodes.get(&id).and_then(|node| {
842 if !node.visible {
843 return None;
844 }
845 let mesh_id = node.mesh_id?;
846 let local_aabb = mesh_aabb_fn(mesh_id)?;
847 Some(local_aabb.transformed(&node.world_transform))
848 });
849 if let Some(aabb) = new_aabb {
850 self.spatial.insert_entry(id, aabb);
851 }
852 }
853 }
854
855 pub fn rebuild_spatial_index(&mut self, mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>) {
863 self.update_transforms();
864 self.build_spatial_index_from(&mesh_aabb_fn);
865 }
866
867 pub fn scene_stats(&self) -> SceneStats {
869 self.last_scene_stats
870 }
871
872 pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
879 self.update_transforms();
880
881 let layer_visible: HashMap<LayerId, bool> =
882 self.layers.iter().map(|l| (l.id, l.visible)).collect();
883
884 let layer_locked: HashMap<LayerId, bool> =
885 self.layers.iter().map(|l| (l.id, l.locked)).collect();
886
887 let mut items = Vec::new();
888 for node in self.nodes.values() {
889 if !node.visible {
890 continue;
891 }
892 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
893 continue;
894 }
895 let Some(mesh_id) = node.mesh_id else {
896 continue;
897 };
898 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
899 let mut item_settings = node.appearance;
900 item_settings.pick_id = PickId(node.id);
901 item_settings.selected = if locked {
902 false
903 } else {
904 selection.contains(node.id)
905 };
906 items.push(SceneRenderItem {
907 mesh_id,
908 model: node.world_transform.to_cols_array_2d(),
909 settings: item_settings,
910 show_normals: node.show_normals,
911 material: node.material,
912 active_attribute: None,
913 scalar_range: None,
914 colourmap_id: None,
915 nan_colour: None,
916 warp_attribute: None,
917 warp_scale: 1.0,
918 deform_instance: node.deform_instance,
919 receives_decals: node.receives_decals,
920 lic: None,
921 });
922 }
923 items
924 }
925
926 pub fn collect_render_items_culled(
936 &mut self,
937 selection: &Selection,
938 frustum: &crate::camera::frustum::Frustum,
939 mesh_aabb_fn: impl Fn(MeshId) -> Option<Aabb>,
940 ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
941 self.update_transforms();
942
943 let layer_visible: HashMap<LayerId, bool> =
944 self.layers.iter().map(|l| (l.id, l.visible)).collect();
945 let layer_locked: HashMap<LayerId, bool> =
946 self.layers.iter().map(|l| (l.id, l.locked)).collect();
947
948 let mut items = Vec::new();
949 let mut stats = crate::camera::frustum::CullStats::default();
950
951 if self.nodes.len() >= SPATIAL_THRESHOLD {
952 if !self.spatial_built {
954 self.build_spatial_index_from(&mesh_aabb_fn);
955 } else {
956 self.flush_spatial_dirty(&mesh_aabb_fn);
957 }
958
959 let t0 = std::time::Instant::now();
960 let mut candidate_ids = Vec::new();
961 self.spatial
962 .collect_visible(frustum, &mut candidate_ids, &mut stats);
963 let traversal_ms = t0.elapsed().as_secs_f32() * 1000.0;
964
965 for id in candidate_ids {
966 let Some(node) = self.nodes.get(&id) else {
967 continue;
968 };
969 if !node.visible {
970 continue;
971 }
972 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
973 continue;
974 }
975 let Some(mesh_id) = node.mesh_id else {
976 continue;
977 };
978 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
979 let mut item_settings = node.appearance;
980 item_settings.pick_id = PickId(node.id);
981 item_settings.selected = if locked {
982 false
983 } else {
984 selection.contains(node.id)
985 };
986 items.push(SceneRenderItem {
987 mesh_id,
988 model: node.world_transform.to_cols_array_2d(),
989 settings: item_settings,
990 show_normals: node.show_normals,
991 material: node.material,
992 active_attribute: None,
993 scalar_range: None,
994 colourmap_id: None,
995 nan_colour: None,
996 warp_attribute: None,
997 warp_scale: 1.0,
998 deform_instance: node.deform_instance,
999 receives_decals: node.receives_decals,
1000 lic: None,
1001 });
1002 }
1003
1004 self.last_scene_stats = SceneStats {
1005 spatial_index_nodes: self.spatial.node_count(),
1006 frustum_cull_traversal_ms: traversal_ms,
1007 };
1008 } else {
1009 let t0 = std::time::Instant::now();
1011
1012 for node in self.nodes.values() {
1013 if !node.visible {
1014 continue;
1015 }
1016 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1017 continue;
1018 }
1019 let Some(mesh_id) = node.mesh_id else {
1020 continue;
1021 };
1022
1023 stats.total += 1;
1024
1025 if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
1027 let world_aabb = local_aabb.transformed(&node.world_transform);
1028 if frustum.cull_aabb(&world_aabb) {
1029 stats.culled += 1;
1030 continue;
1031 }
1032 }
1033
1034 let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
1035 stats.visible += 1;
1036 let mut item_settings = node.appearance;
1037 item_settings.pick_id = PickId(node.id);
1038 item_settings.selected = if locked {
1039 false
1040 } else {
1041 selection.contains(node.id)
1042 };
1043 items.push(SceneRenderItem {
1044 mesh_id,
1045 model: node.world_transform.to_cols_array_2d(),
1046 settings: item_settings,
1047 show_normals: node.show_normals,
1048 material: node.material,
1049 active_attribute: None,
1050 scalar_range: None,
1051 colourmap_id: None,
1052 nan_colour: None,
1053 warp_attribute: None,
1054 warp_scale: 1.0,
1055 deform_instance: node.deform_instance,
1056 receives_decals: node.receives_decals,
1057 lic: None,
1058 });
1059 }
1060
1061 self.last_scene_stats = SceneStats {
1062 spatial_index_nodes: 0,
1063 frustum_cull_traversal_ms: t0.elapsed().as_secs_f32() * 1000.0,
1064 };
1065 }
1066
1067 (items, stats)
1068 }
1069
1070 pub fn add_light(&mut self, light: LightSource) -> NodeId {
1081 let id = self.next_id;
1082 self.next_id += 1;
1083 let node = SceneNode {
1084 id,
1085 name: String::new(),
1086 mesh_id: None,
1087 material: crate::scene::material::Material::default(),
1088 appearance: crate::scene::material::ItemSettings::default(),
1089 visible: true,
1090 show_normals: false,
1091 local_transform: glam::Mat4::IDENTITY,
1092 world_transform: glam::Mat4::IDENTITY,
1093 parent: None,
1094 children: Vec::new(),
1095 layer: DEFAULT_LAYER,
1096 dirty: true,
1097 deform_instance: None,
1098 receives_decals: false,
1099 light: Some(light),
1100 };
1101 self.nodes.insert(id, node);
1102 self.roots.push(id);
1103 self.version = self.version.wrapping_add(1);
1104 id
1105 }
1106
1107 pub fn collect_lights(&mut self) -> Vec<LightSource> {
1117 self.update_transforms();
1118
1119 let layer_visible: HashMap<LayerId, bool> =
1120 self.layers.iter().map(|l| (l.id, l.visible)).collect();
1121
1122 let mut out = Vec::new();
1123 for node in self.nodes.values() {
1124 if !node.visible {
1125 continue;
1126 }
1127 if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
1128 continue;
1129 }
1130 let Some(src) = &node.light else {
1131 continue;
1132 };
1133 out.push(resolve_light_to_world(src, node.world_transform));
1134 }
1135 out
1136 }
1137
1138 pub fn set_light(&mut self, id: NodeId, light: Option<LightSource>) {
1140 if let Some(node) = self.nodes.get_mut(&id) {
1141 node.light = light;
1142 }
1143 self.version = self.version.wrapping_add(1);
1144 }
1145
1146 pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
1154 self.nodes
1155 .values()
1156 .filter(|n| n.mesh_id == Some(mesh_id))
1157 .count()
1158 }
1159
1160 pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
1164 let mut result = Vec::new();
1165 for &root_id in &self.roots {
1166 self.walk_recursive(root_id, 0, &mut result);
1167 }
1168 result
1169 }
1170
1171 fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
1172 out.push((id, depth));
1173 if let Some(node) = self.nodes.get(&id) {
1174 for &child_id in &node.children {
1175 self.walk_recursive(child_id, depth + 1, out);
1176 }
1177 }
1178 }
1179}
1180
1181impl Default for Scene {
1182 fn default() -> Self {
1183 Self::new()
1184 }
1185}
1186
1187#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1195pub struct DecalHandle(u64);
1196
1197pub struct LiveDecal {
1204 id: u64,
1205 pub item: crate::renderer::DecalItem,
1208 pub animation: Option<crate::renderer::DecalAnimation>,
1210 pub lifetime: Option<f32>,
1212 pub fade_duration: f32,
1215 pub age: f32,
1217}
1218
1219impl Scene {
1220 pub fn add_decal(&mut self, item: crate::renderer::DecalItem) -> DecalHandle {
1222 self.add_live_decal(item, None, None, 0.0)
1223 }
1224
1225 pub fn add_decal_with_lifetime(
1230 &mut self,
1231 item: crate::renderer::DecalItem,
1232 lifetime: f32,
1233 fade_duration: f32,
1234 ) {
1235 self.add_live_decal(item, Some(lifetime), None, fade_duration);
1236 }
1237
1238 pub fn add_decal_animated(
1240 &mut self,
1241 item: crate::renderer::DecalItem,
1242 animation: crate::renderer::DecalAnimation,
1243 lifetime: Option<f32>,
1244 ) -> DecalHandle {
1245 self.add_live_decal(item, lifetime, Some(animation), 0.0)
1246 }
1247
1248 pub fn remove_decal(&mut self, handle: DecalHandle) {
1250 self.live_decals.retain(|d| d.id != handle.0);
1251 }
1252
1253 pub fn update_decals(&mut self, dt: f32) {
1257 for ld in &mut self.live_decals {
1258 ld.age += dt;
1259 }
1260 self.live_decals
1261 .retain(|ld| ld.lifetime.map_or(true, |lt| ld.age < lt));
1262 }
1263
1264 pub fn collect_decal_items(&self) -> Vec<crate::renderer::DecalItem> {
1269 self.live_decals
1270 .iter()
1271 .map(|ld| {
1272 let mut item = ld.item.clone();
1273
1274 if let Some(lt) = ld.lifetime {
1276 let fade = if ld.fade_duration > 0.0 {
1277 ld.fade_duration.min(lt)
1278 } else {
1279 lt * 0.2
1280 };
1281 let time_left = lt - ld.age;
1282 if time_left < fade {
1283 item.alpha *= (time_left / fade).clamp(0.0, 1.0);
1284 }
1285 }
1286
1287 if let Some(anim) = &ld.animation {
1289 match anim {
1290 crate::renderer::DecalAnimation::UvScroll { vx, vy } => {
1291 item.uv_offset[0] =
1293 (ld.item.uv_offset[0] + vx * ld.age).rem_euclid(1.0);
1294 item.uv_offset[1] =
1295 (ld.item.uv_offset[1] + vy * ld.age).rem_euclid(1.0);
1296 }
1297 crate::renderer::DecalAnimation::SpriteSheet { cols, rows, fps } => {
1298 let total = cols * rows;
1299 let frame = ((ld.age * fps) as u32).rem_euclid(total.max(1));
1300 let col = frame % cols;
1301 let row = frame / cols;
1302 item.uv_scale = [1.0 / *cols as f32, 1.0 / *rows as f32];
1303 item.uv_offset = [col as f32 / *cols as f32, row as f32 / *rows as f32];
1304 }
1305 }
1306 }
1307
1308 item
1309 })
1310 .collect()
1311 }
1312
1313 fn add_live_decal(
1314 &mut self,
1315 item: crate::renderer::DecalItem,
1316 lifetime: Option<f32>,
1317 animation: Option<crate::renderer::DecalAnimation>,
1318 fade_duration: f32,
1319 ) -> DecalHandle {
1320 let id = self.next_decal_id;
1321 self.next_decal_id += 1;
1322 self.live_decals.push(LiveDecal {
1323 id,
1324 item,
1325 animation,
1326 lifetime,
1327 fade_duration,
1328 age: 0.0,
1329 });
1330 DecalHandle(id)
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337
1338 #[test]
1339 fn test_add_and_remove() {
1340 let mut scene = Scene::new();
1341 let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1342 assert!(scene.node(id).is_some());
1343 assert_eq!(scene.node_count(), 1);
1344
1345 let removed = scene.remove(id);
1346 assert_eq!(removed, vec![id]);
1347 assert!(scene.node(id).is_none());
1348 assert_eq!(scene.node_count(), 0);
1349 }
1350
1351 #[test]
1352 fn test_remove_cascades_to_children() {
1353 let mut scene = Scene::new();
1354 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1355 let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1356 let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1357 scene.set_parent(child1, Some(parent));
1358 scene.set_parent(child2, Some(parent));
1359
1360 let removed = scene.remove(parent);
1361 assert_eq!(removed.len(), 3);
1362 assert!(removed.contains(&parent));
1363 assert!(removed.contains(&child1));
1364 assert!(removed.contains(&child2));
1365 assert_eq!(scene.node_count(), 0);
1366 }
1367
1368 #[test]
1369 fn test_set_parent_updates_world_transform() {
1370 let mut scene = Scene::new();
1371 let parent = scene.add(
1372 None,
1373 glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
1374 Material::default(),
1375 );
1376 let child = scene.add(
1377 None,
1378 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1379 Material::default(),
1380 );
1381 scene.set_parent(child, Some(parent));
1382 scene.update_transforms();
1383
1384 let world = scene.node(child).unwrap().world_transform();
1385 let pos = world.col(3).truncate();
1386 assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
1387 }
1388
1389 #[test]
1390 fn test_dirty_propagation() {
1391 let mut scene = Scene::new();
1392 let parent = scene.add(
1393 None,
1394 glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
1395 Material::default(),
1396 );
1397 let child = scene.add(
1398 None,
1399 glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
1400 Material::default(),
1401 );
1402 scene.set_parent(child, Some(parent));
1403 scene.update_transforms();
1404
1405 scene.set_local_transform(
1407 parent,
1408 glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
1409 );
1410 scene.update_transforms();
1411
1412 let child_pos = scene
1413 .node(child)
1414 .unwrap()
1415 .world_transform()
1416 .col(3)
1417 .truncate();
1418 assert!(
1419 (child_pos.x - 12.0).abs() < 1e-5,
1420 "expected x=12.0, got {}",
1421 child_pos.x
1422 );
1423 }
1424
1425 #[test]
1426 fn test_layer_visibility_hides_nodes() {
1427 let mut scene = Scene::new();
1428 let layer = scene.add_layer("Hidden");
1429 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1430 scene.set_layer(id, layer);
1431 scene.set_layer_visible(layer, false);
1432
1433 let items = scene.collect_render_items(&Selection::new());
1434 assert!(items.is_empty());
1435 }
1436
1437 #[test]
1438 fn test_collect_skips_invisible_nodes() {
1439 let mut scene = Scene::new();
1440 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1441 scene.set_visible(id, false);
1442
1443 let items = scene.collect_render_items(&Selection::new());
1444 assert!(items.is_empty());
1445 }
1446
1447 #[test]
1448 fn test_collect_skips_meshless_nodes() {
1449 let mut scene = Scene::new();
1450 scene.add(None, glam::Mat4::IDENTITY, Material::default());
1451
1452 let items = scene.collect_render_items(&Selection::new());
1453 assert!(items.is_empty());
1454 }
1455
1456 #[test]
1457 fn test_collect_marks_selected() {
1458 let mut scene = Scene::new();
1459 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1460
1461 let mut sel = Selection::new();
1462 sel.select_one(id);
1463
1464 let items = scene.collect_render_items(&sel);
1465 assert_eq!(items.len(), 1);
1466 assert!(items[0].settings.selected);
1467 }
1468
1469 #[test]
1470 fn test_unparent_makes_root() {
1471 let mut scene = Scene::new();
1472 let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1473 let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1474 scene.set_parent(child, Some(parent));
1475 assert!(!scene.roots().contains(&child));
1476
1477 scene.set_parent(child, None);
1478 assert!(scene.roots().contains(&child));
1479 assert!(scene.node(child).unwrap().parent().is_none());
1480 }
1481
1482 #[test]
1483 fn test_collect_culled_filters_offscreen() {
1484 let mut scene = Scene::new();
1485 let visible_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1487 let _behind = scene.add(
1489 Some(MeshId(1)),
1490 glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
1491 Material::default(),
1492 );
1493
1494 let sel = Selection::new();
1495 let view = glam::Mat4::look_at_rh(
1497 glam::Vec3::new(0.0, 0.0, 5.0),
1498 glam::Vec3::ZERO,
1499 glam::Vec3::Y,
1500 );
1501 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
1502 let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
1503
1504 let unit_aabb = crate::scene::aabb::Aabb {
1506 min: glam::Vec3::splat(-0.5),
1507 max: glam::Vec3::splat(0.5),
1508 };
1509
1510 let (items, stats) =
1511 scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
1512
1513 assert_eq!(stats.total, 2);
1514 assert_eq!(stats.visible, 1);
1515 assert_eq!(stats.culled, 1);
1516 assert_eq!(items.len(), 1);
1517 assert_eq!(items[0].mesh_id.index(), visible_id as usize - 1); let _ = visible_id; }
1521
1522 #[test]
1525 fn test_layer_locked_field_default_false() {
1526 let scene = Scene::new();
1527 let layers = scene.layers();
1528 let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
1529 assert!(!default_layer.locked);
1530 }
1531
1532 #[test]
1533 fn test_add_layer_has_locked_false_colour_white_and_order() {
1534 let mut scene = Scene::new();
1535 let layer_id = scene.add_layer("Test");
1536 let layers = scene.layers();
1537 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1538 assert!(!layer.locked);
1539 assert_eq!(layer.colour, [1.0, 1.0, 1.0, 1.0]);
1540 assert!(layer.order > 0); }
1542
1543 #[test]
1544 fn test_set_layer_locked() {
1545 let mut scene = Scene::new();
1546 let layer_id = scene.add_layer("Locked");
1547 scene.set_layer_locked(layer_id, true);
1548 assert!(scene.is_layer_locked(layer_id));
1549 scene.set_layer_locked(layer_id, false);
1550 assert!(!scene.is_layer_locked(layer_id));
1551 }
1552
1553 #[test]
1554 fn test_set_layer_colour() {
1555 let mut scene = Scene::new();
1556 let layer_id = scene.add_layer("Coloured");
1557 scene.set_layer_colour(layer_id, [1.0, 0.0, 0.0, 1.0]);
1558 let layers = scene.layers();
1559 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1560 assert_eq!(layer.colour, [1.0, 0.0, 0.0, 1.0]);
1561 }
1562
1563 #[test]
1564 fn test_set_layer_order() {
1565 let mut scene = Scene::new();
1566 let layer_id = scene.add_layer("Orderly");
1567 scene.set_layer_order(layer_id, 99);
1568 let layers = scene.layers();
1569 let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1570 assert_eq!(layer.order, 99);
1571 }
1572
1573 #[test]
1574 fn test_locked_layer_suppresses_selection_in_render_items() {
1575 let mut scene = Scene::new();
1576 let layer_id = scene.add_layer("Locked");
1577 let node_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1578 scene.set_layer(node_id, layer_id);
1579 scene.set_layer_locked(layer_id, true);
1580
1581 let mut sel = Selection::new();
1582 sel.select_one(node_id);
1583
1584 let items = scene.collect_render_items(&sel);
1585 assert_eq!(items.len(), 1, "locked layer nodes still render");
1586 assert!(
1587 !items[0].settings.selected,
1588 "locked layer nodes must not appear selected"
1589 );
1590 }
1591
1592 #[test]
1593 fn test_layers_sorted_by_order() {
1594 let mut scene = Scene::new();
1595 let a = scene.add_layer("A");
1596 let b = scene.add_layer("B");
1597 scene.set_layer_order(a, 10);
1599 scene.set_layer_order(b, 5);
1600 let layers = scene.layers();
1601 let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1603 let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1604 assert!(
1605 pos_b < pos_a,
1606 "layer B (order=5) should appear before A (order=10)"
1607 );
1608 }
1609
1610 #[test]
1613 fn test_create_group_returns_id() {
1614 let mut scene = Scene::new();
1615 let gid = scene.create_group("MyGroup");
1616 let group = scene.get_group(gid).unwrap();
1617 assert_eq!(group.name, "MyGroup");
1618 assert!(group.members.is_empty());
1619 }
1620
1621 #[test]
1622 fn test_add_to_group_and_remove_from_group() {
1623 let mut scene = Scene::new();
1624 let gid = scene.create_group("G");
1625 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1626 scene.add_to_group(node_id, gid);
1627 assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1628 scene.remove_from_group(node_id, gid);
1629 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1630 }
1631
1632 #[test]
1633 fn test_groups_returns_all_groups() {
1634 let mut scene = Scene::new();
1635 scene.create_group("G1");
1636 scene.create_group("G2");
1637 assert_eq!(scene.groups().len(), 2);
1638 }
1639
1640 #[test]
1641 fn test_node_groups_returns_containing_groups() {
1642 let mut scene = Scene::new();
1643 let g1 = scene.create_group("G1");
1644 let g2 = scene.create_group("G2");
1645 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1646 scene.add_to_group(node_id, g1);
1647 scene.add_to_group(node_id, g2);
1648 let groups = scene.node_groups(node_id);
1649 assert_eq!(groups.len(), 2);
1650 assert!(groups.contains(&g1));
1651 assert!(groups.contains(&g2));
1652 }
1653
1654 #[test]
1655 fn test_remove_node_cleans_up_group_membership() {
1656 let mut scene = Scene::new();
1657 let gid = scene.create_group("G");
1658 let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1659 scene.add_to_group(node_id, gid);
1660 scene.remove(node_id);
1661 assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1662 }
1663
1664 #[test]
1667 fn test_mesh_ref_count_zero_for_unused_mesh() {
1668 let scene = Scene::new();
1669 assert_eq!(scene.mesh_ref_count(MeshId(42)), 0);
1670 }
1671
1672 #[test]
1673 fn test_mesh_ref_count_correct_for_nodes() {
1674 let mut scene = Scene::new();
1675 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1676 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1677 scene.add(Some(MeshId(1)), glam::Mat4::IDENTITY, Material::default());
1678 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1679 assert_eq!(scene.mesh_ref_count(MeshId(1)), 1);
1680 }
1681
1682 #[test]
1683 fn test_mesh_ref_count_decreases_after_remove() {
1684 let mut scene = Scene::new();
1685 let node_a = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1686 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1687 assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1688 scene.remove(node_a);
1689 assert_eq!(scene.mesh_ref_count(MeshId(0)), 1);
1690 }
1691
1692 #[test]
1693 fn test_remove_group() {
1694 let mut scene = Scene::new();
1695 let gid = scene.create_group("G");
1696 scene.remove_group(gid);
1697 assert!(scene.get_group(gid).is_none());
1698 assert!(scene.groups().is_empty());
1699 }
1700
1701 #[test]
1702 fn test_walk_depth_first_order() {
1703 let mut scene = Scene::new();
1704 let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1705 let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1706 let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1707 let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1708 scene.set_parent(child_a, Some(root));
1709 scene.set_parent(child_b, Some(root));
1710 scene.set_parent(grandchild, Some(child_a));
1711
1712 let walk = scene.walk_depth_first();
1713 assert_eq!(walk.len(), 4);
1714 assert_eq!(walk[0], (root, 0));
1715 assert_eq!(walk[1], (child_a, 1));
1716 assert_eq!(walk[2], (grandchild, 2));
1717 assert_eq!(walk[3], (child_b, 1));
1718 }
1719
1720 fn make_test_frustum() -> crate::camera::frustum::Frustum {
1723 let view = glam::Mat4::look_at_rh(
1724 glam::Vec3::new(0.0, 0.0, 300.0),
1725 glam::Vec3::ZERO,
1726 glam::Vec3::Y,
1727 );
1728 let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 1.0, 400.0);
1729 crate::camera::frustum::Frustum::from_view_proj(&(proj * view))
1730 }
1731
1732 fn unit_aabb() -> crate::scene::aabb::Aabb {
1733 crate::scene::aabb::Aabb {
1734 min: glam::Vec3::splat(-0.5),
1735 max: glam::Vec3::splat(0.5),
1736 }
1737 }
1738
1739 #[test]
1744 fn test_octree_parity_with_known_counts() {
1745 let mut scene = Scene::new();
1746 let frustum = make_test_frustum();
1747
1748 for _ in 0..100 {
1750 scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1751 }
1752 for _ in 0..500 {
1754 let mat = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
1755 scene.add(Some(MeshId(0)), mat, Material::default());
1756 }
1757
1758 assert!(scene.node_count() >= SPATIAL_THRESHOLD);
1759
1760 let sel = Selection::new();
1761 let (items, stats) =
1762 scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1763
1764 assert_eq!(
1765 items.len(),
1766 100,
1767 "expected 100 visible items, got {}",
1768 items.len()
1769 );
1770 assert_eq!(stats.visible, 100);
1771 let scene_stats = scene.scene_stats();
1772 assert!(
1773 scene_stats.spatial_index_nodes > 0,
1774 "spatial index should be active"
1775 );
1776 }
1777
1778 #[test]
1781 fn test_octree_incremental_transform_update() {
1782 let mut scene = Scene::new();
1783 let frustum = make_test_frustum();
1784
1785 let mut ids = Vec::new();
1786 for _ in 0..600 {
1788 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1789 ids.push(id);
1790 }
1791
1792 let sel = Selection::new();
1793 let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1794 assert_eq!(items.len(), 600, "all 600 should be visible initially");
1795
1796 let behind = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 9000.0));
1798 for &id in &ids[..300] {
1799 scene.set_local_transform(id, behind);
1800 }
1801
1802 let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1803 assert_eq!(
1804 items2.len(),
1805 300,
1806 "300 nodes should be visible after moving the others out"
1807 );
1808 }
1809
1810 #[test]
1812 fn test_octree_node_removal() {
1813 let mut scene = Scene::new();
1814 let frustum = make_test_frustum();
1815
1816 let mut ids = Vec::new();
1817 for _ in 0..600 {
1818 let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1819 ids.push(id);
1820 }
1821
1822 let sel = Selection::new();
1823 let (items, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1824 assert_eq!(items.len(), 600);
1825
1826 for &id in &ids[..150] {
1828 scene.remove(id);
1829 }
1830
1831 let (items2, _) = scene.collect_render_items_culled(&sel, &frustum, |_| Some(unit_aabb()));
1832 assert_eq!(items2.len(), 450, "should have 450 after removing 150");
1833 }
1834}