mittens_engine/engine/ecs/system/
gltf_bounds_visualization_system.rs1use crate::engine::ecs::component::{
2 BoundsComponent, ColorComponent, ComponentRef, EmissiveComponent, GLTFComponent, MeshComponent,
3 OpacityComponent, OverlayComponent, RaycastableComponent, RenderableComponent,
4 SelectableComponent, SerializeComponent, TransformComponent, TransformParentComponent,
5};
6use crate::engine::ecs::system::GLTFSystem;
7use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};
8use crate::engine::graphics::RenderAssets;
9use crate::engine::graphics::VisualWorld;
10use std::collections::{HashMap, HashSet};
11
12const BOUNDS_EDGE_THICKNESS: f32 = 0.01;
13const BOUNDS_EMISSIVE_INTENSITY: f32 = 2.0;
14const BOUNDS_OPACITY: f32 = 0.95;
15
16#[derive(Debug, Clone, Copy)]
17struct BoundsMarker {
18 target: ComponentId,
19 root: ComponentId,
20}
21
22#[derive(Debug, Default)]
24pub struct GltfBoundsVisualizationSystem {
25 markers: HashMap<ComponentId, Vec<BoundsMarker>>,
26}
27
28impl GltfBoundsVisualizationSystem {
29 pub fn tick_with_queue(
30 &mut self,
31 world: &mut World,
32 gltf_system: &GLTFSystem,
33 _visuals: &mut VisualWorld,
34 render_assets: &mut RenderAssets,
35 emit: &mut dyn SignalEmitter,
36 ) {
37 self.cleanup(world);
38
39 for gltf_id in gltf_system.tracked_components() {
40 let Some(gltf) = world.get_component_by_id_as::<GLTFComponent>(gltf_id) else {
41 continue;
42 };
43 if !gltf.spawned {
44 continue;
45 }
46 if gltf.bounds_visible {
47 self.ensure_markers(world, render_assets, emit, gltf_id);
48 } else {
49 self.remove_markers(emit, gltf_id);
50 }
51 }
52 }
53
54 fn cleanup(&mut self, world: &World) {
55 self.markers.retain(|gltf_id, markers| {
56 if world.get_component_record(*gltf_id).is_none() {
57 return false;
58 }
59 markers.retain(|marker| {
60 world.get_component_record(marker.root).is_some()
61 && world.get_component_record(marker.target).is_some()
62 });
63 true
64 });
65 }
66
67 fn ensure_markers(
68 &mut self,
69 world: &mut World,
70 render_assets: &mut RenderAssets,
71 emit: &mut dyn SignalEmitter,
72 gltf_id: ComponentId,
73 ) {
74 let existing_targets: HashSet<ComponentId> = self
75 .markers
76 .get(&gltf_id)
77 .into_iter()
78 .flatten()
79 .map(|marker| marker.target)
80 .collect();
81 let mut stack = world
82 .get_component_by_id_as::<GLTFComponent>(gltf_id)
83 .map(|gltf| gltf.spawned_node_transforms.clone())
84 .unwrap_or_default();
85 let mut additions = Vec::new();
86
87 while let Some(node_transform) = stack.pop() {
88 let children = world.children_of(node_transform).to_vec();
89 for child in children {
90 if world
91 .get_component_by_id_as::<TransformComponent>(child)
92 .is_some()
93 {
94 stack.push(child);
95 }
96 if existing_targets.contains(&child)
97 || world
98 .get_component_by_id_as::<RenderableComponent>(child)
99 .and_then(RenderableComponent::get_handle)
100 .is_none()
101 || !world.children_of(child).iter().any(|&sidecar| {
102 world
103 .get_component_by_id_as::<MeshComponent>(sidecar)
104 .is_some()
105 })
106 {
107 continue;
108 }
109 let Some(bounds) = world.children_of(child).iter().find_map(|&sidecar| {
110 world
111 .get_component_by_id_as::<BoundsComponent>(sidecar)
112 .map(|bounds| bounds.local)
113 }) else {
114 continue;
115 };
116 additions.push(spawn_marker(world, render_assets, emit, child, bounds));
117 }
118 }
119
120 self.markers.entry(gltf_id).or_default().extend(additions);
121 }
122
123 fn remove_markers(&mut self, emit: &mut dyn SignalEmitter, gltf_id: ComponentId) {
124 let Some(markers) = self.markers.get_mut(&gltf_id) else {
125 return;
126 };
127 for marker in markers.drain(..) {
128 emit.push_intent_now(
129 marker.root,
130 IntentValue::RemoveSubtree {
131 component_ids: vec![marker.root],
132 },
133 );
134 }
135 }
136}
137
138fn spawn_marker(
139 world: &mut World,
140 render_assets: &mut RenderAssets,
141 emit: &mut dyn SignalEmitter,
142 target: ComponentId,
143 bounds: crate::engine::graphics::bounds::Aabb,
144) -> BoundsMarker {
145 let center = bounds.center();
146 let local = TransformComponent::new()
147 .with_position(center[0], center[1], center[2])
148 .with_scale(bounds.width(), bounds.height(), bounds.depth());
149 let target_guid = world
150 .get_component_record(target)
151 .expect("bounds target must exist")
152 .guid;
153 let root = world.add_component_boxed_named(
154 "gltf_bounds_marker",
155 Box::new(
156 TransformParentComponent::new().with_target_source(ComponentRef::Guid(target_guid)),
157 ),
158 );
159 let local = world.add_component(local);
160 let selectable = world.add_component(SelectableComponent::off());
161 let serialize = world.add_component(SerializeComponent::off());
162 let overlay = world.add_component(OverlayComponent::new());
163 let renderable = world.add_component(RenderableComponent::wireframe_box(
164 render_assets,
165 BOUNDS_EDGE_THICKNESS,
166 ));
167 let raycastable = world.add_component(RaycastableComponent::disabled());
168 let color = world.add_component(ColorComponent::rgba(1.0, 0.35, 0.015, 1.0));
169 let emissive = world.add_component(EmissiveComponent::new(BOUNDS_EMISSIVE_INTENSITY));
170 let opacity = world.add_component(OpacityComponent::new().with_opacity(BOUNDS_OPACITY));
171 let _ = world.add_child(root, local);
172 let _ = world.add_child(root, selectable);
173 let _ = world.add_child(root, serialize);
174 let _ = world.add_child(local, overlay);
175 let _ = world.add_child(overlay, renderable);
176 let _ = world.add_child(renderable, raycastable);
177 let _ = world.add_child(renderable, color);
178 let _ = world.add_child(renderable, emissive);
179 let _ = world.add_child(renderable, opacity);
180 world.init_component_tree(root, emit);
181
182 BoundsMarker { target, root }
183}