Skip to main content

oxide_transform/
lib.rs

1//! Transform and hierarchy components plus propagation utilities.
2
3use glam::{Mat4, Quat, Vec3};
4use oxide_ecs::entity::Entity;
5use oxide_ecs::world::World;
6use oxide_ecs::{query, Component};
7use oxide_math::transform::Transform;
8
9#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Parent(pub Entity);
11
12#[derive(Component, Clone, Debug, Default)]
13pub struct Children(pub Vec<Entity>);
14
15impl Children {
16    pub fn new() -> Self {
17        Self(Vec::new())
18    }
19
20    pub fn with(entities: Vec<Entity>) -> Self {
21        Self(entities)
22    }
23
24    pub fn push(&mut self, entity: Entity) {
25        self.0.push(entity);
26    }
27
28    pub fn remove(&mut self, entity: Entity) {
29        self.0.retain(|&e| e != entity);
30    }
31
32    pub fn len(&self) -> usize {
33        self.0.len()
34    }
35
36    pub fn is_empty(&self) -> bool {
37        self.0.is_empty()
38    }
39
40    pub fn iter(&self) -> impl Iterator<Item = Entity> + '_ {
41        self.0.iter().copied()
42    }
43}
44
45#[derive(Component, Clone, Debug)]
46pub struct TransformComponent {
47    pub transform: Transform,
48    pub is_dirty: bool,
49}
50
51impl Default for TransformComponent {
52    fn default() -> Self {
53        Self {
54            transform: Transform {
55                position: Vec3::ZERO,
56                rotation: Quat::IDENTITY,
57                scale: Vec3::ONE,
58            },
59            is_dirty: true,
60        }
61    }
62}
63
64impl TransformComponent {
65    pub fn new(transform: Transform) -> Self {
66        Self {
67            transform,
68            is_dirty: true,
69        }
70    }
71
72    pub fn from_position(position: Vec3) -> Self {
73        Self::new(Transform {
74            position,
75            rotation: Quat::IDENTITY,
76            scale: Vec3::ONE,
77        })
78    }
79
80    pub fn from_position_rotation(position: Vec3, rotation: Quat) -> Self {
81        Self::new(Transform {
82            position,
83            rotation,
84            scale: Vec3::ONE,
85        })
86    }
87
88    pub fn to_matrix(&self) -> Mat4 {
89        self.transform.to_matrix()
90    }
91
92    pub fn mark_dirty(&mut self) {
93        self.is_dirty = true;
94    }
95
96    pub fn clear_dirty(&mut self) {
97        self.is_dirty = false;
98    }
99
100    pub fn set_transform(&mut self, transform: Transform) {
101        self.transform = transform;
102        self.mark_dirty();
103    }
104
105    pub fn transform_mut(&mut self) -> &mut Transform {
106        self.mark_dirty();
107        &mut self.transform
108    }
109}
110
111impl From<Transform> for TransformComponent {
112    fn from(transform: Transform) -> Self {
113        Self::new(transform)
114    }
115}
116
117impl From<TransformComponent> for Transform {
118    fn from(component: TransformComponent) -> Self {
119        component.transform
120    }
121}
122
123#[derive(Component, Clone, Copy, Debug)]
124pub struct GlobalTransform {
125    pub matrix: Mat4,
126}
127
128impl Default for GlobalTransform {
129    fn default() -> Self {
130        Self {
131            matrix: Mat4::IDENTITY,
132        }
133    }
134}
135
136impl GlobalTransform {
137    pub fn from_matrix(matrix: Mat4) -> Self {
138        Self { matrix }
139    }
140
141    pub fn identity() -> Self {
142        Self {
143            matrix: Mat4::IDENTITY,
144        }
145    }
146
147    pub fn position(&self) -> Vec3 {
148        self.matrix.col(3).truncate()
149    }
150
151    pub fn mul(&self, other: &GlobalTransform) -> GlobalTransform {
152        GlobalTransform {
153            matrix: self.matrix * other.matrix,
154        }
155    }
156}
157
158pub fn attach_child(world: &mut World, parent: Entity, child: Entity) {
159    if let Some(existing_parent) = world.get::<Parent>(child).copied() {
160        if existing_parent.0 != parent {
161            if let Some(old_children) = world.get_mut::<Children>(existing_parent.0) {
162                old_children.remove(child);
163            }
164        }
165    }
166
167    world.entity_mut(child).insert(Parent(parent));
168
169    if let Some(children) = world.get_mut::<Children>(parent) {
170        if !children.0.contains(&child) {
171            children.push(child);
172        }
173    } else {
174        world.entity_mut(parent).insert(Children::with(vec![child]));
175    }
176
177    mark_subtree_dirty(world, child);
178}
179
180pub fn detach_child(world: &mut World, parent: Entity, child: Entity) {
181    if let Some(children) = world.get_mut::<Children>(parent) {
182        children.remove(child);
183    }
184    world.entity_mut(child).remove::<Parent>();
185    mark_subtree_dirty(world, child);
186}
187
188pub fn mark_subtree_dirty(world: &mut World, root: Entity) {
189    if let Some(local) = world.get_mut::<TransformComponent>(root) {
190        local.mark_dirty();
191    }
192
193    let children = world
194        .get::<Children>(root)
195        .map(|c| c.0.clone())
196        .unwrap_or_default();
197
198    for child in children {
199        mark_subtree_dirty(world, child);
200    }
201}
202
203pub fn transform_propagate_system(world: &mut World) {
204    let root_entities: Vec<Entity> = {
205        let mut roots = Vec::new();
206        let mut query = world
207            .query_filtered::<Entity, (query::With<TransformComponent>, query::Without<Parent>)>();
208        for entity in query.iter(world) {
209            roots.push(entity);
210        }
211        roots
212    };
213
214    for root in root_entities {
215        propagate_from_root(world, root, GlobalTransform::identity(), false);
216    }
217}
218
219fn propagate_from_root(
220    world: &mut World,
221    entity: Entity,
222    parent_global: GlobalTransform,
223    parent_changed: bool,
224) {
225    let (local_matrix, local_dirty) = match world.get::<TransformComponent>(entity) {
226        Some(transform) => (transform.to_matrix(), transform.is_dirty),
227        None => (Mat4::IDENTITY, false),
228    };
229
230    let computed_global = GlobalTransform::from_matrix(parent_global.matrix * local_matrix);
231    let existing_global = world.get::<GlobalTransform>(entity).copied();
232    let should_update = parent_changed || local_dirty || existing_global.is_none();
233
234    if should_update {
235        if let Some(global) = world.get_mut::<GlobalTransform>(entity) {
236            *global = computed_global;
237        } else {
238            world.entity_mut(entity).insert(computed_global);
239        }
240    }
241
242    if local_dirty {
243        if let Some(local) = world.get_mut::<TransformComponent>(entity) {
244            local.clear_dirty();
245        }
246    }
247
248    let current_global = if should_update {
249        computed_global
250    } else {
251        existing_global.unwrap_or(computed_global)
252    };
253
254    let children = world
255        .get::<Children>(entity)
256        .map(|children| children.0.clone())
257        .unwrap_or_default();
258
259    for child in children {
260        propagate_from_root(world, child, current_global, should_update);
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use glam::Vec3;
268
269    #[test]
270    fn transform_propagation_works_for_parent_child() {
271        let mut world = World::new();
272
273        let root = world
274            .spawn((
275                TransformComponent::from_position(Vec3::new(1.0, 0.0, 0.0)),
276                GlobalTransform::default(),
277            ))
278            .id();
279
280        let child = world
281            .spawn((
282                TransformComponent::from_position(Vec3::new(0.0, 2.0, 0.0)),
283                GlobalTransform::default(),
284            ))
285            .id();
286
287        attach_child(&mut world, root, child);
288        transform_propagate_system(&mut world);
289
290        let root_global = world
291            .get::<GlobalTransform>(root)
292            .expect("root global transform should exist");
293        assert_eq!(root_global.position(), Vec3::new(1.0, 0.0, 0.0));
294
295        let child_global = world
296            .get::<GlobalTransform>(child)
297            .expect("child global transform should exist");
298        assert_eq!(child_global.position(), Vec3::new(1.0, 2.0, 0.0));
299    }
300
301    #[test]
302    fn dirty_transform_marks_and_clears() {
303        let mut world = World::new();
304        let entity = world
305            .spawn((TransformComponent::default(), GlobalTransform::default()))
306            .id();
307
308        transform_propagate_system(&mut world);
309        assert!(
310            !world
311                .get::<TransformComponent>(entity)
312                .expect("transform component should exist")
313                .is_dirty
314        );
315
316        let transform = world
317            .get_mut::<TransformComponent>(entity)
318            .expect("transform component should exist");
319        transform.transform_mut().position = Vec3::new(3.0, 0.0, 0.0);
320
321        transform_propagate_system(&mut world);
322
323        let global = world
324            .get::<GlobalTransform>(entity)
325            .expect("global transform should exist");
326        assert_eq!(global.position(), Vec3::new(3.0, 0.0, 0.0));
327        assert!(
328            !world
329                .get::<TransformComponent>(entity)
330                .expect("transform component should exist")
331                .is_dirty
332        );
333    }
334}