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