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