Skip to content

Commit

Permalink
Upstream DebugPickingPlugin from bevy_mod_picking (#17177)
Browse files Browse the repository at this point in the history
# Objective

The debug features (`DebugPickingPlugin`) from `bevy_mod_picking` were
not upstreamed with the rest of the core changes, this PR reintroduces
it for usage inside `bevy_dev_tools`

## Solution

Vast majority of this code is taken as-is from `bevy_mod_picking` aside
from changes to ensure compilation and code style, as such @aevyrie was
added as the co-author for this change.

### Main changes
* `multiselection` support - the relevant code was explicitly not
included in the process of upstreaming the rest of the package, so it
also has been omitted here.
* `bevy_egui` support - the old package had a preference for using
`bevy_egui` instead of `bevy_ui` if possible, I couldn't see a way to
support this in a core crate, so this has been removed.

Relevant code has been added to the `bevy_dev_tools` crate instead of
`bevy_picking` as it is a better fit and requires a dependency on
`bevy_ui` for drawing debug elements.

### Minor changes
* Changed the debug text size from `60` to `12` as the former was so
large as to be unreadable in the new example.

## Testing
* `cargo run -p ci`
* Added a new example in `dev_tools/picking_debug` and visually verified
the in-window results and the console messages

---------

Co-authored-by: Aevyrie <[email protected]>
Co-authored-by: Alice Cecile <[email protected]>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent d1e5702 commit 5faff84
Show file tree
Hide file tree
Showing 6 changed files with 428 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }

Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_dev_tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
300 changes: 300 additions & 0 deletions crates/bevy_dev_tools/src/picking_debug.rs
Original file line number Diff line number Diff line change
@@ -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<Self>) -> bool {
matches!(*this, Self::Normal | Self::Noisy)
}
/// A condition indicating the plugin is disabled
pub fn is_disabled(this: Res<Self>) -> bool {
matches!(*this, Self::Disabled)
}
/// A condition indicating the plugin is enabled and in noisy mode
pub fn is_noisy(this: Res<Self>) -> 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<DebugPickingMode>| {
/// *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::<DebugPickingMode>()
.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::<pointer::PointerInput>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<Over>,
log_pointer_event_debug::<Out>,
log_pointer_event_debug::<Pressed>,
log_pointer_event_debug::<Released>,
log_pointer_event_debug::<Click>,
log_pointer_event_trace::<Move>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<DragStart>,
log_pointer_event_trace::<Drag>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<DragEnd>,
log_pointer_event_debug::<DragEnter>,
log_pointer_event_trace::<DragOver>.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::<DragLeave>,
log_pointer_event_debug::<DragDrop>,
)
.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<E: Event + Debug>(mut events: EventReader<pointer::PointerInput>) {
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<E: Debug + Clone + Reflect>(
mut pointer_events: EventReader<Pointer<E>>,
) {
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<E: Debug + Clone + Reflect>(
mut pointer_events: EventReader<Pointer<E>>,
) {
for event in pointer_events.read() {
trace!("{event}");
}
}

/// Adds [`PointerDebug`] to pointers automatically.
pub fn add_pointer_debug(
mut commands: Commands,
pointers: Query<Entity, (With<PointerId>, Without<PointerDebug>)>,
) {
for entity in &pointers {
commands.entity(entity).insert(PointerDebug::default());
}
}

/// Hide text from pointers.
pub fn pointer_debug_visibility(
debug: Res<DebugPickingMode>,
mut pointers: Query<&mut Visibility, With<PointerId>>,
) {
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<Location>,

/// 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<HoverMap>,
entity_names: Query<NameOrEntity>,
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<Entity, With<bevy_window::PrimaryWindow>>,
pointers: Query<(Entity, &PointerId, &PointerDebug)>,
scale: Res<UiScale>,
) {
let font_handle: Handle<Font> = 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));
}
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 5faff84

Please sign in to comment.