gizmo-engine 0.8.0

A custom ECS and physics engine aimed for realistic simulations.
Documentation
pub struct TransformSyncSystem;

impl gizmo_core::system::System for TransformSyncSystem {
    fn access_info(&self) -> gizmo_core::system::AccessInfo {
        let mut info = gizmo_core::system::AccessInfo::new();
        // Since we borrow components, we can mark it as exclusive to be safe, or specify component access.
        // For simplicity and safety during hierarchy traversal, we'll make it exclusive.
        info.is_exclusive = true;
        info
    }

    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
        // SAFETY: scheduled system; the scheduler guarantees no other system mutably
        // aliases Transform while this runs (see `World::query_unchecked`).
        if let Some(mut transforms) = unsafe {
            world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::Transform>>()
        } {
            for (_, mut trans) in transforms.iter_mut() {
                trans.update_local_matrix();
            }
        }
    }
}

pub struct TransformPropagateSystem;

impl gizmo_core::system::System for TransformPropagateSystem {
    fn access_info(&self) -> gizmo_core::system::AccessInfo {
        let mut info = gizmo_core::system::AccessInfo::new();
        info.is_exclusive = true; // Safe fallback for complex queries
        info
    }

    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
        // Query to get root transforms (no Parent)
        // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
        let root_query = unsafe {
            world.query_unchecked::<(
                &gizmo_physics_core::Transform,
                gizmo_core::query::Mut<gizmo_physics_core::components::GlobalTransform>,
                gizmo_core::query::Without<gizmo_core::component::Parent>,
            )>()
        };

        let mut queue = Vec::new();
        // A `Children` cycle (reachable if the editor reparents an entity onto its own
        // descendant) would otherwise grow `queue` forever — this system runs EVERY
        // frame, so a single cyclic edit hangs the whole app. Track visited ids and
        // never enqueue one twice. For a valid tree this is a no-op (each node has one
        // parent → is enqueued once).
        let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();

        if let Some(mut roots) = root_query {
            let mut children_query = world.query::<&gizmo_core::component::Children>();
            for (id, (local, mut global, _)) in roots.iter_mut() {
                global.matrix = local.local_matrix;
                visited.insert(id);
                if let Some(children_q) = &mut children_query {
                    if let Some(children) = children_q.get(id) {
                        for &child_id in &children.0 {
                            if visited.insert(child_id) {
                                queue.push((global.matrix, child_id));
                            }
                        }
                    }
                }
            }
        }

        // Processing children (we need random access, so we do individual queries)
        let mut local_query = world.query::<&gizmo_physics_core::Transform>();
        // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
        let mut global_query = unsafe {
            world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::components::GlobalTransform>>()
        };
        let mut children_query = world.query::<&gizmo_core::component::Children>();

        let mut head = 0;
        while head < queue.len() {
            let (parent_matrix, current_id) = queue[head];
            head += 1;

            if let (Some(lq), Some(gq)) = (&mut local_query, &mut global_query) {
                if let (Some(local), Some(mut global)) = (lq.get(current_id), gq.get_mut(current_id)) {
                    global.matrix = parent_matrix * local.local_matrix;

                    if let Some(cq) = &mut children_query {
                        if let Some(children) = cq.get(current_id) {
                            for &child_id in &children.0 {
                                if visited.insert(child_id) {
                                    queue.push((global.matrix, child_id));
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

pub struct BoneAttachmentSystem;

impl gizmo_core::system::System for BoneAttachmentSystem {
    fn access_info(&self) -> gizmo_core::system::AccessInfo {
        let mut info = gizmo_core::system::AccessInfo::new();
        info.is_exclusive = true;
        info
    }

    fn run(&mut self, world: &gizmo_core::world::World, _dt: f32) {
        if let Some(query) = world.query::<&gizmo_renderer::components::BoneAttachment>() {
            let mut skeletons = world.query::<&gizmo_renderer::components::Skeleton>();
            // SAFETY: scheduled system; scheduler guarantees disjoint mutable access.
            let mut transforms = unsafe {
                world.query_unchecked::<gizmo_core::query::Mut<gizmo_physics_core::Transform>>()
            };

            for (id, attachment) in query.iter() {
                if let Some(sq) = &mut skeletons {
                    if let Some(skeleton) = sq.get(attachment.target_entity.id()) {
                        if let Some(global_matrix) = skeleton.global_poses.get(attachment.bone_index) {
                            if let Some(tq) = &mut transforms {
                                if let Some(mut trans) = tq.get_mut(id) {
                                    let final_mat = *global_matrix * attachment.offset;
                                    let (t, r, s) = gizmo_renderer::decompose_mat4(final_mat);
                                    trans.position = t;
                                    trans.rotation = r;
                                    trans.scale = s;
                                    trans.update_local_matrix();
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use gizmo_core::component::Children;
    use gizmo_core::system::System;
    use gizmo_core::world::World;
    use gizmo_physics_core::components::GlobalTransform;
    use gizmo_physics_core::Transform;

    #[test]
    fn transform_propagate_terminates_on_children_cycle() {
        let mut world = World::new();
        let a = world.spawn();
        let b = world.spawn();
        world.add_component(a, Transform::default());
        world.add_component(a, GlobalTransform::default());
        world.add_component(b, Transform::default());
        world.add_component(b, GlobalTransform::default());
        // A `Children` cycle with neither node parented (both are propagation roots).
        // The old BFS had no visited set → its queue grew forever and this per-frame
        // system hung the whole app. Completing at all is the assertion.
        world.add_component(a, Children(vec![b.id()]));
        world.add_component(b, Children(vec![a.id()]));

        let mut sys = TransformPropagateSystem;
        sys.run(&world, 0.0);
    }
}