Skip to main content

proof_engine/scene/
node.rs

1//! Scene nodes — typed wrappers for the scene graph with parent-child transforms.
2//!
3//! Each node has a local transform (position, rotation_z, scale). The world
4//! transform is computed by walking the parent chain. Dirty flags prevent
5//! redundant world-transform recomputation.
6
7use glam::{Mat4, Vec3};
8
9// ── Transform ─────────────────────────────────────────────────────────────────
10
11/// A 3-DOF local transform stored compactly.
12#[derive(Debug, Clone, Copy)]
13pub struct Transform {
14    pub position:   Vec3,
15    pub rotation_z: f32,   // radians, applied around Z axis (2D-first engine)
16    pub scale:      Vec3,
17}
18
19impl Transform {
20    pub const IDENTITY: Self = Self {
21        position:   Vec3::ZERO,
22        rotation_z: 0.0,
23        scale:      Vec3::ONE,
24    };
25
26    pub fn from_position(position: Vec3) -> Self {
27        Self { position, ..Self::IDENTITY }
28    }
29
30    pub fn from_pos_scale(position: Vec3, scale: f32) -> Self {
31        Self { position, scale: Vec3::splat(scale), ..Self::IDENTITY }
32    }
33
34    /// Build a 4×4 local-to-parent matrix.
35    pub fn to_matrix(&self) -> Mat4 {
36        let t = Mat4::from_translation(self.position);
37        let r = Mat4::from_rotation_z(self.rotation_z);
38        let s = Mat4::from_scale(self.scale);
39        t * r * s
40    }
41
42    /// Interpolate toward another transform (for smooth animation).
43    pub fn lerp_toward(&self, other: &Transform, t: f32) -> Transform {
44        Transform {
45            position:   self.position.lerp(other.position, t),
46            rotation_z: self.rotation_z + (other.rotation_z - self.rotation_z) * t,
47            scale:      self.scale.lerp(other.scale, t),
48        }
49    }
50}
51
52impl Default for Transform {
53    fn default() -> Self { Self::IDENTITY }
54}
55
56// ── Node ──────────────────────────────────────────────────────────────────────
57
58/// Unique ID for a scene node.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
60pub struct NodeId(pub u64);
61
62/// A scene graph node with parent-child transform hierarchy.
63#[derive(Debug, Clone)]
64pub struct SceneNode {
65    pub id:             NodeId,
66    pub name:           Option<String>,
67    pub local:          Transform,
68    pub visible:        bool,
69    /// Whether world_transform needs recomputation.
70    dirty:              bool,
71    /// Cached world-space transform matrix.
72    world_transform:    Mat4,
73    pub parent:         Option<NodeId>,
74    pub children:       Vec<NodeId>,
75    /// Arbitrary tag for queries (e.g. "player", "enemy", "ui").
76    pub tag:            Option<String>,
77    /// Draw order within the same layer (lower = drawn first).
78    pub sort_key:       i32,
79    /// User-defined metadata slot (e.g. entity index).
80    pub user_data:      u64,
81}
82
83impl SceneNode {
84    pub fn new(id: NodeId, position: Vec3) -> Self {
85        Self {
86            id,
87            name: None,
88            local: Transform::from_position(position),
89            visible: true,
90            dirty: true,
91            world_transform: Mat4::IDENTITY,
92            parent: None,
93            children: Vec::new(),
94            tag: None,
95            sort_key: 0,
96            user_data: 0,
97        }
98    }
99
100    pub fn with_name(mut self, name: impl Into<String>) -> Self {
101        self.name = Some(name.into());
102        self
103    }
104
105    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
106        self.tag = Some(tag.into());
107        self
108    }
109
110    pub fn with_sort_key(mut self, key: i32) -> Self {
111        self.sort_key = key;
112        self
113    }
114
115    /// Set local position and mark dirty.
116    pub fn set_position(&mut self, pos: Vec3) {
117        self.local.position = pos;
118        self.dirty = true;
119    }
120
121    /// Set local rotation (Z axis) and mark dirty.
122    pub fn set_rotation_z(&mut self, angle: f32) {
123        self.local.rotation_z = angle;
124        self.dirty = true;
125    }
126
127    /// Set uniform local scale and mark dirty.
128    pub fn set_scale(&mut self, scale: f32) {
129        self.local.scale = Vec3::splat(scale);
130        self.dirty = true;
131    }
132
133    /// Set the full local transform and mark dirty.
134    pub fn set_transform(&mut self, t: Transform) {
135        self.local = t;
136        self.dirty = true;
137    }
138
139    /// Compute and cache the world transform given the parent's world matrix.
140    /// Returns whether recomputation occurred.
141    pub fn update_world_transform(&mut self, parent_world: &Mat4) -> bool {
142        if !self.dirty { return false; }
143        self.world_transform = *parent_world * self.local.to_matrix();
144        self.dirty = false;
145        true
146    }
147
148    /// Get the cached world transform matrix.
149    pub fn world_matrix(&self) -> &Mat4 { &self.world_transform }
150
151    /// World-space position (column 3 of world matrix).
152    pub fn world_position(&self) -> Vec3 {
153        self.world_transform.w_axis.truncate()
154    }
155
156    /// World-space scale (magnitude of columns 0, 1, 2).
157    pub fn world_scale(&self) -> Vec3 {
158        Vec3::new(
159            self.world_transform.x_axis.truncate().length(),
160            self.world_transform.y_axis.truncate().length(),
161            self.world_transform.z_axis.truncate().length(),
162        )
163    }
164
165    /// Mark this node (and implicitly its subtree) as needing world-transform update.
166    pub fn mark_dirty(&mut self) {
167        self.dirty = true;
168    }
169
170    /// Returns true if the node's world transform needs recomputation.
171    pub fn is_dirty(&self) -> bool { self.dirty }
172
173    /// Translate local position by delta.
174    pub fn translate(&mut self, delta: Vec3) {
175        self.local.position += delta;
176        self.dirty = true;
177    }
178
179    /// Rotate local transform by angle (radians, around Z).
180    pub fn rotate_z(&mut self, angle: f32) {
181        self.local.rotation_z += angle;
182        self.dirty = true;
183    }
184
185    pub fn is_visible(&self) -> bool { self.visible }
186    pub fn set_visible(&mut self, v: bool) { self.visible = v; }
187}
188
189// ── Scene graph ───────────────────────────────────────────────────────────────
190
191/// A flat scene graph with parent-child node relationships.
192pub struct SceneGraph {
193    nodes:    Vec<SceneNode>,
194    next_id:  u64,
195    roots:    Vec<NodeId>,
196}
197
198impl SceneGraph {
199    pub fn new() -> Self {
200        Self { nodes: Vec::new(), next_id: 1, roots: Vec::new() }
201    }
202
203    /// Allocate a new root node.
204    pub fn create_root(&mut self, position: Vec3) -> NodeId {
205        let id = NodeId(self.next_id);
206        self.next_id += 1;
207        self.roots.push(id);
208        self.nodes.push(SceneNode::new(id, position));
209        id
210    }
211
212    /// Allocate a new node with a parent.
213    pub fn create_child(&mut self, parent: NodeId, position: Vec3) -> Option<NodeId> {
214        let id = NodeId(self.next_id);
215        self.next_id += 1;
216        let mut node = SceneNode::new(id, position);
217        node.parent = Some(parent);
218
219        // Record child on parent
220        if let Some(p) = self.get_mut(parent) {
221            p.children.push(id);
222        } else {
223            return None;
224        }
225
226        self.nodes.push(node);
227        Some(id)
228    }
229
230    /// Get a node by ID.
231    pub fn get(&self, id: NodeId) -> Option<&SceneNode> {
232        self.nodes.iter().find(|n| n.id == id)
233    }
234
235    /// Get a mutable node by ID.
236    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut SceneNode> {
237        self.nodes.iter_mut().find(|n| n.id == id)
238    }
239
240    /// Remove a node and all its descendants. Returns the count of removed nodes.
241    pub fn remove_subtree(&mut self, id: NodeId) -> usize {
242        let mut to_remove = vec![id];
243        let mut i = 0;
244        while i < to_remove.len() {
245            let nid = to_remove[i];
246            if let Some(node) = self.get(nid) {
247                to_remove.extend(node.children.iter().copied());
248            }
249            i += 1;
250        }
251        let removed = to_remove.len();
252        self.nodes.retain(|n| !to_remove.contains(&n.id));
253        self.roots.retain(|r| !to_remove.contains(r));
254        removed
255    }
256
257    /// Update world transforms for all dirty nodes (depth-first from roots).
258    pub fn flush_transforms(&mut self) {
259        let roots: Vec<NodeId> = self.roots.clone();
260        for root in roots {
261            self.flush_subtree(root, &Mat4::IDENTITY);
262        }
263    }
264
265    fn flush_subtree(&mut self, id: NodeId, parent_world: &Mat4) {
266        let children: Vec<NodeId>;
267        let new_world;
268        {
269            let node = match self.nodes.iter_mut().find(|n| n.id == id) {
270                Some(n) => n,
271                None    => return,
272            };
273            node.update_world_transform(parent_world);
274            new_world = *node.world_matrix();
275            children = node.children.clone();
276        }
277        for child in children {
278            self.flush_subtree(child, &new_world);
279        }
280    }
281
282    /// Find all nodes with a specific tag.
283    pub fn find_by_tag(&self, tag: &str) -> Vec<NodeId> {
284        self.nodes.iter()
285            .filter(|n| n.tag.as_deref() == Some(tag))
286            .map(|n| n.id)
287            .collect()
288    }
289
290    /// Find all visible nodes sorted by sort_key.
291    pub fn visible_sorted(&self) -> Vec<NodeId> {
292        let mut ids: Vec<NodeId> = self.nodes.iter()
293            .filter(|n| n.visible)
294            .map(|n| n.id)
295            .collect();
296        ids.sort_by_key(|&id| {
297            self.get(id).map(|n| n.sort_key).unwrap_or(0)
298        });
299        ids
300    }
301
302    /// Number of nodes in the graph.
303    pub fn len(&self) -> usize { self.nodes.len() }
304    pub fn is_empty(&self) -> bool { self.nodes.is_empty() }
305
306    /// Iterate over all nodes.
307    pub fn iter(&self) -> impl Iterator<Item = &SceneNode> { self.nodes.iter() }
308}
309
310impl Default for SceneGraph {
311    fn default() -> Self { Self::new() }
312}
313
314// ── Tests ─────────────────────────────────────────────────────────────────────
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn create_root_node() {
322        let mut graph = SceneGraph::new();
323        let id = graph.create_root(Vec3::new(1.0, 2.0, 0.0));
324        graph.flush_transforms();
325        let node = graph.get(id).unwrap();
326        let wp = node.world_position();
327        assert!((wp.x - 1.0).abs() < 0.001);
328        assert!((wp.y - 2.0).abs() < 0.001);
329    }
330
331    #[test]
332    fn child_inherits_parent_transform() {
333        let mut graph = SceneGraph::new();
334        let root = graph.create_root(Vec3::new(5.0, 0.0, 0.0));
335        let child = graph.create_child(root, Vec3::new(1.0, 0.0, 0.0)).unwrap();
336        graph.flush_transforms();
337        let wp = graph.get(child).unwrap().world_position();
338        // World position = parent(5,0,0) + local(1,0,0) = (6,0,0)
339        assert!((wp.x - 6.0).abs() < 0.001);
340    }
341
342    #[test]
343    fn dirty_flag_cleared_after_flush() {
344        let mut graph = SceneGraph::new();
345        let id = graph.create_root(Vec3::ZERO);
346        graph.flush_transforms();
347        assert!(!graph.get(id).unwrap().is_dirty());
348        graph.get_mut(id).unwrap().translate(Vec3::X);
349        assert!(graph.get(id).unwrap().is_dirty());
350    }
351
352    #[test]
353    fn remove_subtree() {
354        let mut graph = SceneGraph::new();
355        let root  = graph.create_root(Vec3::ZERO);
356        let child = graph.create_child(root, Vec3::X).unwrap();
357        let _gc   = graph.create_child(child, Vec3::Y).unwrap();
358        assert_eq!(graph.len(), 3);
359        graph.remove_subtree(child);
360        assert_eq!(graph.len(), 1);
361    }
362
363    #[test]
364    fn find_by_tag() {
365        let mut graph = SceneGraph::new();
366        let id = graph.create_root(Vec3::ZERO);
367        graph.get_mut(id).unwrap().tag = Some("player".to_string());
368        let results = graph.find_by_tag("player");
369        assert_eq!(results, vec![id]);
370    }
371}