mittens_engine/engine/ecs/system/
bounds_system.rs1use 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#[derive(Debug, Default)]
21pub struct BoundsSystem;
22
23impl BoundsSystem {
24 pub fn new() -> Self {
25 Self
26 }
27
28 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 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 let Some(r) = world.get_component_by_id_as::<RenderableComponent>(node) {
50 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 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}