dreamwell-engine 1.0.0

Dreamwell pure-logic engine library — transforms, hierarchy, canon pipeline, spatial math, hashing, tile rules, validation, waymark schema, material/lighting descriptors. No SpacetimeDB dependency.
Documentation
//! Scene transform storage and hierarchy propagation.
//!
//! `SceneTransforms` holds per-entity local and world transforms.
//! `propagate_from_roots` walks the hierarchy BFS and multiplies
//! parent world * child local to produce child world transforms.

use crate::game_object::Transform;
use crate::hierarchy::SceneHierarchy;
use crate::transform::GlobalTransform;

/// Per-entity transform storage (SoA layout).
#[derive(Debug, Clone)]
pub struct SceneTransforms {
    /// Local-space transforms (TRS).
    pub local: Vec<Transform>,
    /// World-space transforms (4x4 matrices).
    pub world: Vec<GlobalTransform>,
    /// Dirty flags — set when local transform changes.
    pub dirty: Vec<bool>,
}

impl SceneTransforms {
    /// Create with pre-allocated capacity. All transforms start at identity.
    pub fn with_capacity(capacity: usize) -> Self {
        Self {
            local: vec![Transform::default(); capacity],
            world: vec![GlobalTransform::identity(); capacity],
            dirty: vec![true; capacity],
        }
    }

    /// Mark an entity's transform as dirty (needs world recalculation).
    pub fn mark_dirty(&mut self, entity: u32) {
        let idx = entity as usize;
        if idx < self.dirty.len() {
            self.dirty[idx] = true;
        }
    }

    /// Propagate transforms from roots through hierarchy.
    /// BFS: for each root, compute world = local.to_matrix().
    /// For each child, compute world = parent_world * child_local.
    pub fn propagate_from_roots(&mut self, hierarchy: &SceneHierarchy) {
        let roots = hierarchy.roots();
        let mut stack: Vec<u32> = roots;

        while let Some(entity) = stack.pop() {
            let idx = entity as usize;
            if idx >= self.local.len() {
                continue;
            }

            let local_matrix = GlobalTransform(self.local[idx].to_matrix());

            let parent_world = hierarchy.parent[idx]
                .map(|p| self.world[p as usize])
                .unwrap_or_default();

            self.world[idx] = parent_world.mul(&local_matrix);
            self.dirty[idx] = false;

            // Push children onto stack.
            for &child in hierarchy.children_of(entity) {
                stack.push(child);
            }
        }
    }

    /// Number of entity slots.
    pub fn len(&self) -> usize {
        self.local.len()
    }

    /// Whether the store is empty.
    pub fn is_empty(&self) -> bool {
        self.local.is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn with_capacity_creates_identity() {
        let st = SceneTransforms::with_capacity(5);
        assert_eq!(st.len(), 5);
        assert_eq!(st.world[0], GlobalTransform::identity());
    }

    #[test]
    fn mark_dirty() {
        let mut st = SceneTransforms::with_capacity(3);
        st.dirty[0] = false;
        st.mark_dirty(0);
        assert!(st.dirty[0]);
    }

    #[test]
    fn propagate_identity_roots() {
        let mut st = SceneTransforms::with_capacity(3);
        let h = SceneHierarchy::with_capacity(3);
        st.propagate_from_roots(&h);
        // All roots with default local -> world should be identity.
        for i in 0..3 {
            assert_eq!(st.world[i], GlobalTransform::identity());
            assert!(!st.dirty[i]);
        }
    }

    #[test]
    fn propagate_parent_translation() {
        let mut st = SceneTransforms::with_capacity(3);
        let mut h = SceneHierarchy::with_capacity(3);
        // Entity 0 is root, translated x+5
        st.local[0].position = [5.0, 0.0, 0.0];
        // Entity 1 is child of 0, translated x+3
        st.local[1].position = [3.0, 0.0, 0.0];
        h.set_parent(1, Some(0));

        st.propagate_from_roots(&h);

        // Entity 0 world: x=5
        assert!((st.world[0].translation()[0] - 5.0).abs() < 1e-6);
        // Entity 1 world: x=5+3=8
        assert!((st.world[1].translation()[0] - 8.0).abs() < 1e-6);
    }

    #[test]
    fn propagate_chain_of_three() {
        let mut st = SceneTransforms::with_capacity(3);
        let mut h = SceneHierarchy::with_capacity(3);
        st.local[0].position = [1.0, 0.0, 0.0];
        st.local[1].position = [2.0, 0.0, 0.0];
        st.local[2].position = [3.0, 0.0, 0.0];
        h.set_parent(1, Some(0));
        h.set_parent(2, Some(1));

        st.propagate_from_roots(&h);

        assert!((st.world[0].translation()[0] - 1.0).abs() < 1e-6);
        assert!((st.world[1].translation()[0] - 3.0).abs() < 1e-6);
        assert!((st.world[2].translation()[0] - 6.0).abs() < 1e-6);
    }

    #[test]
    fn propagate_clears_dirty() {
        let mut st = SceneTransforms::with_capacity(2);
        let h = SceneHierarchy::with_capacity(2);
        assert!(st.dirty[0]);
        st.propagate_from_roots(&h);
        assert!(!st.dirty[0]);
    }
}