Skip to main content

gizmo_engine/systems/
transform.rs

1pub struct TransformSyncSystem;
2
3impl gizmo_core::system::System for TransformSyncSystem {
4    fn access_info(&self) -> gizmo_core::system::AccessInfo {
5        let mut info = gizmo_core::system::AccessInfo::new();
6        // Since we borrow components, we can mark it as exclusive to be safe, or specify component access.
7        // For simplicity and safety during hierarchy traversal, we'll make it exclusive.
8        info.is_exclusive = true;
9        info
10    }
11
12    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
13        // SAFETY: scheduled system; the scheduler guarantees no other system mutably
14        // aliases Transform while this runs (see `World::query_unchecked`).
15        if let Some(mut transforms) = unsafe {
16            world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::Transform>>()
17        } {
18            for (_, mut trans) in transforms.iter_mut() {
19                trans.update_local_matrix();
20            }
21        }
22    }
23}
24
25pub struct TransformPropagateSystem;
26
27impl gizmo_core::system::System for TransformPropagateSystem {
28    fn access_info(&self) -> gizmo_core::system::AccessInfo {
29        let mut info = gizmo_core::system::AccessInfo::new();
30        info.is_exclusive = true; // Safe fallback for complex queries
31        info
32    }
33
34    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
35        // Query to get root transforms (no Parent)
36        // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
37        let root_query = unsafe {
38            world.query_unchecked::<(
39                &gizmo_physics_core::Transform,
40                gizmo_core::query::Mut<gizmo_physics_core::components::GlobalTransform>,
41                gizmo_core::query::Without<gizmo_core::component::Parent>,
42            )>()
43        };
44
45        let mut queue = Vec::new();
46        // A `Children` cycle (reachable if the editor reparents an entity onto its own
47        // descendant) would otherwise grow `queue` forever — this system runs EVERY
48        // frame, so a single cyclic edit hangs the whole app. Track visited ids and
49        // never enqueue one twice. For a valid tree this is a no-op (each node has one
50        // parent → is enqueued once).
51        let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
52
53        if let Some(mut roots) = root_query {
54            let mut children_query = world.query::<&gizmo_core::component::Children>();
55            for (id, (local, mut global, _)) in roots.iter_mut() {
56                global.matrix = local.local_matrix;
57                visited.insert(id);
58                if let Some(children_q) = &mut children_query {
59                    if let Some(children) = children_q.get(id) {
60                        for &child_id in &children.0 {
61                            if visited.insert(child_id) {
62                                queue.push((global.matrix, child_id));
63                            }
64                        }
65                    }
66                }
67            }
68        }
69
70        // Processing children (we need random access, so we do individual queries)
71        let mut local_query = world.query::<&gizmo_physics_core::Transform>();
72        // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
73        let mut global_query = unsafe {
74            world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::components::GlobalTransform>>()
75        };
76        let mut children_query = world.query::<&gizmo_core::component::Children>();
77
78        let mut head = 0;
79        while head < queue.len() {
80            let (parent_matrix, current_id) = queue[head];
81            head += 1;
82
83            if let (Some(lq), Some(gq)) = (&mut local_query, &mut global_query) {
84                if let (Some(local), Some(mut global)) = (lq.get(current_id), gq.get_mut(current_id)) {
85                    global.matrix = parent_matrix * local.local_matrix;
86
87                    if let Some(cq) = &mut children_query {
88                        if let Some(children) = cq.get(current_id) {
89                            for &child_id in &children.0 {
90                                if visited.insert(child_id) {
91                                    queue.push((global.matrix, child_id));
92                                }
93                            }
94                        }
95                    }
96                }
97            }
98        }
99    }
100}
101
102pub struct BoneAttachmentSystem;
103
104impl gizmo_core::system::System for BoneAttachmentSystem {
105    fn access_info(&self) -> gizmo_core::system::AccessInfo {
106        let mut info = gizmo_core::system::AccessInfo::new();
107        info.is_exclusive = true;
108        info
109    }
110
111    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
112        if let Some(query) = world.query::<&gizmo_renderer::components::BoneAttachment>() {
113            let mut skeletons = world.query::<&gizmo_renderer::components::Skeleton>();
114            // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
115            let mut transforms = unsafe {
116                world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::Transform>>()
117            };
118
119            for (id, attachment) in query.iter() {
120                if let Some(sq) = &mut skeletons {
121                    if let Some(skeleton) = sq.get(attachment.target_entity.id()) {
122                        if let Some(global_matrix) = skeleton.global_poses.get(attachment.bone_index) {
123                            if let Some(tq) = &mut transforms {
124                                if let Some(mut trans) = tq.get_mut(id) {
125                                    let final_mat = *global_matrix * attachment.offset;
126                                    let (t, r, s) = gizmo_renderer::decompose_mat4(final_mat);
127                                    trans.position = t;
128                                    trans.rotation = r;
129                                    trans.scale = s;
130                                    trans.update_local_matrix();
131                                }
132                            }
133                        }
134                    }
135                }
136            }
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use gizmo_core::component::Children;
145    use gizmo_core::system::System;
146    use gizmo_core::world::World;
147    use gizmo_physics_core::components::GlobalTransform;
148    use gizmo_physics_core::Transform;
149
150    #[test]
151    fn transform_propagate_terminates_on_children_cycle() {
152        let mut world = World::new();
153        let a = world.spawn();
154        let b = world.spawn();
155        world.add_component(a, Transform::default());
156        world.add_component(a, GlobalTransform::default());
157        world.add_component(b, Transform::default());
158        world.add_component(b, GlobalTransform::default());
159        // A `Children` cycle with neither node parented (both are propagation roots).
160        // The old BFS had no visited set → its queue grew forever and this per-frame
161        // system hung the whole app. Completing at all is the assertion.
162        world.add_component(a, Children(vec![b.id()]));
163        world.add_component(b, Children(vec![a.id()]));
164
165        let mut sys = TransformPropagateSystem;
166        sys.run(&world, 0.0);
167    }
168}