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