notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

world.rs (30404B)


      1 use glam::{Mat4, Quat, Vec3};
      2 
      3 use crate::camera::Camera;
      4 use crate::model::Model;
      5 
      6 /// A unique handle for a node in the scene graph.
      7 /// Uses arena index + generation to prevent stale handle reuse.
      8 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
      9 pub struct NodeId {
     10     pub index: u32,
     11     pub generation: u32,
     12 }
     13 
     14 /// Backward-compatible alias for existing code that uses ObjectId.
     15 pub type ObjectId = NodeId;
     16 
     17 /// Transform for a scene node (position, rotation, scale).
     18 #[derive(Clone, Debug)]
     19 pub struct Transform {
     20     pub translation: Vec3,
     21     pub rotation: Quat,
     22     pub scale: Vec3,
     23 }
     24 
     25 impl Default for Transform {
     26     fn default() -> Self {
     27         Self {
     28             translation: Vec3::ZERO,
     29             rotation: Quat::IDENTITY,
     30             scale: Vec3::ONE,
     31         }
     32     }
     33 }
     34 
     35 impl Transform {
     36     pub fn from_translation(t: Vec3) -> Self {
     37         Self {
     38             translation: t,
     39             ..Default::default()
     40         }
     41     }
     42 
     43     pub fn to_matrix(&self) -> Mat4 {
     44         Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
     45     }
     46 }
     47 
     48 /// A node in the scene graph.
     49 pub struct Node {
     50     /// Local transform relative to parent (or world if root).
     51     pub local: Transform,
     52 
     53     /// Cached world-space matrix. Valid when `dirty == false`.
     54     world_matrix: Mat4,
     55 
     56     /// When true, world_matrix needs recomputation.
     57     dirty: bool,
     58 
     59     /// Generation for this slot (matches NodeId.generation when alive).
     60     generation: u32,
     61 
     62     /// Parent node. None means this is a root node.
     63     parent: Option<NodeId>,
     64 
     65     /// First child (intrusive linked list through siblings).
     66     first_child: Option<NodeId>,
     67 
     68     /// Next sibling in parent's child list.
     69     next_sibling: Option<NodeId>,
     70 
     71     /// If Some, this node is renderable with the given Model handle.
     72     /// If None, this is a grouping/transform-only node.
     73     pub model: Option<Model>,
     74 
     75     /// Whether this slot is occupied.
     76     alive: bool,
     77 }
     78 
     79 impl Node {
     80     /// Get the cached world-space matrix.
     81     /// Only valid after `update_world_transforms()`.
     82     pub fn world_matrix(&self) -> Mat4 {
     83         self.world_matrix
     84     }
     85 }
     86 
     87 pub struct World {
     88     pub camera: Camera,
     89 
     90     /// Arena of all nodes.
     91     nodes: Vec<Node>,
     92 
     93     /// Free slot indices for reuse.
     94     free_list: Vec<u32>,
     95 
     96     /// Cached list of NodeIds that have a Model (renderable).
     97     /// Rebuilt when renderables_dirty is true.
     98     renderables: Vec<NodeId>,
     99 
    100     /// True when renderables list needs rebuilding.
    101     renderables_dirty: bool,
    102 
    103     pub selected_object: Option<NodeId>,
    104 }
    105 
    106 impl World {
    107     pub fn new(camera: Camera) -> Self {
    108         Self {
    109             camera,
    110             nodes: Vec::new(),
    111             free_list: Vec::new(),
    112             renderables: Vec::new(),
    113             renderables_dirty: false,
    114             selected_object: None,
    115         }
    116     }
    117 
    118     // ── Arena internals ──────────────────────────────────────────
    119 
    120     fn alloc_slot(&mut self) -> (u32, u32) {
    121         if let Some(index) = self.free_list.pop() {
    122             let node = &mut self.nodes[index as usize];
    123             node.generation += 1;
    124             node.alive = true;
    125             node.dirty = true;
    126             node.parent = None;
    127             node.first_child = None;
    128             node.next_sibling = None;
    129             node.model = None;
    130             node.world_matrix = Mat4::IDENTITY;
    131             (index, node.generation)
    132         } else {
    133             let index = self.nodes.len() as u32;
    134             self.nodes.push(Node {
    135                 local: Transform::default(),
    136                 world_matrix: Mat4::IDENTITY,
    137                 dirty: true,
    138                 generation: 0,
    139                 parent: None,
    140                 first_child: None,
    141                 next_sibling: None,
    142                 model: None,
    143                 alive: true,
    144             });
    145             (index, 0)
    146         }
    147     }
    148 
    149     fn is_valid(&self, id: NodeId) -> bool {
    150         let idx = id.index as usize;
    151         idx < self.nodes.len()
    152             && self.nodes[idx].alive
    153             && self.nodes[idx].generation == id.generation
    154     }
    155 
    156     fn mark_dirty(&mut self, id: NodeId) {
    157         let mut stack = vec![id];
    158         while let Some(nid) = stack.pop() {
    159             let node = &mut self.nodes[nid.index as usize];
    160             if node.dirty {
    161                 continue;
    162             }
    163             node.dirty = true;
    164             let mut child = node.first_child;
    165             while let Some(c) = child {
    166                 stack.push(c);
    167                 child = self.nodes[c.index as usize].next_sibling;
    168             }
    169         }
    170     }
    171 
    172     fn attach_child(&mut self, parent: NodeId, child: NodeId) {
    173         let old_first = self.nodes[parent.index as usize].first_child;
    174         self.nodes[child.index as usize].next_sibling = old_first;
    175         self.nodes[parent.index as usize].first_child = Some(child);
    176     }
    177 
    178     fn detach_child(&mut self, parent: NodeId, child: NodeId) {
    179         let first = self.nodes[parent.index as usize].first_child;
    180         if first == Some(child) {
    181             self.nodes[parent.index as usize].first_child =
    182                 self.nodes[child.index as usize].next_sibling;
    183         } else {
    184             let mut prev = first;
    185             while let Some(p) = prev {
    186                 let next = self.nodes[p.index as usize].next_sibling;
    187                 if next == Some(child) {
    188                     self.nodes[p.index as usize].next_sibling =
    189                         self.nodes[child.index as usize].next_sibling;
    190                     break;
    191                 }
    192                 prev = next;
    193             }
    194         }
    195         self.nodes[child.index as usize].next_sibling = None;
    196     }
    197 
    198     fn is_ancestor(&self, ancestor: NodeId, node: NodeId) -> bool {
    199         let mut cur = Some(node);
    200         while let Some(c) = cur {
    201             if c == ancestor {
    202                 return true;
    203             }
    204             cur = self.nodes[c.index as usize].parent;
    205         }
    206         false
    207     }
    208 
    209     // ── Public scene graph API ───────────────────────────────────
    210 
    211     /// Create a grouping node (no model) with an optional parent.
    212     pub fn create_node(&mut self, local: Transform, parent: Option<NodeId>) -> NodeId {
    213         let (index, generation) = self.alloc_slot();
    214         self.nodes[index as usize].local = local;
    215 
    216         let id = NodeId { index, generation };
    217 
    218         if let Some(p) = parent
    219             && self.is_valid(p)
    220         {
    221             self.nodes[index as usize].parent = Some(p);
    222             self.attach_child(p, id);
    223         }
    224 
    225         id
    226     }
    227 
    228     /// Create a renderable node with a Model and optional parent.
    229     pub fn create_renderable(
    230         &mut self,
    231         model: Model,
    232         local: Transform,
    233         parent: Option<NodeId>,
    234     ) -> NodeId {
    235         let id = self.create_node(local, parent);
    236         self.nodes[id.index as usize].model = Some(model);
    237         self.renderables_dirty = true;
    238         id
    239     }
    240 
    241     /// Remove a node and all its descendants.
    242     pub fn remove_node(&mut self, id: NodeId) -> bool {
    243         if !self.is_valid(id) {
    244             return false;
    245         }
    246 
    247         // Collect all nodes in the subtree
    248         let mut to_remove = Vec::new();
    249         let mut stack = vec![id];
    250         while let Some(nid) = stack.pop() {
    251             to_remove.push(nid);
    252             let mut child = self.nodes[nid.index as usize].first_child;
    253             while let Some(c) = child {
    254                 stack.push(c);
    255                 child = self.nodes[c.index as usize].next_sibling;
    256             }
    257         }
    258 
    259         // Detach root of subtree from its parent
    260         if let Some(parent_id) = self.nodes[id.index as usize].parent {
    261             self.detach_child(parent_id, id);
    262         }
    263 
    264         // Free all collected nodes
    265         for nid in &to_remove {
    266             let node = &mut self.nodes[nid.index as usize];
    267             node.alive = false;
    268             node.first_child = None;
    269             node.next_sibling = None;
    270             node.parent = None;
    271             node.model = None;
    272             self.free_list.push(nid.index);
    273         }
    274 
    275         self.renderables_dirty = true;
    276         true
    277     }
    278 
    279     /// Set a node's local transform. Marks it and descendants dirty.
    280     pub fn set_local_transform(&mut self, id: NodeId, local: Transform) -> bool {
    281         if !self.is_valid(id) {
    282             return false;
    283         }
    284         self.nodes[id.index as usize].local = local;
    285         self.mark_dirty(id);
    286         true
    287     }
    288 
    289     /// Reparent a node. Pass None to make it a root node.
    290     pub fn set_parent(&mut self, id: NodeId, new_parent: Option<NodeId>) -> bool {
    291         if !self.is_valid(id) {
    292             return false;
    293         }
    294         if let Some(p) = new_parent {
    295             if !self.is_valid(p) {
    296                 return false;
    297             }
    298             if self.is_ancestor(id, p) {
    299                 return false;
    300             }
    301         }
    302 
    303         // Detach from old parent
    304         if let Some(old_parent) = self.nodes[id.index as usize].parent {
    305             self.detach_child(old_parent, id);
    306         }
    307 
    308         // Attach to new parent
    309         self.nodes[id.index as usize].parent = new_parent;
    310         if let Some(p) = new_parent {
    311             self.attach_child(p, id);
    312         }
    313 
    314         self.mark_dirty(id);
    315         true
    316     }
    317 
    318     /// Attach or detach a Model on an existing node.
    319     pub fn set_model(&mut self, id: NodeId, model: Option<Model>) -> bool {
    320         if !self.is_valid(id) {
    321             return false;
    322         }
    323         self.nodes[id.index as usize].model = model;
    324         self.renderables_dirty = true;
    325         true
    326     }
    327 
    328     /// Get the cached world matrix for a node.
    329     pub fn world_matrix(&self, id: NodeId) -> Option<Mat4> {
    330         if !self.is_valid(id) {
    331             return None;
    332         }
    333         Some(self.nodes[id.index as usize].world_matrix)
    334     }
    335 
    336     /// Get a node's local transform.
    337     pub fn local_transform(&self, id: NodeId) -> Option<&Transform> {
    338         if !self.is_valid(id) {
    339             return None;
    340         }
    341         Some(&self.nodes[id.index as usize].local)
    342     }
    343 
    344     /// Get a node by id.
    345     pub fn get_node(&self, id: NodeId) -> Option<&Node> {
    346         if !self.is_valid(id) {
    347             return None;
    348         }
    349         Some(&self.nodes[id.index as usize])
    350     }
    351 
    352     /// Get the parent of a node, if it has one.
    353     pub fn node_parent(&self, id: NodeId) -> Option<NodeId> {
    354         if !self.is_valid(id) {
    355             return None;
    356         }
    357         self.nodes[id.index as usize].parent
    358     }
    359 
    360     /// Get the Model handle for a node, if it has one.
    361     pub fn node_model(&self, id: NodeId) -> Option<Model> {
    362         if !self.is_valid(id) {
    363             return None;
    364         }
    365         self.nodes[id.index as usize].model
    366     }
    367 
    368     /// Iterate renderable node ids (nodes with a Model).
    369     pub fn renderables(&self) -> &[NodeId] {
    370         &self.renderables
    371     }
    372 
    373     /// Recompute world matrices for all dirty nodes. Call once per frame.
    374     pub fn update_world_transforms(&mut self) {
    375         // Rebuild renderables list if needed
    376         if self.renderables_dirty {
    377             self.renderables.clear();
    378             for (i, node) in self.nodes.iter().enumerate() {
    379                 if node.alive && node.model.is_some() {
    380                     self.renderables.push(NodeId {
    381                         index: i as u32,
    382                         generation: node.generation,
    383                     });
    384                 }
    385             }
    386             self.renderables_dirty = false;
    387         }
    388 
    389         // Process root nodes (no parent) and recurse into children
    390         for i in 0..self.nodes.len() {
    391             let node = &self.nodes[i];
    392             if !node.alive || !node.dirty || node.parent.is_some() {
    393                 continue;
    394             }
    395             self.nodes[i].world_matrix = self.nodes[i].local.to_matrix();
    396             self.nodes[i].dirty = false;
    397             self.update_children(i);
    398         }
    399 
    400         // Second pass: catch any remaining dirty nodes (reparented mid-frame)
    401         for i in 0..self.nodes.len() {
    402             if self.nodes[i].alive && self.nodes[i].dirty {
    403                 self.recompute_world_matrix(i);
    404             }
    405         }
    406     }
    407 
    408     fn update_children(&mut self, parent_idx: usize) {
    409         let parent_world = self.nodes[parent_idx].world_matrix;
    410         let mut child_id = self.nodes[parent_idx].first_child;
    411         while let Some(cid) = child_id {
    412             let ci = cid.index as usize;
    413             if self.nodes[ci].alive {
    414                 let local = self.nodes[ci].local.to_matrix();
    415                 self.nodes[ci].world_matrix = parent_world * local;
    416                 self.nodes[ci].dirty = false;
    417                 self.update_children(ci);
    418             }
    419             child_id = self.nodes[ci].next_sibling;
    420         }
    421     }
    422 
    423     fn recompute_world_matrix(&mut self, index: usize) {
    424         // Build chain from this node up to root
    425         let mut chain = Vec::with_capacity(8);
    426         let mut cur = index;
    427         loop {
    428             chain.push(cur);
    429             match self.nodes[cur].parent {
    430                 Some(p) if self.nodes[p.index as usize].alive => {
    431                     cur = p.index as usize;
    432                 }
    433                 _ => break,
    434             }
    435         }
    436 
    437         // Walk from root down to target
    438         chain.reverse();
    439         let mut parent_world = Mat4::IDENTITY;
    440         for &idx in &chain {
    441             let node = &self.nodes[idx];
    442             if !node.dirty {
    443                 parent_world = node.world_matrix;
    444                 continue;
    445             }
    446             let world = parent_world * node.local.to_matrix();
    447             self.nodes[idx].world_matrix = world;
    448             self.nodes[idx].dirty = false;
    449             parent_world = world;
    450         }
    451     }
    452 
    453     // ── Backward-compatible API ──────────────────────────────────
    454 
    455     /// Legacy: place a renderable object as a root node.
    456     pub fn add_object(&mut self, model: Model, transform: Transform) -> ObjectId {
    457         self.create_renderable(model, transform, None)
    458     }
    459 
    460     /// Legacy: remove an object.
    461     pub fn remove_object(&mut self, id: ObjectId) -> bool {
    462         self.remove_node(id)
    463     }
    464 
    465     /// Legacy: update an object's transform.
    466     pub fn update_transform(&mut self, id: ObjectId, transform: Transform) -> bool {
    467         self.set_local_transform(id, transform)
    468     }
    469 
    470     /// Legacy: get a node by object id.
    471     pub fn get_object(&self, id: ObjectId) -> Option<&Node> {
    472         self.get_node(id)
    473     }
    474 
    475     /// Number of renderable objects in the scene.
    476     pub fn num_objects(&self) -> usize {
    477         self.renderables.len()
    478     }
    479 }
    480 
    481 #[cfg(test)]
    482 mod tests {
    483     use super::*;
    484     use crate::model::Model;
    485     use glam::Vec3;
    486 
    487     fn test_world() -> World {
    488         World::new(Camera::new(Vec3::new(0.0, 2.0, 5.0), Vec3::ZERO))
    489     }
    490 
    491     fn model(id: u64) -> Model {
    492         Model { id }
    493     }
    494 
    495     // ── Arena basics ──────────────────────────────────────────────
    496 
    497     #[test]
    498     fn create_node_returns_valid_id() {
    499         let mut w = test_world();
    500         let id = w.create_node(Transform::default(), None);
    501         assert!(w.is_valid(id));
    502         assert!(w.get_node(id).is_some());
    503     }
    504 
    505     #[test]
    506     fn create_renderable_appears_in_renderables() {
    507         let mut w = test_world();
    508         let id = w.create_renderable(model(1), Transform::default(), None);
    509         w.update_world_transforms();
    510         assert_eq!(w.renderables().len(), 1);
    511         assert_eq!(w.renderables()[0], id);
    512     }
    513 
    514     #[test]
    515     fn grouping_node_not_in_renderables() {
    516         let mut w = test_world();
    517         w.create_node(Transform::default(), None);
    518         w.update_world_transforms();
    519         assert_eq!(w.renderables().len(), 0);
    520     }
    521 
    522     #[test]
    523     fn multiple_renderables() {
    524         let mut w = test_world();
    525         let a = w.create_renderable(model(1), Transform::default(), None);
    526         let b = w.create_renderable(model(2), Transform::default(), None);
    527         w.update_world_transforms();
    528         assert_eq!(w.num_objects(), 2);
    529         let ids = w.renderables();
    530         assert!(ids.contains(&a));
    531         assert!(ids.contains(&b));
    532     }
    533 
    534     // ── Removal and free list ─────────────────────────────────────
    535 
    536     #[test]
    537     fn remove_node_invalidates_id() {
    538         let mut w = test_world();
    539         let id = w.create_renderable(model(1), Transform::default(), None);
    540         assert!(w.remove_node(id));
    541         assert!(!w.is_valid(id));
    542         assert!(w.get_node(id).is_none());
    543     }
    544 
    545     #[test]
    546     fn remove_node_clears_renderables() {
    547         let mut w = test_world();
    548         let id = w.create_renderable(model(1), Transform::default(), None);
    549         w.update_world_transforms();
    550         assert_eq!(w.num_objects(), 1);
    551         w.remove_node(id);
    552         w.update_world_transforms();
    553         assert_eq!(w.num_objects(), 0);
    554     }
    555 
    556     #[test]
    557     fn stale_handle_after_reuse() {
    558         let mut w = test_world();
    559         let old = w.create_node(Transform::default(), None);
    560         w.remove_node(old);
    561         // Allocate a new node, which should reuse the slot with bumped generation
    562         let new = w.create_node(Transform::default(), None);
    563         assert_eq!(old.index, new.index);
    564         assert_ne!(old.generation, new.generation);
    565         // Old handle must be invalid
    566         assert!(!w.is_valid(old));
    567         assert!(w.is_valid(new));
    568     }
    569 
    570     #[test]
    571     fn remove_nonexistent_returns_false() {
    572         let mut w = test_world();
    573         let fake = NodeId {
    574             index: 99,
    575             generation: 0,
    576         };
    577         assert!(!w.remove_node(fake));
    578     }
    579 
    580     // ── Parent-child relationships ────────────────────────────────
    581 
    582     #[test]
    583     fn create_with_parent() {
    584         let mut w = test_world();
    585         let parent = w.create_node(Transform::default(), None);
    586         let child = w.create_node(Transform::default(), Some(parent));
    587         let parent_node = w.get_node(parent).unwrap();
    588         assert_eq!(parent_node.first_child, Some(child));
    589     }
    590 
    591     #[test]
    592     fn reparent_node() {
    593         let mut w = test_world();
    594         let a = w.create_node(Transform::default(), None);
    595         let b = w.create_node(Transform::default(), None);
    596         let child = w.create_node(Transform::default(), Some(a));
    597 
    598         // Child is under a
    599         assert_eq!(w.get_node(a).unwrap().first_child, Some(child));
    600 
    601         // Reparent to b
    602         assert!(w.set_parent(child, Some(b)));
    603         assert!(w.get_node(a).unwrap().first_child.is_none());
    604         assert_eq!(w.get_node(b).unwrap().first_child, Some(child));
    605     }
    606 
    607     #[test]
    608     fn reparent_to_none_makes_root() {
    609         let mut w = test_world();
    610         let parent = w.create_node(Transform::default(), None);
    611         let child = w.create_node(Transform::default(), Some(parent));
    612         assert!(w.set_parent(child, None));
    613         assert!(w.get_node(parent).unwrap().first_child.is_none());
    614     }
    615 
    616     #[test]
    617     fn cycle_prevention() {
    618         let mut w = test_world();
    619         let a = w.create_node(Transform::default(), None);
    620         let b = w.create_node(Transform::default(), Some(a));
    621         let c = w.create_node(Transform::default(), Some(b));
    622 
    623         // Trying to make a a child of c should fail (c -> b -> a cycle)
    624         assert!(!w.set_parent(a, Some(c)));
    625 
    626         // Trying to make a a child of b should also fail
    627         assert!(!w.set_parent(a, Some(b)));
    628 
    629         // Self-parenting should fail
    630         assert!(!w.set_parent(a, Some(a)));
    631     }
    632 
    633     #[test]
    634     fn remove_subtree() {
    635         let mut w = test_world();
    636         let root = w.create_node(Transform::default(), None);
    637         let child = w.create_renderable(model(1), Transform::default(), Some(root));
    638         let grandchild = w.create_renderable(model(2), Transform::default(), Some(child));
    639 
    640         w.remove_node(root);
    641 
    642         assert!(!w.is_valid(root));
    643         assert!(!w.is_valid(child));
    644         assert!(!w.is_valid(grandchild));
    645     }
    646 
    647     #[test]
    648     fn remove_child_detaches_from_parent() {
    649         let mut w = test_world();
    650         let parent = w.create_node(Transform::default(), None);
    651         let c1 = w.create_node(Transform::default(), Some(parent));
    652         let c2 = w.create_node(Transform::default(), Some(parent));
    653 
    654         w.remove_node(c1);
    655 
    656         // Parent should still have c2
    657         assert!(w.is_valid(parent));
    658         assert!(w.is_valid(c2));
    659         let parent_node = w.get_node(parent).unwrap();
    660         assert_eq!(parent_node.first_child, Some(c2));
    661     }
    662 
    663     // ── Transform computation ─────────────────────────────────────
    664 
    665     #[test]
    666     fn root_world_matrix_equals_local() {
    667         let mut w = test_world();
    668         let t = Transform::from_translation(Vec3::new(1.0, 2.0, 3.0));
    669         let expected = t.to_matrix();
    670         let id = w.create_node(t, None);
    671         w.update_world_transforms();
    672         assert_eq!(w.world_matrix(id).unwrap(), expected);
    673     }
    674 
    675     #[test]
    676     fn child_inherits_parent_transform() {
    677         let mut w = test_world();
    678         let parent_t = Transform::from_translation(Vec3::new(10.0, 0.0, 0.0));
    679         let child_t = Transform::from_translation(Vec3::new(0.0, 5.0, 0.0));
    680 
    681         let parent = w.create_node(parent_t.clone(), None);
    682         let child = w.create_node(child_t.clone(), Some(parent));
    683         w.update_world_transforms();
    684 
    685         let expected = parent_t.to_matrix() * child_t.to_matrix();
    686         let actual = w.world_matrix(child).unwrap();
    687 
    688         // Check that the child's world position is (10, 5, 0)
    689         let pos = actual.col(3);
    690         assert!((pos.x - 10.0).abs() < 1e-5);
    691         assert!((pos.y - 5.0).abs() < 1e-5);
    692         assert!((pos.z - 0.0).abs() < 1e-5);
    693         assert_eq!(actual, expected);
    694     }
    695 
    696     #[test]
    697     fn grandchild_transform_chain() {
    698         let mut w = test_world();
    699         let t1 = Transform::from_translation(Vec3::new(1.0, 0.0, 0.0));
    700         let t2 = Transform::from_translation(Vec3::new(0.0, 2.0, 0.0));
    701         let t3 = Transform::from_translation(Vec3::new(0.0, 0.0, 3.0));
    702 
    703         let a = w.create_node(t1.clone(), None);
    704         let b = w.create_node(t2.clone(), Some(a));
    705         let c = w.create_node(t3.clone(), Some(b));
    706         w.update_world_transforms();
    707 
    708         let world_c = w.world_matrix(c).unwrap();
    709         let pos = world_c.col(3);
    710         assert!((pos.x - 1.0).abs() < 1e-5);
    711         assert!((pos.y - 2.0).abs() < 1e-5);
    712         assert!((pos.z - 3.0).abs() < 1e-5);
    713     }
    714 
    715     // ── Dirty flag propagation ────────────────────────────────────
    716 
    717     #[test]
    718     fn moving_parent_updates_children() {
    719         let mut w = test_world();
    720         let parent = w.create_node(Transform::from_translation(Vec3::X), None);
    721         let child = w.create_node(Transform::from_translation(Vec3::Y), Some(parent));
    722         w.update_world_transforms();
    723 
    724         // Verify initial position
    725         let pos = w.world_matrix(child).unwrap().col(3);
    726         assert!((pos.x - 1.0).abs() < 1e-5);
    727         assert!((pos.y - 1.0).abs() < 1e-5);
    728 
    729         // Move parent
    730         w.set_local_transform(
    731             parent,
    732             Transform::from_translation(Vec3::new(5.0, 0.0, 0.0)),
    733         );
    734         w.update_world_transforms();
    735 
    736         // Child should now be at (5, 1, 0)
    737         let pos = w.world_matrix(child).unwrap().col(3);
    738         assert!((pos.x - 5.0).abs() < 1e-5);
    739         assert!((pos.y - 1.0).abs() < 1e-5);
    740     }
    741 
    742     #[test]
    743     fn set_local_transform_invalid_id() {
    744         let mut w = test_world();
    745         let fake = NodeId {
    746             index: 0,
    747             generation: 99,
    748         };
    749         assert!(!w.set_local_transform(fake, Transform::default()));
    750     }
    751 
    752     // ── set_model ─────────────────────────────────────────────────
    753 
    754     #[test]
    755     fn attach_model_to_grouping_node() {
    756         let mut w = test_world();
    757         let id = w.create_node(Transform::default(), None);
    758         w.update_world_transforms();
    759         assert_eq!(w.num_objects(), 0);
    760 
    761         w.set_model(id, Some(model(42)));
    762         w.update_world_transforms();
    763         assert_eq!(w.num_objects(), 1);
    764     }
    765 
    766     #[test]
    767     fn detach_model_from_renderable() {
    768         let mut w = test_world();
    769         let id = w.create_renderable(model(1), Transform::default(), None);
    770         w.update_world_transforms();
    771         assert_eq!(w.num_objects(), 1);
    772 
    773         w.set_model(id, None);
    774         w.update_world_transforms();
    775         assert_eq!(w.num_objects(), 0);
    776         // Node still valid, just no longer renderable
    777         assert!(w.is_valid(id));
    778     }
    779 
    780     // ── Backward-compatible API ───────────────────────────────────
    781 
    782     #[test]
    783     fn legacy_add_remove_object() {
    784         let mut w = test_world();
    785         let id = w.add_object(model(1), Transform::from_translation(Vec3::Z));
    786         w.update_world_transforms();
    787         assert_eq!(w.num_objects(), 1);
    788         assert!(w.get_object(id).is_some());
    789 
    790         assert!(w.remove_object(id));
    791         w.update_world_transforms();
    792         assert_eq!(w.num_objects(), 0);
    793     }
    794 
    795     #[test]
    796     fn legacy_update_transform() {
    797         let mut w = test_world();
    798         let id = w.add_object(model(1), Transform::from_translation(Vec3::ZERO));
    799         w.update_world_transforms();
    800 
    801         let new_t = Transform::from_translation(Vec3::new(7.0, 8.0, 9.0));
    802         assert!(w.update_transform(id, new_t));
    803         w.update_world_transforms();
    804 
    805         let pos = w.world_matrix(id).unwrap().col(3);
    806         assert!((pos.x - 7.0).abs() < 1e-5);
    807         assert!((pos.y - 8.0).abs() < 1e-5);
    808         assert!((pos.z - 9.0).abs() < 1e-5);
    809     }
    810 
    811     // ── Multiple siblings ─────────────────────────────────────────
    812 
    813     #[test]
    814     fn multiple_children_all_transform_correctly() {
    815         let mut w = test_world();
    816         let parent = w.create_node(Transform::from_translation(Vec3::new(10.0, 0.0, 0.0)), None);
    817         let c1 = w.create_node(
    818             Transform::from_translation(Vec3::new(1.0, 0.0, 0.0)),
    819             Some(parent),
    820         );
    821         let c2 = w.create_node(
    822             Transform::from_translation(Vec3::new(2.0, 0.0, 0.0)),
    823             Some(parent),
    824         );
    825         let c3 = w.create_node(
    826             Transform::from_translation(Vec3::new(3.0, 0.0, 0.0)),
    827             Some(parent),
    828         );
    829         w.update_world_transforms();
    830 
    831         assert!((w.world_matrix(c1).unwrap().col(3).x - 11.0).abs() < 1e-5);
    832         assert!((w.world_matrix(c2).unwrap().col(3).x - 12.0).abs() < 1e-5);
    833         assert!((w.world_matrix(c3).unwrap().col(3).x - 13.0).abs() < 1e-5);
    834     }
    835 
    836     #[test]
    837     fn remove_middle_sibling() {
    838         let mut w = test_world();
    839         let parent = w.create_node(Transform::default(), None);
    840         let c1 = w.create_node(Transform::default(), Some(parent));
    841         let c2 = w.create_node(Transform::default(), Some(parent));
    842         let c3 = w.create_node(Transform::default(), Some(parent));
    843 
    844         w.remove_node(c2);
    845 
    846         assert!(w.is_valid(c1));
    847         assert!(!w.is_valid(c2));
    848         assert!(w.is_valid(c3));
    849 
    850         // Parent should still link to c1 and c3
    851         // (linked list: c3 -> c1 after c2 removed, since prepend order is c3, c2, c1)
    852         let mut count = 0;
    853         let mut cur = w.get_node(parent).unwrap().first_child;
    854         while let Some(c) = cur {
    855             count += 1;
    856             cur = w.get_node(c).unwrap().next_sibling;
    857         }
    858         assert_eq!(count, 2);
    859     }
    860 
    861     // ── Scale and rotation ────────────────────────────────────────
    862 
    863     #[test]
    864     fn scaled_parent_affects_child_position() {
    865         let mut w = test_world();
    866         let parent_t = Transform {
    867             translation: Vec3::ZERO,
    868             rotation: Quat::IDENTITY,
    869             scale: Vec3::splat(2.0),
    870         };
    871         let child_t = Transform::from_translation(Vec3::new(1.0, 0.0, 0.0));
    872 
    873         let parent = w.create_node(parent_t, None);
    874         let child = w.create_node(child_t, Some(parent));
    875         w.update_world_transforms();
    876 
    877         // Child at local (1,0,0) under 2x scale parent should be at world (2,0,0)
    878         let pos = w.world_matrix(child).unwrap().col(3);
    879         assert!((pos.x - 2.0).abs() < 1e-5);
    880     }
    881 
    882     // ── Reparent updates transforms ───────────────────────────────
    883 
    884     #[test]
    885     fn reparent_recomputes_world_matrix() {
    886         let mut w = test_world();
    887         let a = w.create_node(Transform::from_translation(Vec3::new(10.0, 0.0, 0.0)), None);
    888         let b = w.create_node(Transform::from_translation(Vec3::new(20.0, 0.0, 0.0)), None);
    889         let child = w.create_node(
    890             Transform::from_translation(Vec3::new(1.0, 0.0, 0.0)),
    891             Some(a),
    892         );
    893         w.update_world_transforms();
    894 
    895         // Under a: world x = 11
    896         assert!((w.world_matrix(child).unwrap().col(3).x - 11.0).abs() < 1e-5);
    897 
    898         // Reparent to b
    899         w.set_parent(child, Some(b));
    900         w.update_world_transforms();
    901 
    902         // Under b: world x = 21
    903         assert!((w.world_matrix(child).unwrap().col(3).x - 21.0).abs() < 1e-5);
    904     }
    905 
    906     // ── Edge case: empty world ────────────────────────────────────
    907 
    908     #[test]
    909     fn empty_world_update_is_safe() {
    910         let mut w = test_world();
    911         w.update_world_transforms();
    912         assert_eq!(w.renderables().len(), 0);
    913     }
    914 
    915     #[test]
    916     fn world_matrix_invalid_id_returns_none() {
    917         let w = test_world();
    918         let fake = NodeId {
    919             index: 0,
    920             generation: 0,
    921         };
    922         assert!(w.world_matrix(fake).is_none());
    923     }
    924 }