feat(ecs): add WorldTransform propagation through parent-child hierarchy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,11 @@ pub mod sparse_set;
|
|||||||
pub mod world;
|
pub mod world;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
pub mod hierarchy;
|
pub mod hierarchy;
|
||||||
|
pub mod world_transform;
|
||||||
|
|
||||||
pub use entity::{Entity, EntityAllocator};
|
pub use entity::{Entity, EntityAllocator};
|
||||||
pub use sparse_set::SparseSet;
|
pub use sparse_set::SparseSet;
|
||||||
pub use world::World;
|
pub use world::World;
|
||||||
pub use transform::Transform;
|
pub use transform::Transform;
|
||||||
pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots};
|
pub use hierarchy::{Parent, Children, add_child, remove_child, despawn_recursive, roots};
|
||||||
|
pub use world_transform::{WorldTransform, propagate_transforms};
|
||||||
|
|||||||
135
crates/voltex_ecs/src/world_transform.rs
Normal file
135
crates/voltex_ecs/src/world_transform.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user