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 }