From 28f786a1085e266cf3b67c3e2c3237622b969217 Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Fri, 6 Mar 2026 12:00:52 +0100 Subject: [PATCH 01/11] Add support for custom picking data --- Cargo.toml | 12 + crates/bevy_picking/src/backend.rs | 105 ++++++++- crates/bevy_picking/src/events.rs | 1 + crates/bevy_picking/src/hover.rs | 3 + examples/picking/custom_hit_data.rs | 222 ++++++++++++++++++ .../ui/navigation/directional_navigation.rs | 1 + .../directional_navigation_overrides.rs | 1 + 7 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 examples/picking/custom_hit_data.rs diff --git a/Cargo.toml b/Cargo.toml index b257dd99a187c..a7d1c05c11f2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4983,6 +4983,18 @@ description = "Demonstrates picking meshes" category = "Picking" wasm = true +[[example]] +name = "custom_hit_data" +path = "examples/picking/custom_hit_data.rs" +doc-scrape-examples = true +required-features = ["mesh_picking"] + +[package.metadata.example.custom_hit_data] +name = "Custom Hit Data" +description = "Demonstrates a custom picking backend with custom hit data." +category = "Picking" +wasm = true + [[example]] name = "simple_picking" path = "examples/picking/simple_picking.rs" diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 83ba7fc010e96..152133ed3a931 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -31,6 +31,9 @@ //! automatically constructs rays in world space for all cameras and pointers, handling details like //! viewports and DPI for you. +use core::{any::Any, fmt}; +use std::sync::Arc; + use bevy_ecs::prelude::*; use bevy_math::Vec3; use bevy_reflect::Reflect; @@ -39,13 +42,45 @@ use bevy_reflect::Reflect; /// /// This includes the most common types in this module, re-exported for your convenience. pub mod prelude { - pub use super::{ray::RayMap, HitData, PointerHits}; + pub use super::{ray::RayMap, HitData, HitDataExtra, PointerHits}; pub use crate::{ pointer::{PointerId, PointerLocation}, Pickable, PickingSystems, }; } +/// Extra data attached to a [`HitData`] by a picking backend. +/// +/// Use this for backend-specific data like triangle indices, UVs, or material information. +/// Any `Clone + Send + Sync + fmt::Debug + 'static` type implements this trait automatically. +/// +/// ```rust +/// #[derive(Clone, Debug)] +/// struct MyHitInfo { triangle_index: u32 } +/// ``` +/// +/// Read it back with [`HitData::extra_as`]: +/// +/// ```rust +/// # use bevy_picking::backend::HitData; +/// # #[derive(Clone, Debug)] struct MyHitInfo { triangle_index: u32 } +/// fn read_extra(hit: &HitData) { +/// if let Some(info) = hit.extra_as::() { +/// println!("Hit triangle {}", info.triangle_index); +/// } +/// } +/// ``` +pub trait HitDataExtra: Any + Send + Sync + fmt::Debug { + /// Returns `self` as `&dyn Any` for downcasting. + fn as_any(&self) -> &dyn Any; +} + +impl HitDataExtra for T { + fn as_any(&self) -> &dyn Any { + self + } +} + /// A message produced by a picking backend after it has run its hit tests, describing the entities /// under a pointer. /// @@ -95,8 +130,10 @@ impl PointerHits { } /// Holds data from a successful pointer hit test. See [`HitData::depth`] for important details. -#[derive(Clone, Debug, PartialEq, Reflect)] -#[reflect(Clone, PartialEq)] +/// +/// Backends can attach arbitrary typed data via [`HitData::extra`]. See [`HitDataExtra`]. +#[derive(Debug, Reflect)] +#[reflect(Debug)] pub struct HitData { /// The camera entity used to detect this hit. Useful when you need to find the ray that was /// cast for this hit when using a raycasting backend. @@ -111,6 +148,32 @@ pub struct HitData { pub position: Option, /// The normal vector of the hit test, if the data is available from the backend. pub normal: Option, + /// Optional backend-specific extra data attached to this hit. Read it with + /// [`HitData::extra_as`]. This field is excluded from [`PartialEq`] + /// comparisons and reflection. + #[reflect(ignore)] + pub extra: Option>, +} + +impl Clone for HitData { + fn clone(&self) -> Self { + Self { + camera: self.camera, + depth: self.depth, + position: self.position, + normal: self.normal, + extra: self.extra.as_ref().map(Arc::clone), + } + } +} + +impl PartialEq for HitData { + fn eq(&self, other: &Self) -> bool { + self.camera == other.camera + && self.depth == other.depth + && self.position == other.position + && self.normal == other.normal + } } impl HitData { @@ -121,6 +184,42 @@ impl HitData { depth, position, normal, + extra: None, + } + } + + /// Returns any attached extra data as `T` if available. + pub fn extra_as(&self) -> Option<&T> { + self.extra.as_deref()?.as_any().downcast_ref::() + } + + /// Creates a [`HitData`] with backend-specific extra data. `extra` can be + /// any [`HitDataExtra`]. + /// + /// # Example + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// # use bevy_picking::backend::HitData; + /// #[derive(Clone, Debug)] + /// struct MyHitInfo { triangle_index: u32 } + /// + /// # let camera = Entity::PLACEHOLDER; + /// let hit = HitData::new_with_extra(camera, 1.0, None, None, MyHitInfo { triangle_index: 7 }); + /// ``` + pub fn new_with_extra( + camera: Entity, + depth: f32, + position: Option, + normal: Option, + extra: impl HitDataExtra, + ) -> Self { + Self { + camera, + depth, + position, + normal, + extra: Some(Arc::new(extra)), } } } diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index ee2cf133723f2..25efafcfe2a95 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -1248,6 +1248,7 @@ mod tests { camera, position: None, normal: None, + extra: None, }, ); } diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 7a7723885e219..aeb7365e35601 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -462,6 +462,7 @@ mod tests { camera, position: None, normal: None, + extra: None, }, ); hover_map.insert(PointerId::Mouse, entity_map); @@ -514,6 +515,7 @@ mod tests { camera, position: None, normal: None, + extra: None, }, ); hover_map.insert(PointerId::Mouse, entity_map); @@ -579,6 +581,7 @@ mod tests { camera, position: None, normal: None, + extra: None, }, ); hover_map.insert(PointerId::Mouse, entity_map); diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs new file mode 100644 index 0000000000000..04cf6f62db7a3 --- /dev/null +++ b/examples/picking/custom_hit_data.rs @@ -0,0 +1,222 @@ +//! Demonstrates a custom picking backend with custom hit data. +//! +//! The backend writes triangle vertices into [`PointerHits`], and a follow-up +//! system draws an outline around the hovered triangle. + +use bevy::{ + color::palettes::css::*, + picking::{ + backend::{ray::RayMap, HitData, PointerHits}, + mesh_picking::{ + ray_cast::{MeshRayCast, MeshRayCastSettings, RayCastVisibility}, + MeshPickingSettings, + }, + prelude::*, + PickingSettings, PickingSystems, + }, + prelude::*, +}; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, MeshPickingPlugin)) + .insert_resource(MeshPickingSettings { + require_markers: true, + ..default() + }) + .insert_resource(PickingSettings { + is_window_picking_enabled: false, + ..default() + }) + .init_resource::() + .add_systems(Startup, (setup_gizmos, setup_scene)) + .add_systems( + PreUpdate, + ( + cache_hovered_triangles.after(PickingSystems::Backend), + custom_backend_system.in_set(PickingSystems::Backend), + ), + ) + .add_systems(Update, draw_hit_gizmos) + .run(); +} + +#[derive(Clone, Debug)] +struct TriangleHitInfo { + triangle_vertices: Option<[Vec3; 3]>, +} + +#[derive(Component)] +struct Shape; + +#[derive(Resource, Default)] +struct HoveredTriangles(Vec); + +struct TriangleOverlay { + position: Vec3, + normal: Vec3, + vertices: [Vec3; 3], +} + +fn custom_backend_system( + ray_map: Res, + cameras: Query<&Camera>, + pickables: Query<&Pickable>, + shapes: Query<(), With>, + mut ray_cast: MeshRayCast, + mut pointer_hits: MessageWriter, +) { + for (&ray_id, &ray) in ray_map.iter() { + let Ok(camera) = cameras.get(ray_id.camera) else { + continue; + }; + + let settings = MeshRayCastSettings { + visibility: RayCastVisibility::VisibleInView, + filter: &|entity| shapes.contains(entity), + early_exit_test: &|entity_hit| { + pickables + .get(entity_hit) + .is_ok_and(|p| p.should_block_lower) + }, + }; + + let picks: Vec<(Entity, HitData)> = ray_cast + .cast_ray(ray, &settings) + .iter() + .map(|(entity, hit)| { + let extra = TriangleHitInfo { + triangle_vertices: hit.triangle, + }; + + let hit_data = HitData::new_with_extra( + ray_id.camera, + hit.distance, + Some(hit.point), + Some(hit.normal), + extra, + ); + + (*entity, hit_data) + }) + .collect(); + + if !picks.is_empty() { + pointer_hits.write(PointerHits::new(ray_id.pointer, picks, camera.order as f32)); + } + } +} + +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let shapes: &[(&str, Mesh, Color)] = &[ + ("Cube", Mesh::from(Cuboid::default()), RED.into()), + ( + "Sphere", + Mesh::from(Sphere::default().mesh().ico(2).unwrap()), + GREEN.into(), + ), + ( + "Cylinder", + Mesh::from(Cylinder { + half_height: 0.5, + radius: 0.5, + }), + BLUE.into(), + ), + ]; + + let n = shapes.len() as f32; + for (i, (_name, mesh, color)) in shapes.iter().enumerate() { + let x = (i as f32 - (n - 1.0) / 2.0) * 2.2; + let material = materials.add(StandardMaterial { + base_color: *color, + ..default() + }); + + commands.spawn(( + Mesh3d(meshes.add(mesh.clone())), + MeshMaterial3d(material), + Transform::from_xyz(x, 0.5, 0.0), + Shape, + Pickable::default(), + )); + } + + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))), + MeshMaterial3d(materials.add(Color::from(DARK_GRAY))), + Pickable::IGNORE, + )); + + commands.spawn(( + PointLight { + intensity: 3_000_000.0, + range: 50.0, + ..default() + }, + Transform::from_xyz(0.0, 8.0, 4.0), + )); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 2.5, 6.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + )); +} + +fn setup_gizmos(mut config_store: ResMut) { + let (config, _) = config_store.config_mut::(); + config.depth_bias = -1.0; + config.line.width = 3.0; +} + +fn cache_hovered_triangles( + mut pointer_hits: MessageReader, + mut hovered_triangles: ResMut, +) { + hovered_triangles.0.clear(); + + for hits in pointer_hits.read() { + for (_, hit) in &hits.picks { + let (Some(position), Some(normal)) = (hit.position, hit.normal) else { + continue; + }; + + let Some(info) = hit.extra_as::() else { + continue; + }; + let Some(vertices) = info.triangle_vertices else { + continue; + }; + + hovered_triangles.0.push(TriangleOverlay { + position, + normal, + vertices, + }); + } + } +} + +fn draw_hit_gizmos(hovered_triangles: Res, mut gizmos: Gizmos) { + for triangle in &hovered_triangles.0 { + gizmos.arrow( + triangle.position, + triangle.position + triangle.normal.normalize() * 0.5, + WHITE, + ); + + let vertices = triangle.vertices; + let center = (vertices[0] + vertices[1] + vertices[2]) / 3.0; + let offset = triangle.normal.normalize_or_zero() * 0.025; + + let outline = vertices.map(|vertex| center + (vertex - center) * 1.05 + offset); + + gizmos.line(outline[0], outline[1], WHITE); + gizmos.line(outline[1], outline[2], WHITE); + gizmos.line(outline[2], outline[0], WHITE); + } +} diff --git a/examples/ui/navigation/directional_navigation.rs b/examples/ui/navigation/directional_navigation.rs index 78c125dc5a1aa..ce67448627cf9 100644 --- a/examples/ui/navigation/directional_navigation.rs +++ b/examples/ui/navigation/directional_navigation.rs @@ -462,6 +462,7 @@ fn interact_with_focused_button( depth: 0.0, position: None, normal: None, + extra: None, }, duration: Duration::from_secs_f32(0.1), }, diff --git a/examples/ui/navigation/directional_navigation_overrides.rs b/examples/ui/navigation/directional_navigation_overrides.rs index f0f39e86a037a..351d964233774 100644 --- a/examples/ui/navigation/directional_navigation_overrides.rs +++ b/examples/ui/navigation/directional_navigation_overrides.rs @@ -855,6 +855,7 @@ fn interact_with_focused_button( depth: 0.0, position: None, normal: None, + extra: None, }, duration: Duration::from_secs_f32(0.1), }, From 1ab9ae741710b10ec5d319df0297c5acff3f7ab5 Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Fri, 6 Mar 2026 12:26:41 +0100 Subject: [PATCH 02/11] fix missing examples in docs --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index da73df5181a67..0db6f5acf2cfd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -438,6 +438,7 @@ Example | Description Example | Description --- | --- +[Custom Hit Data](../examples/picking/custom_hit_data.rs) | Demonstrates a custom picking backend with custom hit data. [Drag and Drop](../examples/picking/dragdrop_picking.rs) | Demonstrates drag and drop using picking events [Mesh Picking](../examples/picking/mesh_picking.rs) | Demonstrates picking meshes [Picking Debug Tools](../examples/picking/debug_picking.rs) | Demonstrates picking debug overlay From 91368e902d2d215ea90a11cf7f0db6139a09952b Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Fri, 6 Mar 2026 12:43:41 +0100 Subject: [PATCH 03/11] import from alloc --- crates/bevy_picking/src/backend.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 152133ed3a931..e9a60b3e15314 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -31,8 +31,8 @@ //! automatically constructs rays in world space for all cameras and pointers, handling details like //! viewports and DPI for you. +use alloc::sync::Arc; use core::{any::Any, fmt}; -use std::sync::Arc; use bevy_ecs::prelude::*; use bevy_math::Vec3; From 19b23036dde4d6b565fc9e69137d47ea3785149e Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Fri, 6 Mar 2026 13:18:10 +0100 Subject: [PATCH 04/11] simplify example --- examples/picking/custom_hit_data.rs | 45 ++++++----------------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index 04cf6f62db7a3..7cc2008c8ca6d 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -46,9 +46,6 @@ struct TriangleHitInfo { triangle_vertices: Option<[Vec3; 3]>, } -#[derive(Component)] -struct Shape; - #[derive(Resource, Default)] struct HoveredTriangles(Vec); @@ -62,7 +59,6 @@ fn custom_backend_system( ray_map: Res, cameras: Query<&Camera>, pickables: Query<&Pickable>, - shapes: Query<(), With>, mut ray_cast: MeshRayCast, mut pointer_hits: MessageWriter, ) { @@ -73,7 +69,7 @@ fn custom_backend_system( let settings = MeshRayCastSettings { visibility: RayCastVisibility::VisibleInView, - filter: &|entity| shapes.contains(entity), + filter: &|e| pickables.get(e).map_or(false, |p| p.is_hoverable), early_exit_test: &|entity_hit| { pickables .get(entity_hit) @@ -112,36 +108,20 @@ fn setup_scene( mut meshes: ResMut>, mut materials: ResMut>, ) { - let shapes: &[(&str, Mesh, Color)] = &[ - ("Cube", Mesh::from(Cuboid::default()), RED.into()), - ( - "Sphere", - Mesh::from(Sphere::default().mesh().ico(2).unwrap()), - GREEN.into(), - ), - ( - "Cylinder", - Mesh::from(Cylinder { - half_height: 0.5, - radius: 0.5, - }), - BLUE.into(), - ), + let shapes: [(Mesh, Color); 3] = [ + (Cuboid::default().into(), RED.into()), + (Sphere::default().mesh().ico(2).unwrap(), GREEN.into()), + (Cylinder::default().into(), BLUE.into()), ]; - let n = shapes.len() as f32; - for (i, (_name, mesh, color)) in shapes.iter().enumerate() { - let x = (i as f32 - (n - 1.0) / 2.0) * 2.2; - let material = materials.add(StandardMaterial { - base_color: *color, - ..default() - }); + for (i, (mesh, color)) in shapes.iter().enumerate() { + let x = i as f32 * 1.5 - 1.5; + let material = materials.add(StandardMaterial::from_color(*color)); commands.spawn(( Mesh3d(meshes.add(mesh.clone())), MeshMaterial3d(material), Transform::from_xyz(x, 0.5, 0.0), - Shape, Pickable::default(), )); } @@ -152,14 +132,7 @@ fn setup_scene( Pickable::IGNORE, )); - commands.spawn(( - PointLight { - intensity: 3_000_000.0, - range: 50.0, - ..default() - }, - Transform::from_xyz(0.0, 8.0, 4.0), - )); + commands.spawn((PointLight::default(), Transform::from_xyz(0.0, 8.0, 4.0))); commands.spawn(( Camera3d::default(), From 46c7ef7259fbfe77e30780766f61610d029fe1be Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Fri, 6 Mar 2026 13:52:02 +0100 Subject: [PATCH 05/11] lint --- examples/picking/custom_hit_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index 7cc2008c8ca6d..778afb18ff5aa 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -69,7 +69,7 @@ fn custom_backend_system( let settings = MeshRayCastSettings { visibility: RayCastVisibility::VisibleInView, - filter: &|e| pickables.get(e).map_or(false, |p| p.is_hoverable), + filter: &|e| pickables.get(e).is_ok_and(|p| p.is_hoverable), early_exit_test: &|entity_hit| { pickables .get(entity_hit) From 2a62f95dd48da74d25c5d8e9f65b574812b3058e Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Mon, 9 Mar 2026 08:31:19 +0100 Subject: [PATCH 06/11] Reorder function positions --- examples/picking/custom_hit_data.rs | 88 ++++++++++++++--------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index 778afb18ff5aa..9c08fc9dcd1a9 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -33,8 +33,8 @@ fn main() { .add_systems( PreUpdate, ( - cache_hovered_triangles.after(PickingSystems::Backend), custom_backend_system.in_set(PickingSystems::Backend), + cache_hovered_triangles.after(PickingSystems::Backend), ), ) .add_systems(Update, draw_hit_gizmos) @@ -55,6 +55,49 @@ struct TriangleOverlay { vertices: [Vec3; 3], } +fn setup_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let shapes: [(Mesh, Color); 3] = [ + (Cuboid::default().into(), RED.into()), + (Sphere::default().mesh().ico(2).unwrap(), GREEN.into()), + (Cylinder::default().into(), BLUE.into()), + ]; + + for (i, (mesh, color)) in shapes.iter().enumerate() { + let x = i as f32 * 1.5 - 1.5; + let material = materials.add(StandardMaterial::from_color(*color)); + + commands.spawn(( + Mesh3d(meshes.add(mesh.clone())), + MeshMaterial3d(material), + Transform::from_xyz(x, 0.5, 0.0), + Pickable::default(), + )); + } + + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))), + MeshMaterial3d(materials.add(Color::from(DARK_GRAY))), + Pickable::IGNORE, + )); + + commands.spawn((PointLight::default(), Transform::from_xyz(0.0, 8.0, 4.0))); + + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 2.5, 6.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + )); +} + +fn setup_gizmos(mut config_store: ResMut) { + let (config, _) = config_store.config_mut::(); + config.depth_bias = -1.0; + config.line.width = 3.0; +} + fn custom_backend_system( ray_map: Res, cameras: Query<&Camera>, @@ -103,49 +146,6 @@ fn custom_backend_system( } } -fn setup_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let shapes: [(Mesh, Color); 3] = [ - (Cuboid::default().into(), RED.into()), - (Sphere::default().mesh().ico(2).unwrap(), GREEN.into()), - (Cylinder::default().into(), BLUE.into()), - ]; - - for (i, (mesh, color)) in shapes.iter().enumerate() { - let x = i as f32 * 1.5 - 1.5; - let material = materials.add(StandardMaterial::from_color(*color)); - - commands.spawn(( - Mesh3d(meshes.add(mesh.clone())), - MeshMaterial3d(material), - Transform::from_xyz(x, 0.5, 0.0), - Pickable::default(), - )); - } - - commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(30.0, 30.0))), - MeshMaterial3d(materials.add(Color::from(DARK_GRAY))), - Pickable::IGNORE, - )); - - commands.spawn((PointLight::default(), Transform::from_xyz(0.0, 8.0, 4.0))); - - commands.spawn(( - Camera3d::default(), - Transform::from_xyz(0.0, 2.5, 6.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - )); -} - -fn setup_gizmos(mut config_store: ResMut) { - let (config, _) = config_store.config_mut::(); - config.depth_bias = -1.0; - config.line.width = 3.0; -} - fn cache_hovered_triangles( mut pointer_hits: MessageReader, mut hovered_triangles: ResMut, From b1f078a8abd6b0b99344ddf0a6006bf0472b2bcf Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Mon, 9 Mar 2026 08:42:04 +0100 Subject: [PATCH 07/11] Explanatory comments --- examples/picking/custom_hit_data.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index 9c08fc9dcd1a9..0f72c592a9d11 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -1,7 +1,12 @@ //! Demonstrates a custom picking backend with custom hit data. //! -//! The backend writes triangle vertices into [`PointerHits`], and a follow-up -//! system draws an outline around the hovered triangle. +//! The example contains pickable 3D meshes. When a mesh is hovered, a custom +//! picking backend performs a ray cast against the mesh and retrieves the +//! triangle that was hit. The triangle vertices are stored in a custom struct +//! (`TriangleHitInfo`) that implements `HitDataExtra`, and saved into `HitData` +//! structs. This information is not available by default in `HitData` and thus +//! requires its `extra` field. A follow-up system reads the hit data and draws +//! an outline around the hovered triangle using gizmos. use bevy::{ color::palettes::css::*, @@ -41,6 +46,9 @@ fn main() { .run(); } +/// The custom hit data used by our picking backend. All structs that implement +/// `Clone + Send + Sync + fmt::Debug + 'static` automatically implement +/// `HitDataExtra` and can be used as extra data in `HitData`. #[derive(Clone, Debug)] struct TriangleHitInfo { triangle_vertices: Option<[Vec3; 3]>, @@ -186,6 +194,8 @@ fn draw_hit_gizmos(hovered_triangles: Res, mut gizmos: Gizmos) let center = (vertices[0] + vertices[1] + vertices[2]) / 3.0; let offset = triangle.normal.normalize_or_zero() * 0.025; + // The outline is made bigger and offset a bit to prevent being covered + // by the mesh let outline = vertices.map(|vertex| center + (vertex - center) * 1.05 + offset); gizmos.line(outline[0], outline[1], WHITE); From 94114a9b88696a1bfce67a9969d4ec5fbc430738 Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Thu, 23 Apr 2026 11:04:16 +0200 Subject: [PATCH 08/11] Remove as_any helper --- crates/bevy_picking/src/backend.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index e9a60b3e15314..b15c81e05dbcc 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -70,16 +70,9 @@ pub mod prelude { /// } /// } /// ``` -pub trait HitDataExtra: Any + Send + Sync + fmt::Debug { - /// Returns `self` as `&dyn Any` for downcasting. - fn as_any(&self) -> &dyn Any; -} +pub trait HitDataExtra: Any + Send + Sync + fmt::Debug {} -impl HitDataExtra for T { - fn as_any(&self) -> &dyn Any { - self - } -} +impl HitDataExtra for T {} /// A message produced by a picking backend after it has run its hit tests, describing the entities /// under a pointer. @@ -190,7 +183,8 @@ impl HitData { /// Returns any attached extra data as `T` if available. pub fn extra_as(&self) -> Option<&T> { - self.extra.as_deref()?.as_any().downcast_ref::() + let extra: &dyn Any = self.extra.as_deref()?; + extra.downcast_ref::() } /// Creates a [`HitData`] with backend-specific extra data. `extra` can be From c815c8db19f207a6a4a41b5f257f47b283f8cbd4 Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Thu, 23 Apr 2026 11:12:49 +0200 Subject: [PATCH 09/11] Remove unneeded clone requirement --- crates/bevy_picking/src/backend.rs | 10 +++++----- examples/picking/custom_hit_data.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index b15c81e05dbcc..795a3bbd5d127 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -52,10 +52,10 @@ pub mod prelude { /// Extra data attached to a [`HitData`] by a picking backend. /// /// Use this for backend-specific data like triangle indices, UVs, or material information. -/// Any `Clone + Send + Sync + fmt::Debug + 'static` type implements this trait automatically. +/// Any `Send + Sync + fmt::Debug + 'static` type implements this trait automatically. /// /// ```rust -/// #[derive(Clone, Debug)] +/// #[derive(Debug)] /// struct MyHitInfo { triangle_index: u32 } /// ``` /// @@ -63,7 +63,7 @@ pub mod prelude { /// /// ```rust /// # use bevy_picking::backend::HitData; -/// # #[derive(Clone, Debug)] struct MyHitInfo { triangle_index: u32 } +/// # #[derive(Debug)] struct MyHitInfo { triangle_index: u32 } /// fn read_extra(hit: &HitData) { /// if let Some(info) = hit.extra_as::() { /// println!("Hit triangle {}", info.triangle_index); @@ -72,7 +72,7 @@ pub mod prelude { /// ``` pub trait HitDataExtra: Any + Send + Sync + fmt::Debug {} -impl HitDataExtra for T {} +impl HitDataExtra for T {} /// A message produced by a picking backend after it has run its hit tests, describing the entities /// under a pointer. @@ -195,7 +195,7 @@ impl HitData { /// ```rust /// # use bevy_ecs::prelude::*; /// # use bevy_picking::backend::HitData; - /// #[derive(Clone, Debug)] + /// #[derive(Debug)] /// struct MyHitInfo { triangle_index: u32 } /// /// # let camera = Entity::PLACEHOLDER; diff --git a/examples/picking/custom_hit_data.rs b/examples/picking/custom_hit_data.rs index 0f72c592a9d11..dabbfa159eca9 100644 --- a/examples/picking/custom_hit_data.rs +++ b/examples/picking/custom_hit_data.rs @@ -47,9 +47,9 @@ fn main() { } /// The custom hit data used by our picking backend. All structs that implement -/// `Clone + Send + Sync + fmt::Debug + 'static` automatically implement -/// `HitDataExtra` and can be used as extra data in `HitData`. -#[derive(Clone, Debug)] +/// `Send + Sync + fmt::Debug + 'static` automatically implement `HitDataExtra` +/// and can be used as extra data in `HitData`. +#[derive(Debug)] struct TriangleHitInfo { triangle_vertices: Option<[Vec3; 3]>, } From e7c410dff51d34cf6b621570f1b5ec42ffcfc5e3 Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Thu, 23 Apr 2026 12:07:47 +0200 Subject: [PATCH 10/11] Add more design motivation in doc comments. --- crates/bevy_picking/src/backend.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 795a3bbd5d127..b20bc810a7069 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -51,8 +51,13 @@ pub mod prelude { /// Extra data attached to a [`HitData`] by a picking backend. /// -/// Use this for backend-specific data like triangle indices, UVs, or material information. -/// Any `Send + Sync + fmt::Debug + 'static` type implements this trait automatically. +/// Use this for backend-specific data like triangle indices, UVs, or material +/// information. +/// Any `Send + Sync + fmt::Debug + 'static` type implements this trait +/// automatically. `Clone` is not required: extra data is stored in an [`Arc`], +/// so [`HitData`] can still implement [`Clone`]. `Clone` requires knowing the +/// size of the type, which is not possible with dynamically dispatched types, +/// so it cannot be used for `dyn HitDataExtra`. /// /// ```rust /// #[derive(Debug)] @@ -141,9 +146,11 @@ pub struct HitData { pub position: Option, /// The normal vector of the hit test, if the data is available from the backend. pub normal: Option, - /// Optional backend-specific extra data attached to this hit. Read it with - /// [`HitData::extra_as`]. This field is excluded from [`PartialEq`] - /// comparisons and reflection. + /// Optional backend-specific extra data attached to this hit. Read it with [`HitData::extra_as`]. + /// + /// This is stored in an [`Arc`] so cloning [`HitData`] stays cheap. This field is excluded + /// from [`PartialEq`] because value equality for trait objects would require extra dynamic + /// downcasting that the picking pipeline does not need. #[reflect(ignore)] pub extra: Option>, } @@ -182,6 +189,9 @@ impl HitData { } /// Returns any attached extra data as `T` if available. + /// + /// This returns `None` if no extra data was attached, or if the hit stores a + /// different concrete extra data type. pub fn extra_as(&self) -> Option<&T> { let extra: &dyn Any = self.extra.as_deref()?; extra.downcast_ref::() From 94183a8c619a9651dc13b886e755b1d2955576da Mon Sep 17 00:00:00 2001 From: Martin Edlund Date: Thu, 23 Apr 2026 12:27:22 +0200 Subject: [PATCH 11/11] Add unit test for extra hit data --- crates/bevy_picking/src/backend.rs | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index b20bc810a7069..9e3f91b09a3d1 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -342,3 +342,53 @@ pub mod ray { .ok() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, PartialEq)] + struct TriangleHitInfo { + triangle_index: u32, + } + + #[derive(Debug, PartialEq)] + struct OtherHitInfo { + triangle_index: u32, + } + + #[test] + fn hit_data_extra() { + let camera = Entity::PLACEHOLDER; + + let hit = HitData::new_with_extra( + camera, + 1.0, + Some(Vec3::new(1.0, 2.0, 3.0)), + Some(Vec3::Y), + TriangleHitInfo { triangle_index: 7 }, + ); + + assert_eq!( + hit.extra_as::(), + Some(&TriangleHitInfo { triangle_index: 7 }) + ); + assert_eq!(hit.extra_as::(), None); + + let cloned = hit.clone(); + assert_eq!( + cloned.extra_as::(), + Some(&TriangleHitInfo { triangle_index: 7 }) + ); + + let other_extra = HitData::new_with_extra( + camera, + 1.0, + Some(Vec3::new(1.0, 2.0, 3.0)), + Some(Vec3::Y), + TriangleHitInfo { triangle_index: 99 }, + ); + + assert_eq!(hit, other_extra); + } +}