Skip to content

Commit 5faff84

Browse files
skimmedsquareaevyriealice-i-cecile
authored
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 <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent d1e5702 commit 5faff84

File tree

6 files changed

+428
-0
lines changed

6 files changed

+428
-0
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3871,6 +3871,18 @@ category = "Picking"
38713871
wasm = true
38723872
required-features = ["bevy_sprite_picking_backend"]
38733873

3874+
[[example]]
3875+
name = "debug_picking"
3876+
path = "examples/picking/debug_picking.rs"
3877+
doc-scrape-examples = true
3878+
required-features = ["bevy_dev_tools"]
3879+
3880+
[package.metadata.example.debug_picking]
3881+
name = "Picking Debug Tools"
3882+
description = "Demonstrates picking debug overlay"
3883+
category = "Picking"
3884+
wasm = true
3885+
38743886
[[example]]
38753887
name = "animation_masks"
38763888
path = "examples/animation/animation_masks.rs"

crates/bevy_dev_tools/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" }
2020
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
2121
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" }
2222
bevy_input = { path = "../bevy_input", version = "0.16.0-dev" }
23+
bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" }
2324
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
25+
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" }
2426
bevy_time = { path = "../bevy_time", version = "0.16.0-dev" }
2527
bevy_text = { path = "../bevy_text", version = "0.16.0-dev" }
2628
bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" }
29+
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" }
2730
bevy_window = { path = "../bevy_window", version = "0.16.0-dev" }
2831
bevy_state = { path = "../bevy_state", version = "0.16.0-dev" }
2932

crates/bevy_dev_tools/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ pub mod ci_testing;
2020

2121
pub mod fps_overlay;
2222

23+
pub mod picking_debug;
24+
2325
pub mod states;
2426

2527
/// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools`
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
//! Text and on-screen debugging tools
2+
3+
use bevy_app::prelude::*;
4+
use bevy_asset::prelude::*;
5+
use bevy_color::prelude::*;
6+
use bevy_ecs::prelude::*;
7+
use bevy_picking::backend::HitData;
8+
use bevy_picking::hover::HoverMap;
9+
use bevy_picking::pointer::{Location, PointerId, PointerPress};
10+
use bevy_picking::prelude::*;
11+
use bevy_picking::{pointer, PickSet};
12+
use bevy_reflect::prelude::*;
13+
use bevy_render::prelude::*;
14+
use bevy_text::prelude::*;
15+
use bevy_ui::prelude::*;
16+
use core::cmp::Ordering;
17+
use core::fmt::{Debug, Display, Formatter, Result};
18+
use tracing::{debug, trace};
19+
20+
/// This resource determines the runtime behavior of the debug plugin.
21+
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Resource)]
22+
pub enum DebugPickingMode {
23+
/// Only log non-noisy events, show the debug overlay.
24+
Normal,
25+
/// Log all events, including noisy events like `Move` and `Drag`, show the debug overlay.
26+
Noisy,
27+
/// Do not show the debug overlay or log any messages.
28+
#[default]
29+
Disabled,
30+
}
31+
32+
impl DebugPickingMode {
33+
/// A condition indicating the plugin is enabled
34+
pub fn is_enabled(this: Res<Self>) -> bool {
35+
matches!(*this, Self::Normal | Self::Noisy)
36+
}
37+
/// A condition indicating the plugin is disabled
38+
pub fn is_disabled(this: Res<Self>) -> bool {
39+
matches!(*this, Self::Disabled)
40+
}
41+
/// A condition indicating the plugin is enabled and in noisy mode
42+
pub fn is_noisy(this: Res<Self>) -> bool {
43+
matches!(*this, Self::Noisy)
44+
}
45+
}
46+
47+
/// Logs events for debugging
48+
///
49+
/// "Normal" events are logged at the `debug` level. "Noisy" events are logged at the `trace` level.
50+
/// See [Bevy's LogPlugin](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) and [Bevy
51+
/// Cheatbook: Logging, Console Messages](https://bevy-cheatbook.github.io/features/log.html) for
52+
/// details.
53+
///
54+
/// Usually, the default level printed is `info`, so debug and trace messages will not be displayed
55+
/// even when this plugin is active. You can set `RUST_LOG` to change this.
56+
///
57+
/// You can also change the log filter at runtime in your code. The [LogPlugin
58+
/// docs](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) give an example.
59+
///
60+
/// Use the [`DebugPickingMode`] state resource to control this plugin. Example:
61+
///
62+
/// ```ignore
63+
/// use DebugPickingMode::{Normal, Disabled};
64+
/// app.insert_resource(DebugPickingMode::Normal)
65+
/// .add_systems(
66+
/// PreUpdate,
67+
/// (|mut mode: ResMut<DebugPickingMode>| {
68+
/// *mode = match *mode {
69+
/// DebugPickingMode::Disabled => DebugPickingMode::Normal,
70+
/// _ => DebugPickingMode::Disabled,
71+
/// };
72+
/// })
73+
/// .distributive_run_if(bevy::input::common_conditions::input_just_pressed(
74+
/// KeyCode::F3,
75+
/// )),
76+
/// )
77+
/// ```
78+
/// This sets the starting mode of the plugin to [`DebugPickingMode::Disabled`] and binds the F3 key
79+
/// to toggle it.
80+
#[derive(Debug, Default, Clone)]
81+
pub struct DebugPickingPlugin;
82+
83+
impl Plugin for DebugPickingPlugin {
84+
fn build(&self, app: &mut App) {
85+
app.init_resource::<DebugPickingMode>()
86+
.add_systems(
87+
PreUpdate,
88+
pointer_debug_visibility.in_set(PickSet::PostHover),
89+
)
90+
.add_systems(
91+
PreUpdate,
92+
(
93+
// This leaves room to easily change the log-level associated
94+
// with different events, should that be desired.
95+
log_event_debug::<pointer::PointerInput>.run_if(DebugPickingMode::is_noisy),
96+
log_pointer_event_debug::<Over>,
97+
log_pointer_event_debug::<Out>,
98+
log_pointer_event_debug::<Pressed>,
99+
log_pointer_event_debug::<Released>,
100+
log_pointer_event_debug::<Click>,
101+
log_pointer_event_trace::<Move>.run_if(DebugPickingMode::is_noisy),
102+
log_pointer_event_debug::<DragStart>,
103+
log_pointer_event_trace::<Drag>.run_if(DebugPickingMode::is_noisy),
104+
log_pointer_event_debug::<DragEnd>,
105+
log_pointer_event_debug::<DragEnter>,
106+
log_pointer_event_trace::<DragOver>.run_if(DebugPickingMode::is_noisy),
107+
log_pointer_event_debug::<DragLeave>,
108+
log_pointer_event_debug::<DragDrop>,
109+
)
110+
.distributive_run_if(DebugPickingMode::is_enabled)
111+
.in_set(PickSet::Last),
112+
);
113+
114+
app.add_systems(
115+
PreUpdate,
116+
(add_pointer_debug, update_debug_data, debug_draw)
117+
.chain()
118+
.distributive_run_if(DebugPickingMode::is_enabled)
119+
.in_set(PickSet::Last),
120+
);
121+
}
122+
}
123+
124+
/// Listen for any event and logs it at the debug level
125+
pub fn log_event_debug<E: Event + Debug>(mut events: EventReader<pointer::PointerInput>) {
126+
for event in events.read() {
127+
debug!("{event:?}");
128+
}
129+
}
130+
131+
/// Listens for pointer events of type `E` and logs them at "debug" level
132+
pub fn log_pointer_event_debug<E: Debug + Clone + Reflect>(
133+
mut pointer_events: EventReader<Pointer<E>>,
134+
) {
135+
for event in pointer_events.read() {
136+
debug!("{event}");
137+
}
138+
}
139+
140+
/// Listens for pointer events of type `E` and logs them at "trace" level
141+
pub fn log_pointer_event_trace<E: Debug + Clone + Reflect>(
142+
mut pointer_events: EventReader<Pointer<E>>,
143+
) {
144+
for event in pointer_events.read() {
145+
trace!("{event}");
146+
}
147+
}
148+
149+
/// Adds [`PointerDebug`] to pointers automatically.
150+
pub fn add_pointer_debug(
151+
mut commands: Commands,
152+
pointers: Query<Entity, (With<PointerId>, Without<PointerDebug>)>,
153+
) {
154+
for entity in &pointers {
155+
commands.entity(entity).insert(PointerDebug::default());
156+
}
157+
}
158+
159+
/// Hide text from pointers.
160+
pub fn pointer_debug_visibility(
161+
debug: Res<DebugPickingMode>,
162+
mut pointers: Query<&mut Visibility, With<PointerId>>,
163+
) {
164+
let visible = match *debug {
165+
DebugPickingMode::Disabled => Visibility::Hidden,
166+
_ => Visibility::Visible,
167+
};
168+
for mut vis in &mut pointers {
169+
*vis = visible;
170+
}
171+
}
172+
173+
/// Storage for per-pointer debug information.
174+
#[derive(Debug, Component, Clone, Default)]
175+
pub struct PointerDebug {
176+
/// The pointer location.
177+
pub location: Option<Location>,
178+
179+
/// Representation of the different pointer button states.
180+
pub press: PointerPress,
181+
182+
/// List of hit elements to be displayed.
183+
pub hits: Vec<(String, HitData)>,
184+
}
185+
186+
fn bool_to_icon(f: &mut Formatter, prefix: &str, input: bool) -> Result {
187+
write!(f, "{prefix}{}", if input { "[X]" } else { "[ ]" })
188+
}
189+
190+
impl Display for PointerDebug {
191+
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
192+
if let Some(location) = &self.location {
193+
writeln!(f, "Location: {:.2?}", location.position)?;
194+
}
195+
bool_to_icon(f, "Pressed: ", self.press.is_primary_pressed())?;
196+
bool_to_icon(f, " ", self.press.is_middle_pressed())?;
197+
bool_to_icon(f, " ", self.press.is_secondary_pressed())?;
198+
let mut sorted_hits = self.hits.clone();
199+
sorted_hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
200+
for (entity, hit) in sorted_hits.iter() {
201+
write!(f, "\nEntity: {entity:?}")?;
202+
if let Some((position, normal)) = hit.position.zip(hit.normal) {
203+
write!(f, ", Position: {position:.2?}, Normal: {normal:.2?}")?;
204+
}
205+
write!(f, ", Depth: {:.2?}", hit.depth)?;
206+
}
207+
208+
Ok(())
209+
}
210+
}
211+
212+
/// Update typed debug data used to draw overlays
213+
pub fn update_debug_data(
214+
hover_map: Res<HoverMap>,
215+
entity_names: Query<NameOrEntity>,
216+
mut pointers: Query<(
217+
&PointerId,
218+
&pointer::PointerLocation,
219+
&PointerPress,
220+
&mut PointerDebug,
221+
)>,
222+
) {
223+
for (id, location, press, mut debug) in &mut pointers {
224+
*debug = PointerDebug {
225+
location: location.location().cloned(),
226+
press: press.to_owned(),
227+
hits: hover_map
228+
.get(id)
229+
.iter()
230+
.flat_map(|h| h.iter())
231+
.filter_map(|(e, h)| {
232+
if let Ok(entity_name) = entity_names.get(*e) {
233+
Some((entity_name.to_string(), h.to_owned()))
234+
} else {
235+
None
236+
}
237+
})
238+
.collect(),
239+
};
240+
}
241+
}
242+
243+
/// Draw text on each cursor with debug info
244+
pub fn debug_draw(
245+
mut commands: Commands,
246+
camera_query: Query<(Entity, &Camera)>,
247+
primary_window: Query<Entity, With<bevy_window::PrimaryWindow>>,
248+
pointers: Query<(Entity, &PointerId, &PointerDebug)>,
249+
scale: Res<UiScale>,
250+
) {
251+
let font_handle: Handle<Font> = Default::default();
252+
for (entity, id, debug) in pointers.iter() {
253+
let Some(pointer_location) = &debug.location else {
254+
continue;
255+
};
256+
let text = format!("{id:?}\n{debug}");
257+
258+
for camera in camera_query
259+
.iter()
260+
.map(|(entity, camera)| {
261+
(
262+
entity,
263+
camera.target.normalize(primary_window.get_single().ok()),
264+
)
265+
})
266+
.filter_map(|(entity, target)| Some(entity).zip(target))
267+
.filter(|(_entity, target)| target == &pointer_location.target)
268+
.map(|(cam_entity, _target)| cam_entity)
269+
{
270+
let mut pointer_pos = pointer_location.position;
271+
if let Some(viewport) = camera_query
272+
.get(camera)
273+
.ok()
274+
.and_then(|(_, camera)| camera.logical_viewport_rect())
275+
{
276+
pointer_pos -= viewport.min;
277+
}
278+
279+
commands
280+
.entity(entity)
281+
.insert((
282+
Text::new(text.clone()),
283+
TextFont {
284+
font: font_handle.clone(),
285+
font_size: 12.0,
286+
..Default::default()
287+
},
288+
TextColor(Color::WHITE),
289+
Node {
290+
position_type: PositionType::Absolute,
291+
left: Val::Px(pointer_pos.x + 5.0) / scale.0,
292+
top: Val::Px(pointer_pos.y + 5.0) / scale.0,
293+
..Default::default()
294+
},
295+
))
296+
.insert(PickingBehavior::IGNORE)
297+
.insert(TargetCamera(camera));
298+
}
299+
}
300+
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ Example | Description
383383
Example | Description
384384
--- | ---
385385
[Mesh Picking](../examples/picking/mesh_picking.rs) | Demonstrates picking meshes
386+
[Picking Debug Tools](../examples/picking/debug_picking.rs) | Demonstrates picking debug overlay
386387
[Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects
387388
[Sprite Picking](../examples/picking/sprite_picking.rs) | Demonstrates picking sprites and sprite atlases
388389

0 commit comments

Comments
 (0)