diff --git a/Cargo.toml b/Cargo.toml index 6be54d00eb451..639f90337ba41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3871,6 +3871,18 @@ category = "Picking" wasm = true required-features = ["bevy_sprite_picking_backend"] +[[example]] +name = "debug_picking" +path = "examples/picking/debug_picking.rs" +doc-scrape-examples = true +required-features = ["bevy_dev_tools"] + +[package.metadata.example.debug_picking] +name = "Picking Debug Tools" +description = "Demonstrates picking debug overlay" +category = "Picking" +wasm = true + [[example]] name = "animation_masks" path = "examples/animation/animation_masks.rs" diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index e4cb07bdaae52..223474a0f0493 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -20,10 +20,13 @@ bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" } bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 8b328a95042db..40f8ce58f5699 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -20,6 +20,8 @@ pub mod ci_testing; pub mod fps_overlay; +pub mod picking_debug; + pub mod states; /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs new file mode 100644 index 0000000000000..02ee8063184b0 --- /dev/null +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -0,0 +1,300 @@ +//! Text and on-screen debugging tools + +use bevy_app::prelude::*; +use bevy_asset::prelude::*; +use bevy_color::prelude::*; +use bevy_ecs::prelude::*; +use bevy_picking::backend::HitData; +use bevy_picking::hover::HoverMap; +use bevy_picking::pointer::{Location, PointerId, PointerPress}; +use bevy_picking::prelude::*; +use bevy_picking::{pointer, PickSet}; +use bevy_reflect::prelude::*; +use bevy_render::prelude::*; +use bevy_text::prelude::*; +use bevy_ui::prelude::*; +use core::cmp::Ordering; +use core::fmt::{Debug, Display, Formatter, Result}; +use tracing::{debug, trace}; + +/// This resource determines the runtime behavior of the debug plugin. +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Resource)] +pub enum DebugPickingMode { + /// Only log non-noisy events, show the debug overlay. + Normal, + /// Log all events, including noisy events like `Move` and `Drag`, show the debug overlay. + Noisy, + /// Do not show the debug overlay or log any messages. + #[default] + Disabled, +} + +impl DebugPickingMode { + /// A condition indicating the plugin is enabled + pub fn is_enabled(this: Res) -> bool { + matches!(*this, Self::Normal | Self::Noisy) + } + /// A condition indicating the plugin is disabled + pub fn is_disabled(this: Res) -> bool { + matches!(*this, Self::Disabled) + } + /// A condition indicating the plugin is enabled and in noisy mode + pub fn is_noisy(this: Res) -> bool { + matches!(*this, Self::Noisy) + } +} + +/// Logs events for debugging +/// +/// "Normal" events are logged at the `debug` level. "Noisy" events are logged at the `trace` level. +/// See [Bevy's LogPlugin](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) and [Bevy +/// Cheatbook: Logging, Console Messages](https://bevy-cheatbook.github.io/features/log.html) for +/// details. +/// +/// Usually, the default level printed is `info`, so debug and trace messages will not be displayed +/// even when this plugin is active. You can set `RUST_LOG` to change this. +/// +/// You can also change the log filter at runtime in your code. The [LogPlugin +/// docs](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) give an example. +/// +/// Use the [`DebugPickingMode`] state resource to control this plugin. Example: +/// +/// ```ignore +/// use DebugPickingMode::{Normal, Disabled}; +/// app.insert_resource(DebugPickingMode::Normal) +/// .add_systems( +/// PreUpdate, +/// (|mut mode: ResMut| { +/// *mode = match *mode { +/// DebugPickingMode::Disabled => DebugPickingMode::Normal, +/// _ => DebugPickingMode::Disabled, +/// }; +/// }) +/// .distributive_run_if(bevy::input::common_conditions::input_just_pressed( +/// KeyCode::F3, +/// )), +/// ) +/// ``` +/// This sets the starting mode of the plugin to [`DebugPickingMode::Disabled`] and binds the F3 key +/// to toggle it. +#[derive(Debug, Default, Clone)] +pub struct DebugPickingPlugin; + +impl Plugin for DebugPickingPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems( + PreUpdate, + pointer_debug_visibility.in_set(PickSet::PostHover), + ) + .add_systems( + PreUpdate, + ( + // This leaves room to easily change the log-level associated + // with different events, should that be desired. + log_event_debug::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_debug::, + ) + .distributive_run_if(DebugPickingMode::is_enabled) + .in_set(PickSet::Last), + ); + + app.add_systems( + PreUpdate, + (add_pointer_debug, update_debug_data, debug_draw) + .chain() + .distributive_run_if(DebugPickingMode::is_enabled) + .in_set(PickSet::Last), + ); + } +} + +/// Listen for any event and logs it at the debug level +pub fn log_event_debug(mut events: EventReader) { + for event in events.read() { + debug!("{event:?}"); + } +} + +/// Listens for pointer events of type `E` and logs them at "debug" level +pub fn log_pointer_event_debug( + mut pointer_events: EventReader>, +) { + for event in pointer_events.read() { + debug!("{event}"); + } +} + +/// Listens for pointer events of type `E` and logs them at "trace" level +pub fn log_pointer_event_trace( + mut pointer_events: EventReader>, +) { + for event in pointer_events.read() { + trace!("{event}"); + } +} + +/// Adds [`PointerDebug`] to pointers automatically. +pub fn add_pointer_debug( + mut commands: Commands, + pointers: Query, Without)>, +) { + for entity in &pointers { + commands.entity(entity).insert(PointerDebug::default()); + } +} + +/// Hide text from pointers. +pub fn pointer_debug_visibility( + debug: Res, + mut pointers: Query<&mut Visibility, With>, +) { + let visible = match *debug { + DebugPickingMode::Disabled => Visibility::Hidden, + _ => Visibility::Visible, + }; + for mut vis in &mut pointers { + *vis = visible; + } +} + +/// Storage for per-pointer debug information. +#[derive(Debug, Component, Clone, Default)] +pub struct PointerDebug { + /// The pointer location. + pub location: Option, + + /// Representation of the different pointer button states. + pub press: PointerPress, + + /// List of hit elements to be displayed. + pub hits: Vec<(String, HitData)>, +} + +fn bool_to_icon(f: &mut Formatter, prefix: &str, input: bool) -> Result { + write!(f, "{prefix}{}", if input { "[X]" } else { "[ ]" }) +} + +impl Display for PointerDebug { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if let Some(location) = &self.location { + writeln!(f, "Location: {:.2?}", location.position)?; + } + bool_to_icon(f, "Pressed: ", self.press.is_primary_pressed())?; + bool_to_icon(f, " ", self.press.is_middle_pressed())?; + bool_to_icon(f, " ", self.press.is_secondary_pressed())?; + let mut sorted_hits = self.hits.clone(); + sorted_hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + for (entity, hit) in sorted_hits.iter() { + write!(f, "\nEntity: {entity:?}")?; + if let Some((position, normal)) = hit.position.zip(hit.normal) { + write!(f, ", Position: {position:.2?}, Normal: {normal:.2?}")?; + } + write!(f, ", Depth: {:.2?}", hit.depth)?; + } + + Ok(()) + } +} + +/// Update typed debug data used to draw overlays +pub fn update_debug_data( + hover_map: Res, + entity_names: Query, + mut pointers: Query<( + &PointerId, + &pointer::PointerLocation, + &PointerPress, + &mut PointerDebug, + )>, +) { + for (id, location, press, mut debug) in &mut pointers { + *debug = PointerDebug { + location: location.location().cloned(), + press: press.to_owned(), + hits: hover_map + .get(id) + .iter() + .flat_map(|h| h.iter()) + .filter_map(|(e, h)| { + if let Ok(entity_name) = entity_names.get(*e) { + Some((entity_name.to_string(), h.to_owned())) + } else { + None + } + }) + .collect(), + }; + } +} + +/// Draw text on each cursor with debug info +pub fn debug_draw( + mut commands: Commands, + camera_query: Query<(Entity, &Camera)>, + primary_window: Query>, + pointers: Query<(Entity, &PointerId, &PointerDebug)>, + scale: Res, +) { + let font_handle: Handle = Default::default(); + for (entity, id, debug) in pointers.iter() { + let Some(pointer_location) = &debug.location else { + continue; + }; + let text = format!("{id:?}\n{debug}"); + + for camera in camera_query + .iter() + .map(|(entity, camera)| { + ( + entity, + camera.target.normalize(primary_window.get_single().ok()), + ) + }) + .filter_map(|(entity, target)| Some(entity).zip(target)) + .filter(|(_entity, target)| target == &pointer_location.target) + .map(|(cam_entity, _target)| cam_entity) + { + let mut pointer_pos = pointer_location.position; + if let Some(viewport) = camera_query + .get(camera) + .ok() + .and_then(|(_, camera)| camera.logical_viewport_rect()) + { + pointer_pos -= viewport.min; + } + + commands + .entity(entity) + .insert(( + Text::new(text.clone()), + TextFont { + font: font_handle.clone(), + font_size: 12.0, + ..Default::default() + }, + TextColor(Color::WHITE), + Node { + position_type: PositionType::Absolute, + left: Val::Px(pointer_pos.x + 5.0) / scale.0, + top: Val::Px(pointer_pos.y + 5.0) / scale.0, + ..Default::default() + }, + )) + .insert(PickingBehavior::IGNORE) + .insert(TargetCamera(camera)); + } + } +} diff --git a/examples/README.md b/examples/README.md index 5b1594b8e016f..69c1d470d7dd6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -383,6 +383,7 @@ Example | Description Example | Description --- | --- [Mesh Picking](../examples/picking/mesh_picking.rs) | Demonstrates picking meshes +[Picking Debug Tools](../examples/picking/debug_picking.rs) | Demonstrates picking debug overlay [Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects [Sprite Picking](../examples/picking/sprite_picking.rs) | Demonstrates picking sprites and sprite atlases diff --git a/examples/picking/debug_picking.rs b/examples/picking/debug_picking.rs new file mode 100644 index 0000000000000..ea42702032cb7 --- /dev/null +++ b/examples/picking/debug_picking.rs @@ -0,0 +1,110 @@ +//! A simple scene to demonstrate picking events for UI and mesh entities, +//! Demonstrates how to change debug settings + +use bevy::dev_tools::picking_debug::{DebugPickingMode, DebugPickingPlugin}; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(bevy::log::LogPlugin { + filter: "bevy_dev_tools=trace".into(), // Show picking logs trace level and up + ..default() + })) + // Unlike UiPickingPlugin, MeshPickingPlugin is not a default plugin + .add_plugins((MeshPickingPlugin, DebugPickingPlugin)) + .add_systems(Startup, setup_scene) + .insert_resource(DebugPickingMode::Normal) + // A system that cycles the debugging state when you press F3: + .add_systems( + PreUpdate, + (|mut mode: ResMut| { + *mode = match *mode { + DebugPickingMode::Disabled => DebugPickingMode::Normal, + DebugPickingMode::Normal => DebugPickingMode::Noisy, + DebugPickingMode::Noisy => DebugPickingMode::Disabled, + } + }) + .distributive_run_if(bevy::input::common_conditions::input_just_pressed( + KeyCode::F3, + )), + ) + .run(); +} + +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands + .spawn(( + Text::new("Click Me to get a box\nDrag cubes to rotate\nPress F3 to cycle between picking debug levels"), + Node { + position_type: PositionType::Absolute, + top: Val::Percent(12.0), + left: Val::Percent(12.0), + ..default() + }, + )) + .observe(on_click_spawn_cube) + .observe( + |out: Trigger>, mut texts: Query<&mut TextColor>| { + let mut text_color = texts.get_mut(out.target()).unwrap(); + text_color.0 = Color::WHITE; + }, + ) + .observe( + |over: Trigger>, mut texts: Query<&mut TextColor>| { + let mut color = texts.get_mut(over.target()).unwrap(); + color.0 = bevy::color::palettes::tailwind::CYAN_400.into(); + }, + ); + + // Base + commands.spawn(( + Name::new("Base"), + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); + + // Light + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + + // Camera + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn on_click_spawn_cube( + _click: Trigger>, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut num: Local, +) { + commands + .spawn(( + Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))), + MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + Transform::from_xyz(0.0, 0.25 + 0.55 * *num as f32, 0.0), + )) + // With the MeshPickingPlugin added, you can add pointer event observers to meshes: + .observe(on_drag_rotate); + *num += 1; +} + +fn on_drag_rotate(drag: Trigger>, mut transforms: Query<&mut Transform>) { + if let Ok(mut transform) = transforms.get_mut(drag.target()) { + transform.rotate_y(drag.delta.x * 0.02); + transform.rotate_x(drag.delta.y * 0.02); + } +}