Skip to main content

mittens_engine/engine/ecs/system/
bounds_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::World;
3use crate::engine::ecs::component::{RenderableComponent, TransformComponent};
4use crate::engine::graphics::RenderAssets;
5use crate::engine::graphics::bounds::{Aabb, mat4_identity, mat4_mul, mesh_local_aabb};
6
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub enum RenderableBoundsMeasure {
9    Measured(Aabb),
10    Unmeasurable,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct UniformFitTransform {
15    pub translation: [f32; 3],
16    pub scale: [f32; 3],
17}
18
19/// Utility system for calculating aggregate bounds of component subtrees.
20#[derive(Debug, Default)]
21pub struct BoundsSystem;
22
23impl BoundsSystem {
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Recursively compute the aggregate AABB of a component subtree in the root's coordinate frame.
29    ///
30    /// This walks the child topology and unions the transformed AABBs of all
31    /// `RenderableComponent`s encountered. It accounts for nested `TransformComponent`s.
32    pub fn calculate_subtree_local_bounds(
33        world: &World,
34        render_assets: &RenderAssets,
35        root: ComponentId,
36    ) -> Option<Aabb> {
37        let mut aggregate: Option<Aabb> = None;
38        let mut stack = vec![(root, mat4_identity())];
39
40        while let Some((node, parent_to_root)) = stack.pop() {
41            let mut local_to_root = parent_to_root;
42
43            // Compose the transform of this node into the root-relative matrix.
44            if let Some(tc) = world.get_component_by_id_as::<TransformComponent>(node) {
45                local_to_root = mat4_mul(parent_to_root, tc.transform.model);
46            }
47
48            // If it's a renderable, union its transformed bounds.
49            if let Some(r) = world.get_component_by_id_as::<RenderableComponent>(node) {
50                // Try looking up the AABB for this specific mesh in RenderAssets,
51                // otherwise fallback to the hardcoded primitives.
52                let aabb = render_assets
53                    .cpu_mesh(r.renderable.mesh)
54                    .and_then(|cpu_mesh| {
55                        Aabb::from_points(
56                            &cpu_mesh
57                                .vertices
58                                .iter()
59                                .map(|v| v.pos)
60                                .collect::<Vec<[f32; 3]>>(),
61                        )
62                    })
63                    .or_else(|| mesh_local_aabb(r.renderable.base_mesh));
64
65                if let Some(local_aabb) = aabb {
66                    let transformed = local_aabb.transformed(local_to_root);
67                    aggregate = Some(match aggregate {
68                        Some(a) => a.union(&transformed),
69                        None => transformed,
70                    });
71                }
72            }
73
74            // Recurse into children.
75            for &child in world.children_of(node) {
76                stack.push((child, local_to_root));
77            }
78        }
79
80        aggregate
81    }
82
83    pub fn measure_renderable_subtree_bounds(
84        world: &World,
85        render_assets: &RenderAssets,
86        root: ComponentId,
87    ) -> RenderableBoundsMeasure {
88        match Self::calculate_subtree_local_bounds(world, render_assets, root) {
89            Some(bounds) => RenderableBoundsMeasure::Measured(bounds),
90            None => RenderableBoundsMeasure::Unmeasurable,
91        }
92    }
93
94    pub fn fit_aabb_uniform(aabb: &Aabb, target_bounds: [f32; 6]) -> Option<UniformFitTransform> {
95        let target = Aabb {
96            min: [target_bounds[0], target_bounds[1], target_bounds[2]],
97            max: [target_bounds[3], target_bounds[4], target_bounds[5]],
98        };
99
100        let measured_dims = [aabb.width(), aabb.height(), aabb.depth()];
101        let target_dims = [target.width(), target.height(), target.depth()];
102        let mut uniform_scale: Option<f32> = None;
103
104        for (&measured, &target) in measured_dims.iter().zip(target_dims.iter()) {
105            if measured <= 1e-6 || target <= 1e-6 {
106                continue;
107            }
108            let axis_scale = target / measured;
109            uniform_scale = Some(match uniform_scale {
110                Some(current) => current.min(axis_scale),
111                None => axis_scale,
112            });
113        }
114
115        let scale = uniform_scale?;
116        let measured_center = aabb.center();
117        let target_center = target.center();
118
119        Some(UniformFitTransform {
120            translation: [
121                target_center[0] - measured_center[0] * scale,
122                target_center[1] - measured_center[1] * scale,
123                target_center[2] - measured_center[2] * scale,
124            ],
125            scale: [scale, scale, scale],
126        })
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::engine::ecs::component::{ColorComponent, RenderableComponent};
134
135    #[test]
136    fn uniform_fit_centers_and_scales_bounds() {
137        let bounds = Aabb {
138            min: [-0.25, -0.5, -0.05],
139            max: [0.25, 0.5, 0.05],
140        };
141
142        let transform = BoundsSystem::fit_aabb_uniform(&bounds, [-1.0, -1.0, -0.1, 1.0, 1.0, 0.1])
143            .expect("fit transform");
144
145        assert_eq!(transform.translation, [0.0, 0.0, 0.0]);
146        assert_eq!(transform.scale, [2.0, 2.0, 2.0]);
147    }
148
149    #[test]
150    fn renderable_measure_wraps_calculated_bounds() {
151        let mut world = World::default();
152        let mut render_assets = RenderAssets::new();
153
154        let root = world.add_component(TransformComponent::new());
155        let shape = world.add_component(TransformComponent::new().with_scale(0.25, 0.5, 0.1));
156        let color = world.add_component(ColorComponent::rgba(1.0, 1.0, 1.0, 1.0));
157        let renderable = world.add_component(RenderableComponent::cube());
158
159        world.add_child(root, shape).expect("attach shape");
160        world.add_child(shape, color).expect("attach color");
161        world
162            .add_child(color, renderable)
163            .expect("attach renderable");
164
165        let measure =
166            BoundsSystem::measure_renderable_subtree_bounds(&world, &mut render_assets, root);
167        let RenderableBoundsMeasure::Measured(bounds) = measure else {
168            panic!("expected measured bounds");
169        };
170
171        assert!((bounds.width() - 0.25).abs() < 1e-4);
172        assert!((bounds.height() - 0.5).abs() < 1e-4);
173        assert!((bounds.depth() - 0.1).abs() < 1e-4);
174    }
175}