Skip to main content

mittens_engine/engine/ecs/system/
gltf_bounds_visualization_system.rs

1use 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/// Draws imported GLTF mesh bounds without inserting debug nodes into the GLTF hierarchy.
23#[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}