Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
159 changes: 156 additions & 3 deletions crates/bevy_picking/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
//! 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 bevy_ecs::prelude::*;
use bevy_math::Vec3;
use bevy_reflect::Reflect;
Expand All @@ -39,13 +42,43 @@ 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 `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)]
/// struct MyHitInfo { triangle_index: u32 }
/// ```
///
/// Read it back with [`HitData::extra_as`]:
///
/// ```rust
/// # use bevy_picking::backend::HitData;
/// # #[derive(Debug)] struct MyHitInfo { triangle_index: u32 }
/// fn read_extra(hit: &HitData) {
/// if let Some(info) = hit.extra_as::<MyHitInfo>() {
/// println!("Hit triangle {}", info.triangle_index);
/// }
/// }
/// ```
pub trait HitDataExtra: Any + Send + Sync + fmt::Debug {}

impl<T: Send + Sync + fmt::Debug + Any + 'static> HitDataExtra for T {}

/// A message produced by a picking backend after it has run its hit tests, describing the entities
/// under a pointer.
///
Expand Down Expand Up @@ -95,8 +128,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.
Expand All @@ -111,6 +146,34 @@ pub struct HitData {
pub position: Option<Vec3>,
/// The normal vector of the hit test, if the data is available from the backend.
pub normal: Option<Vec3>,
/// 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<Arc<dyn HitDataExtra>>,
Comment thread
alice-i-cecile marked this conversation as resolved.
}

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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why doesn't this check extra? This should be documented and justified in that documentation.

It looks like PartialEq is derived for all events, I'm not sure if they are deduplicated somewhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equality would require downcasting both sides every time there is a comparison (https://users.rust-lang.org/t/how-to-compare-two-trait-objects-for-equality/88063), which feels overkill. The explanation was added in e7c410d

}

impl HitData {
Expand All @@ -121,6 +184,46 @@ impl HitData {
depth,
position,
normal,
extra: None,
}
}

/// 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<T: Any>(&self) -> Option<&T> {
let extra: &dyn Any = self.extra.as_deref()?;
extra.downcast_ref::<T>()
}

/// 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(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<Vec3>,
normal: Option<Vec3>,
extra: impl HitDataExtra,
) -> Self {
Self {
camera,
depth,
position,
normal,
extra: Some(Arc::new(extra)),
}
}
}
Expand Down Expand Up @@ -239,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::<TriangleHitInfo>(),
Some(&TriangleHitInfo { triangle_index: 7 })
);
assert_eq!(hit.extra_as::<OtherHitInfo>(), None);

let cloned = hit.clone();
assert_eq!(
cloned.extra_as::<TriangleHitInfo>(),
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);
}
}
1 change: 1 addition & 0 deletions crates/bevy_picking/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,7 @@ mod tests {
camera,
position: None,
normal: None,
extra: None,
},
);
}
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_picking/src/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ mod tests {
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
Expand Down Expand Up @@ -514,6 +515,7 @@ mod tests {
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
Expand Down Expand Up @@ -579,6 +581,7 @@ mod tests {
camera,
position: None,
normal: None,
extra: None,
},
);
hover_map.insert(PointerId::Mouse, entity_map);
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading