use super::*; /// Debugging systems, resources, events, etc. pub struct DebuggingPlugin; impl Plugin for DebuggingPlugin { fn build(&self, app: &mut App) { app.init_state::() .init_resource::() .add_plugins(RapierDebugRenderPlugin::default().disabled()) // Added by Rapier // .add_plugins(AabbGizmoPlugin) // .add_plugins(LightGizmoPlugin) .add_systems(Startup, init_debug_ui) .add_systems( Update, ( (toggle_state_visibility::,) .run_if(state_changed::), toggle_debug_state.run_if(on_keyboard_press(KeyCode::F12)), (toggle_light_gizmo, toggle_aabb_gizmo).run_if(state_changed::), ( (hover_mesh, hover_ui) .run_if(on_event::>.or(on_event::>)), tooltip_follow.run_if(any_component_changed::), sync_resource_to_ui::.run_if(resource_changed::), ) .run_if(in_state(DebuggingState::On)), ), ); // WebGL2-incompatible systems #[cfg(not(target_arch = "wasm32"))] { app.add_plugins(WireframePlugin::default()) .insert_resource(WireframeConfig { global: false, default_color: MAGENTA.into(), }) .add_systems( Update, toggle_rapier_debug_render.run_if(state_changed::), ) .add_systems(OnEnter(DebuggingState::On), enable_wireframe) .add_systems(OnExit(DebuggingState::On), disable_wireframe); } } } /// Tracks if the debugger is on or off for other games systems to hook into /// /// The Debugging state may add it's own global debugging information, but is mostly a shell #[derive(States, Component, Default, Debug, Clone, Hash, Eq, PartialEq)] pub enum DebuggingState { #[default] Off, On, } /// Create the Debugging UI fn init_debug_ui(mut commands: Commands) { // "Debugging On" Indicator commands.spawn(( DebuggingState::On, Name::new("Debug Indicator"), Text(" Debug: ON ".into()), TextColor(WHITE.into()), BackgroundColor(RED.into()), BorderRadius::MAX, Node { align_self: AlignSelf::Center, justify_self: JustifySelf::End, ..default() }, )); // Version string for troubleshooting commands.spawn(( DebuggingState::On, Name::new("Version #"), Text::new(VERSION), TextColor(WHITE.into()), BackgroundColor(BLACK.into()), Node { width: Val::Auto, align_self: AlignSelf::End, justify_self: JustifySelf::End, ..default() }, )); // Tooltip commands.spawn(( DebuggingState::On, Text("Tooltip Placeholder".into()), TextColor(WHITE.into()), SyncResource::::default(), BackgroundColor(BLACK.with_alpha(0.9).into()), GlobalZIndex(i32::MAX), Node { position_type: PositionType::Absolute, margin: UiRect { left: Val::Px(20.0), ..default() }, align_content: AlignContent::Center, justify_content: JustifyContent::Center, ..default() }, )); } /// Toggles the debug state from off -> on // off -> on when triggered fn toggle_debug_state( mut next: ResMut>, curr: Res>, ) { use DebuggingState::*; next.set(match curr.get() { On => Off, Off => On, }); info!("Toggling debug state: {:?} -> {:?}", curr, next); } /// Simple system that enables/disables rapier debug visuals when the debugging state changes #[cfg(not(target_arch = "wasm32"))] fn toggle_rapier_debug_render( state: Res>, mut context: ResMut, ) { context.enabled = *state.get() == DebuggingState::On; } /// Add a generic Tooltip that follows the mouse in debug mode #[derive(Default, Resource)] pub struct ToolTip(HashMap); impl ToolTip { pub fn insert(&mut self, k: &str, v: String) { self.0.insert(k.into(), v); } pub fn remove(&mut self, k: &str) { self.0.remove(k); } } impl Display for ToolTip { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { for (k, v) in self.0.iter() { writeln!(f, "{k}: {v}")? } Ok(()) } } fn tooltip_follow( mut tooltip: Single<(&mut Node, &mut Visibility), With>>, window: Single<&Window>, ) { if let Some(Vec2 { x, y }) = window.cursor_position() { *tooltip.1 = Visibility::Inherited; tooltip.0.left = Val::Px(x); tooltip.0.top = Val::Px(y); } else { *tooltip.1 = Visibility::Hidden; } } /// When you hover over a mesh, update the tooltip with some info fn hover_mesh( mut over_events: EventReader>, mut out_events: EventReader>, mut tooltip: ResMut, meshes: Query<(&Transform, Option<&Name>), With>, ) { out_events .read() .filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target)) .for_each(|_| { tooltip.remove("ID"); tooltip.remove("Pos"); tooltip.remove("Name"); }); over_events .read() .filter_map(|Pointer { target, .. }| meshes.contains(*target).then_some(*target)) .for_each(|e| { if let Ok((t, n)) = meshes.get(e) { let pos = (t.translation.x, t.translation.y, t.translation.z); let name = match n { Some(x) => x, None => "N/A", }; tooltip.insert("ID", format!("{e}")); tooltip.insert("Pos", format!("{pos:.3?}")); tooltip.insert("Name", name.into()); } else { warn!("Failed to query data"); } }); } fn hover_ui( mut over_events: EventReader>, mut out_events: EventReader>, mut tooltip: ResMut, nodes: Query, With>, ) { out_events .read() .filter_map(|Pointer { target, .. }| nodes.contains(*target).then_some(*target)) .for_each(|_| { tooltip.remove("ID"); tooltip.remove("Name"); }); over_events .read() .filter_map(|Pointer { target, .. }| nodes.contains(*target).then_some(*target)) .for_each(|e| { if let Ok(n) = nodes.get(e) { let name = match n { Some(x) => x, None => "N/A", }; tooltip.insert("ID", format!("{e}")); tooltip.insert("Name", name.into()); } else { warn!("Failed to query data"); } }); } #[cfg(not(target_arch = "wasm32"))] fn enable_wireframe(mut wireframe_config: ResMut) { wireframe_config.global = true; } #[cfg(not(target_arch = "wasm32"))] fn disable_wireframe(mut wireframe_config: ResMut) { wireframe_config.global = false; } // Toggle the light gizmo config group fn toggle_light_gizmo( state: Res>, mut config_store: ResMut, ) { let (_, light_group) = config_store.config_mut::(); light_group.draw_all = *state.get() == DebuggingState::On; } // Toggle the aabb gizmo config group fn toggle_aabb_gizmo( state: Res>, mut config_store: ResMut, ) { let (_, aabb_group) = config_store.config_mut::(); aabb_group.draw_all = *state.get() == DebuggingState::On; }