feat(ecs): add WorldTransform propagation through parent-child hierarchy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 20:20:46 +09:00
parent 3e475c93dd
commit 135364ca6d
2 changed files with 137 additions and 0 deletions

View File

@@ -3,9 +3,11 @@ pub mod sparse_set;
pub mod world;
pub mod transform;
pub mod hierarchy;
pub mod world_transform;
pub use entity::{Entity, EntityAllocator};
pub use sparse_set::SparseSet;
pub use world::World;
pub use transform::Transform;
pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots};
pub use world_transform::{WorldTransform, propagate_transforms};

View File

@@ -0,0 +1,135 @@
use voltex_math::Mat4;
use crate::{Entity, World, Transform};
use crate::hierarchy::{Parent, Children};
#[derive(Debug, Clone, Copy)]
pub struct WorldTransform(pub Mat4);
impl WorldTransform {
pub fn identity() -> Self { Self(Mat4::IDENTITY) }
}
pub fn propagate_transforms(world: &mut World) {
// Collect roots: entities with Transform but no Parent
let roots: Vec<Entity> = world.query::<Transform>()
.filter(|(e, _)| world.get::<Parent>(*e).is_none())
.map(|(e, _)| e)
.collect();
for root in roots {
propagate_entity(world, root, Mat4::IDENTITY);
}
}
fn propagate_entity(world: &mut World, entity: Entity, parent_world: Mat4) {
let local = match world.get::<Transform>(entity) {
Some(t) => t.matrix(),
None => return,
};
let world_matrix = parent_world * local;
world.add(entity, WorldTransform(world_matrix));
// Clone children to avoid borrow issues
let children: Vec<Entity> = world.get::<Children>(entity)
.map(|c| c.0.clone())
.unwrap_or_default();
for child in children {
propagate_entity(world, child, world_matrix);
}
}
#[cfg(test)]
mod tests {
use super::*;
use voltex_math::{Vec3, Vec4};
use crate::hierarchy::add_child;
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn test_root_world_transform() {
let mut world = World::new();
let e = world.spawn();
world.add(e, Transform::from_position(Vec3::new(5.0, 0.0, 0.0)));
propagate_transforms(&mut world);
let wt = world.get::<WorldTransform>(e).expect("WorldTransform should be set");
// Transform the origin — should land at (5, 0, 0)
let result = wt.0 * Vec4::new(0.0, 0.0, 0.0, 1.0);
assert!(approx_eq(result.x, 5.0), "x: {}", result.x);
assert!(approx_eq(result.y, 0.0), "y: {}", result.y);
assert!(approx_eq(result.z, 0.0), "z: {}", result.z);
}
#[test]
fn test_child_inherits_parent() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
world.add(parent, Transform::from_position(Vec3::new(10.0, 0.0, 0.0)));
world.add(child, Transform::from_position(Vec3::new(0.0, 5.0, 0.0)));
add_child(&mut world, parent, child);
propagate_transforms(&mut world);
let wt = world.get::<WorldTransform>(child).expect("child WorldTransform should be set");
// Child origin in world space should be (10, 5, 0)
let result = wt.0 * Vec4::new(0.0, 0.0, 0.0, 1.0);
assert!(approx_eq(result.x, 10.0), "x: {}", result.x);
assert!(approx_eq(result.y, 5.0), "y: {}", result.y);
assert!(approx_eq(result.z, 0.0), "z: {}", result.z);
}
#[test]
fn test_three_level_hierarchy() {
let mut world = World::new();
let root = world.spawn();
let mid = world.spawn();
let leaf = world.spawn();
world.add(root, Transform::from_position(Vec3::new(1.0, 0.0, 0.0)));
world.add(mid, Transform::from_position(Vec3::new(0.0, 2.0, 0.0)));
world.add(leaf, Transform::from_position(Vec3::new(0.0, 0.0, 3.0)));
add_child(&mut world, root, mid);
add_child(&mut world, mid, leaf);
propagate_transforms(&mut world);
let wt = world.get::<WorldTransform>(leaf).expect("leaf WorldTransform should be set");
// Leaf origin in world space should be (1, 2, 3)
let result = wt.0 * Vec4::new(0.0, 0.0, 0.0, 1.0);
assert!(approx_eq(result.x, 1.0), "x: {}", result.x);
assert!(approx_eq(result.y, 2.0), "y: {}", result.y);
assert!(approx_eq(result.z, 3.0), "z: {}", result.z);
}
#[test]
fn test_parent_scale_affects_child() {
let mut world = World::new();
let parent = world.spawn();
let child = world.spawn();
// Parent scaled 2x at origin
world.add(parent, Transform::from_position_scale(
Vec3::ZERO,
Vec3::new(2.0, 2.0, 2.0),
));
// Child at local (1, 0, 0)
world.add(child, Transform::from_position(Vec3::new(1.0, 0.0, 0.0)));
add_child(&mut world, parent, child);
propagate_transforms(&mut world);
let wt = world.get::<WorldTransform>(child).expect("child WorldTransform should be set");
// Child origin in world space: parent scale 2x means (1,0,0) -> (2,0,0)
let result = wt.0 * Vec4::new(0.0, 0.0, 0.0, 1.0);
assert!(approx_eq(result.x, 2.0), "x: {}", result.x);
assert!(approx_eq(result.y, 0.0), "y: {}", result.y);
assert!(approx_eq(result.z, 0.0), "z: {}", result.z);
}
}