Skip to main content

viewport_lib/scene/
scene.rs

1//! Scene graph with parent-child hierarchy, layers, and dirty-flag transform propagation.
2//!
3//! `Scene` is a standalone struct that apps own alongside `ViewportRenderer`.
4//! It produces `Vec<SceneRenderItem>` via `collect_render_items()`, which feeds
5//! into `SceneFrame::surfaces` (usually via `SceneFrame::from_surface_items(...)`).
6//! The renderer itself remains stateless.
7
8use std::collections::{HashMap, HashSet};
9
10use crate::interaction::selection::{NodeId, Selection};
11use crate::renderer::SceneRenderItem;
12use crate::resources::mesh_store::MeshId;
13use crate::scene::material::Material;
14use crate::scene::traits::ViewportObject;
15
16// ---------------------------------------------------------------------------
17// Layer
18// ---------------------------------------------------------------------------
19
20/// Opaque layer identifier.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct LayerId(pub u32);
23
24/// A named visibility layer. Nodes belong to exactly one layer.
25pub struct Layer {
26    /// Unique layer identifier.
27    pub id: LayerId,
28    /// Human-readable layer name.
29    pub name: String,
30    /// Whether nodes on this layer are rendered.
31    pub visible: bool,
32    /// When true, nodes on this layer render but cannot appear selected.
33    pub locked: bool,
34    /// Display color for this layer (RGBA, each component 0.0–1.0).
35    pub color: [f32; 4],
36    /// Sort order for layer display. Lower values appear first.
37    pub order: u32,
38}
39
40// ---------------------------------------------------------------------------
41// Group
42// ---------------------------------------------------------------------------
43
44/// Opaque group identifier.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct GroupId(pub u32);
47
48/// A named group of scene nodes. Membership is independent of parent-child hierarchy.
49pub struct Group {
50    /// Unique group identifier.
51    pub id: GroupId,
52    /// Human-readable group name.
53    pub name: String,
54    /// Set of node IDs belonging to this group.
55    pub members: HashSet<NodeId>,
56}
57
58// ---------------------------------------------------------------------------
59// SceneNode
60// ---------------------------------------------------------------------------
61
62/// A node in the scene graph.
63pub struct SceneNode {
64    id: NodeId,
65    name: String,
66    mesh_id: Option<MeshId>,
67    material: Material,
68    visible: bool,
69    show_normals: bool,
70    local_transform: glam::Mat4,
71    world_transform: glam::Mat4,
72    parent: Option<NodeId>,
73    children: Vec<NodeId>,
74    layer: LayerId,
75    dirty: bool,
76}
77
78impl SceneNode {
79    /// Unique identifier for this node.
80    pub fn id(&self) -> NodeId {
81        self.id
82    }
83
84    /// Display name of this node.
85    pub fn name(&self) -> &str {
86        &self.name
87    }
88
89    /// GPU mesh associated with this node, or `None` if no mesh has been uploaded.
90    pub fn mesh_id(&self) -> Option<MeshId> {
91        self.mesh_id
92    }
93
94    /// Material parameters (color, shading, opacity, texture) for this node.
95    pub fn material(&self) -> &Material {
96        &self.material
97    }
98
99    /// Whether this node is visible.
100    pub fn is_visible(&self) -> bool {
101        self.visible
102    }
103
104    /// Whether per-vertex normals are rendered for this node.
105    pub fn show_normals(&self) -> bool {
106        self.show_normals
107    }
108
109    /// Local transform relative to this node's parent (or world if no parent).
110    pub fn local_transform(&self) -> glam::Mat4 {
111        self.local_transform
112    }
113
114    /// World-space transform. Updated by `Scene::update_transforms()`.
115    pub fn world_transform(&self) -> glam::Mat4 {
116        self.world_transform
117    }
118
119    /// Parent node ID, or `None` if this is a root node.
120    pub fn parent(&self) -> Option<NodeId> {
121        self.parent
122    }
123
124    /// IDs of this node's direct children.
125    pub fn children(&self) -> &[NodeId] {
126        &self.children
127    }
128
129    /// Layer this node belongs to.
130    pub fn layer(&self) -> LayerId {
131        self.layer
132    }
133}
134
135impl ViewportObject for SceneNode {
136    fn id(&self) -> u64 {
137        self.id
138    }
139
140    fn mesh_id(&self) -> Option<u64> {
141        self.mesh_id.map(|m| m.index() as u64)
142    }
143
144    fn model_matrix(&self) -> glam::Mat4 {
145        self.world_transform
146    }
147
148    fn position(&self) -> glam::Vec3 {
149        self.world_transform.col(3).truncate()
150    }
151
152    fn rotation(&self) -> glam::Quat {
153        let (_scale, rotation, _translation) = self.world_transform.to_scale_rotation_translation();
154        rotation
155    }
156
157    fn scale(&self) -> glam::Vec3 {
158        let (scale, _rotation, _translation) = self.world_transform.to_scale_rotation_translation();
159        scale
160    }
161
162    fn is_visible(&self) -> bool {
163        self.visible
164    }
165
166    fn color(&self) -> glam::Vec3 {
167        glam::Vec3::from(self.material.base_color)
168    }
169
170    fn show_normals(&self) -> bool {
171        self.show_normals
172    }
173
174    fn material(&self) -> Material {
175        self.material
176    }
177}
178
179// ---------------------------------------------------------------------------
180// Scene
181// ---------------------------------------------------------------------------
182
183/// Default layer ID (always exists, cannot be removed).
184const DEFAULT_LAYER: LayerId = LayerId(0);
185
186/// A scene graph managing nodes with parent-child hierarchy and layers.
187pub struct Scene {
188    nodes: HashMap<NodeId, SceneNode>,
189    roots: Vec<NodeId>,
190    layers: Vec<Layer>,
191    next_id: u64,
192    next_layer_id: u32,
193    groups: Vec<Group>,
194    next_group_id: u32,
195    /// Monotonically increasing generation counter. Incremented on every mutation.
196    /// Callers can compare against a cached value to detect changes without hashing.
197    version: u64,
198}
199
200impl Scene {
201    /// Create an empty scene with a default layer.
202    pub fn new() -> Self {
203        Self {
204            nodes: HashMap::new(),
205            roots: Vec::new(),
206            layers: vec![Layer {
207                id: DEFAULT_LAYER,
208                name: "Default".to_string(),
209                visible: true,
210                locked: false,
211                color: [1.0, 1.0, 1.0, 1.0],
212                order: 0,
213            }],
214            next_id: 1,
215            next_layer_id: 1,
216            groups: Vec::new(),
217            next_group_id: 0,
218            version: 0,
219        }
220    }
221
222    /// Monotonically increasing generation counter.
223    ///
224    /// Incremented by `wrapping_add(1)` on every mutation. Compare against a
225    /// cached value to detect scene changes without hashing instance data.
226    pub fn version(&self) -> u64 {
227        self.version
228    }
229
230    // -- Node lifecycle --
231
232    /// Add a node with a mesh, transform, and material. Returns the new node's ID.
233    pub fn add(
234        &mut self,
235        mesh_id: Option<MeshId>,
236        transform: glam::Mat4,
237        material: Material,
238    ) -> NodeId {
239        self.add_named("", mesh_id, transform, material)
240    }
241
242    /// Add a named node. Returns the new node's ID.
243    pub fn add_named(
244        &mut self,
245        name: &str,
246        mesh_id: Option<MeshId>,
247        transform: glam::Mat4,
248        material: Material,
249    ) -> NodeId {
250        let id = self.next_id;
251        self.next_id += 1;
252        let node = SceneNode {
253            id,
254            name: name.to_string(),
255            mesh_id,
256            material,
257            visible: true,
258            show_normals: false,
259            local_transform: transform,
260            world_transform: transform,
261            parent: None,
262            children: Vec::new(),
263            layer: DEFAULT_LAYER,
264            dirty: true,
265        };
266        self.nodes.insert(id, node);
267        self.roots.push(id);
268        self.version = self.version.wrapping_add(1);
269        id
270    }
271
272    /// Remove a node and all its descendants. Returns all removed IDs
273    /// (caller can use these to release mesh references).
274    pub fn remove(&mut self, id: NodeId) -> Vec<NodeId> {
275        let mut removed = Vec::new();
276        self.remove_recursive(id, &mut removed);
277
278        // Remove from parent's children list or from roots.
279        if let Some(parent_id) = self.nodes.get(&id).and_then(|n| n.parent) {
280            if let Some(parent) = self.nodes.get_mut(&parent_id) {
281                parent.children.retain(|c| *c != id);
282            }
283        } else {
284            self.roots.retain(|r| *r != id);
285        }
286
287        // Actually remove nodes.
288        for &rid in &removed {
289            self.nodes.remove(&rid);
290        }
291        // Also remove from roots any descendant that might have been listed.
292        self.roots.retain(|r| !removed.contains(r));
293
294        // Remove removed nodes from all groups.
295        for group in &mut self.groups {
296            for &rid in &removed {
297                group.members.remove(&rid);
298            }
299        }
300
301        self.version = self.version.wrapping_add(1);
302        removed
303    }
304
305    fn remove_recursive(&self, id: NodeId, out: &mut Vec<NodeId>) {
306        out.push(id);
307        if let Some(node) = self.nodes.get(&id) {
308            for &child in &node.children {
309                self.remove_recursive(child, out);
310            }
311        }
312    }
313
314    // -- Hierarchy --
315
316    /// Reparent a node. `None` makes it a root node.
317    pub fn set_parent(&mut self, child_id: NodeId, new_parent: Option<NodeId>) {
318        // Remove from current parent or roots.
319        let old_parent = self.nodes.get(&child_id).and_then(|n| n.parent);
320        if let Some(old_pid) = old_parent {
321            if let Some(old_p) = self.nodes.get_mut(&old_pid) {
322                old_p.children.retain(|c| *c != child_id);
323            }
324        } else {
325            self.roots.retain(|r| *r != child_id);
326        }
327
328        // Add to new parent or roots.
329        if let Some(new_pid) = new_parent {
330            if let Some(new_p) = self.nodes.get_mut(&new_pid) {
331                new_p.children.push(child_id);
332            }
333        } else {
334            self.roots.push(child_id);
335        }
336
337        if let Some(node) = self.nodes.get_mut(&child_id) {
338            node.parent = new_parent;
339            node.dirty = true;
340        }
341        self.version = self.version.wrapping_add(1);
342    }
343
344    /// Children of a node.
345    pub fn children(&self, id: NodeId) -> &[NodeId] {
346        self.nodes
347            .get(&id)
348            .map(|n| n.children.as_slice())
349            .unwrap_or(&[])
350    }
351
352    /// Parent of a node.
353    pub fn parent(&self, id: NodeId) -> Option<NodeId> {
354        self.nodes.get(&id).and_then(|n| n.parent)
355    }
356
357    /// Root nodes.
358    pub fn roots(&self) -> &[NodeId] {
359        &self.roots
360    }
361
362    // -- Properties --
363
364    /// Set the local transform of a node, marking it and its descendants dirty.
365    pub fn set_local_transform(&mut self, id: NodeId, transform: glam::Mat4) {
366        if let Some(node) = self.nodes.get_mut(&id) {
367            node.local_transform = transform;
368            node.dirty = true;
369        }
370        self.mark_descendants_dirty(id);
371        self.version = self.version.wrapping_add(1);
372    }
373
374    /// Set node visibility.
375    pub fn set_visible(&mut self, id: NodeId, visible: bool) {
376        if let Some(node) = self.nodes.get_mut(&id) {
377            node.visible = visible;
378        }
379        self.version = self.version.wrapping_add(1);
380    }
381
382    /// Set node material.
383    pub fn set_material(&mut self, id: NodeId, material: Material) {
384        if let Some(node) = self.nodes.get_mut(&id) {
385            node.material = material;
386        }
387        self.version = self.version.wrapping_add(1);
388    }
389
390    /// Set node mesh.
391    pub fn set_mesh(&mut self, id: NodeId, mesh_id: Option<MeshId>) {
392        if let Some(node) = self.nodes.get_mut(&id) {
393            node.mesh_id = mesh_id;
394        }
395        self.version = self.version.wrapping_add(1);
396    }
397
398    /// Set node name.
399    pub fn set_name(&mut self, id: NodeId, name: &str) {
400        if let Some(node) = self.nodes.get_mut(&id) {
401            node.name = name.to_string();
402        }
403        self.version = self.version.wrapping_add(1);
404    }
405
406    /// Set whether to show normals.
407    pub fn set_show_normals(&mut self, id: NodeId, show: bool) {
408        if let Some(node) = self.nodes.get_mut(&id) {
409            node.show_normals = show;
410        }
411        self.version = self.version.wrapping_add(1);
412    }
413
414    /// Set the layer of a node.
415    pub fn set_layer(&mut self, id: NodeId, layer: LayerId) {
416        if let Some(node) = self.nodes.get_mut(&id) {
417            node.layer = layer;
418        }
419        self.version = self.version.wrapping_add(1);
420    }
421
422    /// Get a reference to a node.
423    pub fn node(&self, id: NodeId) -> Option<&SceneNode> {
424        self.nodes.get(&id)
425    }
426
427    /// Number of nodes in the scene.
428    pub fn node_count(&self) -> usize {
429        self.nodes.len()
430    }
431
432    /// Iterate over all nodes.
433    pub fn nodes(&self) -> impl Iterator<Item = &SceneNode> {
434        self.nodes.values()
435    }
436
437    // -- Layers --
438
439    /// Add a new layer, returning its ID.
440    pub fn add_layer(&mut self, name: &str) -> LayerId {
441        let id = LayerId(self.next_layer_id);
442        let order = self.next_layer_id;
443        self.next_layer_id += 1;
444        self.layers.push(Layer {
445            id,
446            name: name.to_string(),
447            visible: true,
448            locked: false,
449            color: [1.0, 1.0, 1.0, 1.0],
450            order,
451        });
452        self.version = self.version.wrapping_add(1);
453        id
454    }
455
456    /// Remove a layer, moving all its nodes to the default layer.
457    /// Cannot remove the default layer (LayerId(0)).
458    pub fn remove_layer(&mut self, id: LayerId) {
459        if id == DEFAULT_LAYER {
460            return;
461        }
462        // Move nodes to default layer.
463        for node in self.nodes.values_mut() {
464            if node.layer == id {
465                node.layer = DEFAULT_LAYER;
466            }
467        }
468        self.layers.retain(|l| l.id != id);
469        self.version = self.version.wrapping_add(1);
470    }
471
472    /// Set layer visibility.
473    pub fn set_layer_visible(&mut self, id: LayerId, visible: bool) {
474        if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
475            layer.visible = visible;
476        }
477        self.version = self.version.wrapping_add(1);
478    }
479
480    /// Set layer locked state. Locked layers render but nodes cannot appear selected.
481    pub fn set_layer_locked(&mut self, id: LayerId, locked: bool) {
482        if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
483            layer.locked = locked;
484        }
485        self.version = self.version.wrapping_add(1);
486    }
487
488    /// Set layer display color.
489    pub fn set_layer_color(&mut self, id: LayerId, color: [f32; 4]) {
490        if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
491            layer.color = color;
492        }
493        self.version = self.version.wrapping_add(1);
494    }
495
496    /// Set layer sort order.
497    pub fn set_layer_order(&mut self, id: LayerId, order: u32) {
498        if let Some(layer) = self.layers.iter_mut().find(|l| l.id == id) {
499            layer.order = order;
500        }
501        self.version = self.version.wrapping_add(1);
502    }
503
504    /// Whether a layer is currently locked.
505    pub fn is_layer_locked(&self, id: LayerId) -> bool {
506        self.layers
507            .iter()
508            .find(|l| l.id == id)
509            .map(|l| l.locked)
510            .unwrap_or(false)
511    }
512
513    /// All layers, sorted by their `order` field (ascending).
514    pub fn layers(&self) -> Vec<&Layer> {
515        let mut sorted: Vec<&Layer> = self.layers.iter().collect();
516        sorted.sort_by_key(|l| l.order);
517        sorted
518    }
519
520    // -- Groups --
521
522    /// Create a new named group, returning its ID.
523    pub fn create_group(&mut self, name: &str) -> GroupId {
524        let id = GroupId(self.next_group_id);
525        self.next_group_id += 1;
526        self.groups.push(Group {
527            id,
528            name: name.to_string(),
529            members: HashSet::new(),
530        });
531        self.version = self.version.wrapping_add(1);
532        id
533    }
534
535    /// Remove a group by ID. Does not affect its member nodes.
536    pub fn remove_group(&mut self, id: GroupId) {
537        self.groups.retain(|g| g.id != id);
538        self.version = self.version.wrapping_add(1);
539    }
540
541    /// Add a node to a group.
542    pub fn add_to_group(&mut self, node: NodeId, group: GroupId) {
543        if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
544            g.members.insert(node);
545        }
546        self.version = self.version.wrapping_add(1);
547    }
548
549    /// Remove a node from a group.
550    pub fn remove_from_group(&mut self, node: NodeId, group: GroupId) {
551        if let Some(g) = self.groups.iter_mut().find(|g| g.id == group) {
552            g.members.remove(&node);
553        }
554        self.version = self.version.wrapping_add(1);
555    }
556
557    /// Get a group by ID.
558    pub fn get_group(&self, id: GroupId) -> Option<&Group> {
559        self.groups.iter().find(|g| g.id == id)
560    }
561
562    /// All groups in the scene.
563    pub fn groups(&self) -> &[Group] {
564        &self.groups
565    }
566
567    /// Which groups contain the given node.
568    pub fn node_groups(&self, node: NodeId) -> Vec<GroupId> {
569        self.groups
570            .iter()
571            .filter(|g| g.members.contains(&node))
572            .map(|g| g.id)
573            .collect()
574    }
575
576    // -- Transform propagation --
577
578    /// Recompute world transforms for all dirty nodes (BFS from roots).
579    pub fn update_transforms(&mut self) {
580        // We need to iterate roots and process the tree. Since we can't borrow
581        // self mutably while iterating, collect the root list first.
582        let roots: Vec<NodeId> = self.roots.clone();
583        for &root_id in &roots {
584            self.propagate_transform(root_id, glam::Mat4::IDENTITY);
585        }
586    }
587
588    fn propagate_transform(&mut self, id: NodeId, parent_world: glam::Mat4) {
589        let (dirty, local, children) = {
590            let Some(node) = self.nodes.get(&id) else {
591                return;
592            };
593            (node.dirty, node.local_transform, node.children.clone())
594        };
595
596        if dirty {
597            let world = parent_world * local;
598            let node = self.nodes.get_mut(&id).unwrap();
599            node.world_transform = world;
600            node.dirty = false;
601            // All children must recompute.
602            for &child_id in &children {
603                self.mark_dirty(child_id);
604                self.propagate_transform(child_id, world);
605            }
606        } else {
607            let world = self.nodes[&id].world_transform;
608            for &child_id in &children {
609                self.propagate_transform(child_id, world);
610            }
611        }
612    }
613
614    fn mark_dirty(&mut self, id: NodeId) {
615        if let Some(node) = self.nodes.get_mut(&id) {
616            node.dirty = true;
617        }
618    }
619
620    fn mark_descendants_dirty(&mut self, id: NodeId) {
621        let children = self
622            .nodes
623            .get(&id)
624            .map(|n| n.children.clone())
625            .unwrap_or_default();
626        for child_id in children {
627            self.mark_dirty(child_id);
628            self.mark_descendants_dirty(child_id);
629        }
630    }
631
632    // -- Render collection --
633
634    /// Update transforms and collect render items for all visible nodes.
635    ///
636    /// Skips nodes that are invisible, on an invisible layer, or have no mesh.
637    /// Marks selected nodes based on the provided `Selection`.
638    pub fn collect_render_items(&mut self, selection: &Selection) -> Vec<SceneRenderItem> {
639        self.update_transforms();
640
641        let layer_visible: HashMap<LayerId, bool> =
642            self.layers.iter().map(|l| (l.id, l.visible)).collect();
643
644        let layer_locked: HashMap<LayerId, bool> =
645            self.layers.iter().map(|l| (l.id, l.locked)).collect();
646
647        let mut items = Vec::new();
648        for node in self.nodes.values() {
649            if !node.visible {
650                continue;
651            }
652            if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
653                continue;
654            }
655            let Some(mesh_id) = node.mesh_id else {
656                continue;
657            };
658            let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
659            items.push(SceneRenderItem {
660                mesh_index: mesh_id.index(),
661                model: node.world_transform.to_cols_array_2d(),
662                selected: if locked {
663                    false
664                } else {
665                    selection.contains(node.id)
666                },
667                visible: true,
668                show_normals: node.show_normals,
669                material: node.material,
670                active_attribute: None,
671                scalar_range: None,
672                colormap_id: None,
673                nan_color: None,
674                two_sided: false,
675            });
676        }
677        items
678    }
679
680    /// Update transforms and collect render items, culling objects outside the frustum.
681    ///
682    /// Like `collect_render_items`, but skips objects whose world-space AABB is
683    /// entirely outside the given frustum. `mesh_aabb_fn` should return the
684    /// local-space AABB for a given `MeshId` (typically read from `GpuMesh::aabb`).
685    pub fn collect_render_items_culled(
686        &mut self,
687        selection: &Selection,
688        frustum: &crate::camera::frustum::Frustum,
689        mesh_aabb_fn: impl Fn(MeshId) -> Option<crate::scene::aabb::Aabb>,
690    ) -> (Vec<SceneRenderItem>, crate::camera::frustum::CullStats) {
691        self.update_transforms();
692
693        let layer_visible: HashMap<LayerId, bool> =
694            self.layers.iter().map(|l| (l.id, l.visible)).collect();
695
696        let layer_locked: HashMap<LayerId, bool> =
697            self.layers.iter().map(|l| (l.id, l.locked)).collect();
698
699        let mut items = Vec::new();
700        let mut stats = crate::camera::frustum::CullStats::default();
701
702        for node in self.nodes.values() {
703            if !node.visible {
704                continue;
705            }
706            if !layer_visible.get(&node.layer).copied().unwrap_or(true) {
707                continue;
708            }
709            let Some(mesh_id) = node.mesh_id else {
710                continue;
711            };
712
713            stats.total += 1;
714
715            // Frustum cull using world-space AABB.
716            if let Some(local_aabb) = mesh_aabb_fn(mesh_id) {
717                let world_aabb = local_aabb.transformed(&node.world_transform);
718                if frustum.cull_aabb(&world_aabb) {
719                    stats.culled += 1;
720                    continue;
721                }
722            }
723
724            let locked = layer_locked.get(&node.layer).copied().unwrap_or(false);
725            stats.visible += 1;
726            items.push(SceneRenderItem {
727                mesh_index: mesh_id.index(),
728                model: node.world_transform.to_cols_array_2d(),
729                selected: if locked {
730                    false
731                } else {
732                    selection.contains(node.id)
733                },
734                visible: true,
735                show_normals: node.show_normals,
736                material: node.material,
737                active_attribute: None,
738                scalar_range: None,
739                colormap_id: None,
740                nan_color: None,
741                two_sided: false,
742            });
743        }
744        (items, stats)
745    }
746
747    // -- Tree walking --
748
749    // -- Mesh ref counting --
750
751    /// Count how many scene nodes reference the given mesh.
752    ///
753    /// O(n) over all nodes. Useful for deciding when to free a GPU mesh.
754    pub fn mesh_ref_count(&self, mesh_id: MeshId) -> usize {
755        self.nodes
756            .values()
757            .filter(|n| n.mesh_id == Some(mesh_id))
758            .count()
759    }
760
761    // -- Tree walking --
762
763    /// Depth-first traversal of the scene tree. Returns `(NodeId, depth)` pairs.
764    pub fn walk_depth_first(&self) -> Vec<(NodeId, usize)> {
765        let mut result = Vec::new();
766        for &root_id in &self.roots {
767            self.walk_recursive(root_id, 0, &mut result);
768        }
769        result
770    }
771
772    fn walk_recursive(&self, id: NodeId, depth: usize, out: &mut Vec<(NodeId, usize)>) {
773        out.push((id, depth));
774        if let Some(node) = self.nodes.get(&id) {
775            for &child_id in &node.children {
776                self.walk_recursive(child_id, depth + 1, out);
777            }
778        }
779    }
780}
781
782impl Default for Scene {
783    fn default() -> Self {
784        Self::new()
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791
792    #[test]
793    fn test_add_and_remove() {
794        let mut scene = Scene::new();
795        let id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
796        assert!(scene.node(id).is_some());
797        assert_eq!(scene.node_count(), 1);
798
799        let removed = scene.remove(id);
800        assert_eq!(removed, vec![id]);
801        assert!(scene.node(id).is_none());
802        assert_eq!(scene.node_count(), 0);
803    }
804
805    #[test]
806    fn test_remove_cascades_to_children() {
807        let mut scene = Scene::new();
808        let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
809        let child1 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
810        let child2 = scene.add(None, glam::Mat4::IDENTITY, Material::default());
811        scene.set_parent(child1, Some(parent));
812        scene.set_parent(child2, Some(parent));
813
814        let removed = scene.remove(parent);
815        assert_eq!(removed.len(), 3);
816        assert!(removed.contains(&parent));
817        assert!(removed.contains(&child1));
818        assert!(removed.contains(&child2));
819        assert_eq!(scene.node_count(), 0);
820    }
821
822    #[test]
823    fn test_set_parent_updates_world_transform() {
824        let mut scene = Scene::new();
825        let parent = scene.add(
826            None,
827            glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0)),
828            Material::default(),
829        );
830        let child = scene.add(
831            None,
832            glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
833            Material::default(),
834        );
835        scene.set_parent(child, Some(parent));
836        scene.update_transforms();
837
838        let world = scene.node(child).unwrap().world_transform();
839        let pos = world.col(3).truncate();
840        assert!((pos.x - 6.0).abs() < 1e-5, "expected x=6.0, got {}", pos.x);
841    }
842
843    #[test]
844    fn test_dirty_propagation() {
845        let mut scene = Scene::new();
846        let parent = scene.add(
847            None,
848            glam::Mat4::from_translation(glam::Vec3::new(1.0, 0.0, 0.0)),
849            Material::default(),
850        );
851        let child = scene.add(
852            None,
853            glam::Mat4::from_translation(glam::Vec3::new(2.0, 0.0, 0.0)),
854            Material::default(),
855        );
856        scene.set_parent(child, Some(parent));
857        scene.update_transforms();
858
859        // Now move the parent.
860        scene.set_local_transform(
861            parent,
862            glam::Mat4::from_translation(glam::Vec3::new(10.0, 0.0, 0.0)),
863        );
864        scene.update_transforms();
865
866        let child_pos = scene
867            .node(child)
868            .unwrap()
869            .world_transform()
870            .col(3)
871            .truncate();
872        assert!(
873            (child_pos.x - 12.0).abs() < 1e-5,
874            "expected x=12.0, got {}",
875            child_pos.x
876        );
877    }
878
879    #[test]
880    fn test_layer_visibility_hides_nodes() {
881        let mut scene = Scene::new();
882        let layer = scene.add_layer("Hidden");
883        let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
884        scene.set_layer(id, layer);
885        scene.set_layer_visible(layer, false);
886
887        let items = scene.collect_render_items(&Selection::new());
888        assert!(items.is_empty());
889    }
890
891    #[test]
892    fn test_collect_skips_invisible_nodes() {
893        let mut scene = Scene::new();
894        let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
895        scene.set_visible(id, false);
896
897        let items = scene.collect_render_items(&Selection::new());
898        assert!(items.is_empty());
899    }
900
901    #[test]
902    fn test_collect_skips_meshless_nodes() {
903        let mut scene = Scene::new();
904        scene.add(None, glam::Mat4::IDENTITY, Material::default());
905
906        let items = scene.collect_render_items(&Selection::new());
907        assert!(items.is_empty());
908    }
909
910    #[test]
911    fn test_collect_marks_selected() {
912        let mut scene = Scene::new();
913        let id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
914
915        let mut sel = Selection::new();
916        sel.select_one(id);
917
918        let items = scene.collect_render_items(&sel);
919        assert_eq!(items.len(), 1);
920        assert!(items[0].selected);
921    }
922
923    #[test]
924    fn test_unparent_makes_root() {
925        let mut scene = Scene::new();
926        let parent = scene.add(None, glam::Mat4::IDENTITY, Material::default());
927        let child = scene.add(None, glam::Mat4::IDENTITY, Material::default());
928        scene.set_parent(child, Some(parent));
929        assert!(!scene.roots().contains(&child));
930
931        scene.set_parent(child, None);
932        assert!(scene.roots().contains(&child));
933        assert!(scene.node(child).unwrap().parent().is_none());
934    }
935
936    #[test]
937    fn test_collect_culled_filters_offscreen() {
938        let mut scene = Scene::new();
939        // Object at origin — should be visible.
940        let visible_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
941        // Object far behind camera — should be culled.
942        let _behind = scene.add(
943            Some(MeshId(1)),
944            glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 100.0)),
945            Material::default(),
946        );
947
948        let sel = Selection::new();
949        // Camera at z=5 looking toward origin.
950        let view = glam::Mat4::look_at_rh(
951            glam::Vec3::new(0.0, 0.0, 5.0),
952            glam::Vec3::ZERO,
953            glam::Vec3::Y,
954        );
955        let proj = glam::Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 50.0);
956        let frustum = crate::camera::frustum::Frustum::from_view_proj(&(proj * view));
957
958        // Both meshes get a unit-cube AABB.
959        let unit_aabb = crate::scene::aabb::Aabb {
960            min: glam::Vec3::splat(-0.5),
961            max: glam::Vec3::splat(0.5),
962        };
963
964        let (items, stats) =
965            scene.collect_render_items_culled(&sel, &frustum, |_mesh_id| Some(unit_aabb));
966
967        assert_eq!(stats.total, 2);
968        assert_eq!(stats.visible, 1);
969        assert_eq!(stats.culled, 1);
970        assert_eq!(items.len(), 1);
971        // The visible item should be the one at the origin (mesh_index 0).
972        assert_eq!(items[0].mesh_index, visible_id as usize - 1); // MeshId(0).index() == 0
973        let _ = visible_id; // suppress unused warning
974    }
975
976    // --- Layer lock/color/order tests ---
977
978    #[test]
979    fn test_layer_locked_field_default_false() {
980        let scene = Scene::new();
981        let layers = scene.layers();
982        let default_layer = layers.iter().find(|l| l.id == LayerId(0)).unwrap();
983        assert!(!default_layer.locked);
984    }
985
986    #[test]
987    fn test_add_layer_has_locked_false_color_white_and_order() {
988        let mut scene = Scene::new();
989        let layer_id = scene.add_layer("Test");
990        let layers = scene.layers();
991        let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
992        assert!(!layer.locked);
993        assert_eq!(layer.color, [1.0, 1.0, 1.0, 1.0]);
994        assert!(layer.order > 0); // non-default layer has order >= 1
995    }
996
997    #[test]
998    fn test_set_layer_locked() {
999        let mut scene = Scene::new();
1000        let layer_id = scene.add_layer("Locked");
1001        scene.set_layer_locked(layer_id, true);
1002        assert!(scene.is_layer_locked(layer_id));
1003        scene.set_layer_locked(layer_id, false);
1004        assert!(!scene.is_layer_locked(layer_id));
1005    }
1006
1007    #[test]
1008    fn test_set_layer_color() {
1009        let mut scene = Scene::new();
1010        let layer_id = scene.add_layer("Colored");
1011        scene.set_layer_color(layer_id, [1.0, 0.0, 0.0, 1.0]);
1012        let layers = scene.layers();
1013        let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1014        assert_eq!(layer.color, [1.0, 0.0, 0.0, 1.0]);
1015    }
1016
1017    #[test]
1018    fn test_set_layer_order() {
1019        let mut scene = Scene::new();
1020        let layer_id = scene.add_layer("Orderly");
1021        scene.set_layer_order(layer_id, 99);
1022        let layers = scene.layers();
1023        let layer = layers.iter().find(|l| l.id == layer_id).unwrap();
1024        assert_eq!(layer.order, 99);
1025    }
1026
1027    #[test]
1028    fn test_locked_layer_suppresses_selection_in_render_items() {
1029        let mut scene = Scene::new();
1030        let layer_id = scene.add_layer("Locked");
1031        let node_id = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1032        scene.set_layer(node_id, layer_id);
1033        scene.set_layer_locked(layer_id, true);
1034
1035        let mut sel = Selection::new();
1036        sel.select_one(node_id);
1037
1038        let items = scene.collect_render_items(&sel);
1039        assert_eq!(items.len(), 1, "locked layer nodes still render");
1040        assert!(
1041            !items[0].selected,
1042            "locked layer nodes must not appear selected"
1043        );
1044    }
1045
1046    #[test]
1047    fn test_layers_sorted_by_order() {
1048        let mut scene = Scene::new();
1049        let a = scene.add_layer("A");
1050        let b = scene.add_layer("B");
1051        // Set reverse order
1052        scene.set_layer_order(a, 10);
1053        scene.set_layer_order(b, 5);
1054        let layers = scene.layers();
1055        // Find positions
1056        let pos_b = layers.iter().position(|l| l.id == b).unwrap();
1057        let pos_a = layers.iter().position(|l| l.id == a).unwrap();
1058        assert!(
1059            pos_b < pos_a,
1060            "layer B (order=5) should appear before A (order=10)"
1061        );
1062    }
1063
1064    // --- Group tests ---
1065
1066    #[test]
1067    fn test_create_group_returns_id() {
1068        let mut scene = Scene::new();
1069        let gid = scene.create_group("MyGroup");
1070        let group = scene.get_group(gid).unwrap();
1071        assert_eq!(group.name, "MyGroup");
1072        assert!(group.members.is_empty());
1073    }
1074
1075    #[test]
1076    fn test_add_to_group_and_remove_from_group() {
1077        let mut scene = Scene::new();
1078        let gid = scene.create_group("G");
1079        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1080        scene.add_to_group(node_id, gid);
1081        assert!(scene.get_group(gid).unwrap().members.contains(&node_id));
1082        scene.remove_from_group(node_id, gid);
1083        assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1084    }
1085
1086    #[test]
1087    fn test_groups_returns_all_groups() {
1088        let mut scene = Scene::new();
1089        scene.create_group("G1");
1090        scene.create_group("G2");
1091        assert_eq!(scene.groups().len(), 2);
1092    }
1093
1094    #[test]
1095    fn test_node_groups_returns_containing_groups() {
1096        let mut scene = Scene::new();
1097        let g1 = scene.create_group("G1");
1098        let g2 = scene.create_group("G2");
1099        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1100        scene.add_to_group(node_id, g1);
1101        scene.add_to_group(node_id, g2);
1102        let groups = scene.node_groups(node_id);
1103        assert_eq!(groups.len(), 2);
1104        assert!(groups.contains(&g1));
1105        assert!(groups.contains(&g2));
1106    }
1107
1108    #[test]
1109    fn test_remove_node_cleans_up_group_membership() {
1110        let mut scene = Scene::new();
1111        let gid = scene.create_group("G");
1112        let node_id = scene.add(None, glam::Mat4::IDENTITY, Material::default());
1113        scene.add_to_group(node_id, gid);
1114        scene.remove(node_id);
1115        assert!(!scene.get_group(gid).unwrap().members.contains(&node_id));
1116    }
1117
1118    // --- Mesh ref count tests ---
1119
1120    #[test]
1121    fn test_mesh_ref_count_zero_for_unused_mesh() {
1122        let scene = Scene::new();
1123        assert_eq!(scene.mesh_ref_count(MeshId(42)), 0);
1124    }
1125
1126    #[test]
1127    fn test_mesh_ref_count_correct_for_nodes() {
1128        let mut scene = Scene::new();
1129        scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1130        scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1131        scene.add(Some(MeshId(1)), glam::Mat4::IDENTITY, Material::default());
1132        assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1133        assert_eq!(scene.mesh_ref_count(MeshId(1)), 1);
1134    }
1135
1136    #[test]
1137    fn test_mesh_ref_count_decreases_after_remove() {
1138        let mut scene = Scene::new();
1139        let node_a = scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1140        scene.add(Some(MeshId(0)), glam::Mat4::IDENTITY, Material::default());
1141        assert_eq!(scene.mesh_ref_count(MeshId(0)), 2);
1142        scene.remove(node_a);
1143        assert_eq!(scene.mesh_ref_count(MeshId(0)), 1);
1144    }
1145
1146    #[test]
1147    fn test_remove_group() {
1148        let mut scene = Scene::new();
1149        let gid = scene.create_group("G");
1150        scene.remove_group(gid);
1151        assert!(scene.get_group(gid).is_none());
1152        assert!(scene.groups().is_empty());
1153    }
1154
1155    #[test]
1156    fn test_walk_depth_first_order() {
1157        let mut scene = Scene::new();
1158        let root = scene.add_named("root", None, glam::Mat4::IDENTITY, Material::default());
1159        let child_a = scene.add_named("a", None, glam::Mat4::IDENTITY, Material::default());
1160        let child_b = scene.add_named("b", None, glam::Mat4::IDENTITY, Material::default());
1161        let grandchild = scene.add_named("a1", None, glam::Mat4::IDENTITY, Material::default());
1162        scene.set_parent(child_a, Some(root));
1163        scene.set_parent(child_b, Some(root));
1164        scene.set_parent(grandchild, Some(child_a));
1165
1166        let walk = scene.walk_depth_first();
1167        assert_eq!(walk.len(), 4);
1168        assert_eq!(walk[0], (root, 0));
1169        assert_eq!(walk[1], (child_a, 1));
1170        assert_eq!(walk[2], (grandchild, 2));
1171        assert_eq!(walk[3], (child_b, 1));
1172    }
1173}