Upstream DebugPickingPlugin
from bevy_mod_picking
(#17177)
# 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 <aevyrie@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
d1e5702020
commit
5faff84c10
12
Cargo.toml
12
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"
|
||||
|
@ -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" }
|
||||
|
||||
|
@ -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`
|
||||
|
300
crates/bevy_dev_tools/src/picking_debug.rs
Normal file
300
crates/bevy_dev_tools/src/picking_debug.rs
Normal file
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
|
110
examples/picking/debug_picking.rs
Normal file
110
examples/picking/debug_picking.rs
Normal file
@ -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<DebugPickingMode>| {
|
||||
*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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<Pointer<Out>>, mut texts: Query<&mut TextColor>| {
|
||||
let mut text_color = texts.get_mut(out.target()).unwrap();
|
||||
text_color.0 = Color::WHITE;
|
||||
},
|
||||
)
|
||||
.observe(
|
||||
|over: Trigger<Pointer<Over>>, 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<Pointer<Click>>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
mut num: Local<usize>,
|
||||
) {
|
||||
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<Pointer<Drag>>, 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user