mirror of
https://github.com/bevyengine/bevy
synced 2024-11-24 21:53:07 +00:00
Upstream CorePlugin
from bevy_mod_picking
(#13677)
# Objective This is the first of a series of PRs intended to begin the upstreaming process for `bevy_mod_picking`. The purpose of this PR is to: + Create the new `bevy_picking` crate + Upstream `CorePlugin` as `PickingPlugin` + Upstream the core pointer and backend abstractions. This code has been ported verbatim from the corresponding files in [bevy_picking_core](https://github.com/aevyrie/bevy_mod_picking/tree/main/crates/bevy_picking_core/src) with a few tiny naming and docs tweaks. The work here is only an initial foothold to get the up-streaming process started in earnest. We can do refactoring and improvements once this is in-tree. --------- Co-authored-by: Aevyrie <aevyrie@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
parent
eb3c81374a
commit
aaccbe88aa
10 changed files with 799 additions and 4 deletions
|
@ -63,6 +63,7 @@ default = [
|
|||
"bevy_winit",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_pbr",
|
||||
"bevy_picking",
|
||||
"bevy_gltf",
|
||||
"bevy_render",
|
||||
"bevy_sprite",
|
||||
|
@ -123,6 +124,9 @@ bevy_pbr = [
|
|||
"bevy_core_pipeline",
|
||||
]
|
||||
|
||||
# Provides picking functionality
|
||||
bevy_picking = ["bevy_internal/bevy_picking"]
|
||||
|
||||
# Provides rendering functionality
|
||||
bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
|
||||
|
||||
|
|
|
@ -180,6 +180,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
|
|||
# Provides a collection of developer tools
|
||||
bevy_dev_tools = ["dep:bevy_dev_tools"]
|
||||
|
||||
# Provides a picking functionality
|
||||
bevy_picking = ["dep:bevy_picking"]
|
||||
|
||||
# Enable support for the ios_simulator by downgrading some rendering capabilities
|
||||
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"]
|
||||
|
||||
|
@ -214,18 +217,19 @@ bevy_asset = { path = "../bevy_asset", optional = true, version = "0.14.0-dev" }
|
|||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
|
||||
bevy_color = { path = "../bevy_color", optional = true, version = "0.14.0-dev" }
|
||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.14.0-dev" }
|
||||
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
|
||||
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
|
||||
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.14.0-dev" }
|
||||
bevy_picking = { path = "../bevy_picking", optional = true, version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.14.0-dev" }
|
||||
bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.14.0-dev" }
|
||||
bevy_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
|
||||
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.14.0-dev" }
|
||||
bevy_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
|
||||
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.14.0-dev" }
|
||||
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.14.0-dev" }
|
||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.14.0-dev", default-features = false }
|
||||
bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.14.0-dev" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -44,6 +44,8 @@ pub use bevy_log as log;
|
|||
pub use bevy_math as math;
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
pub use bevy_pbr as pbr;
|
||||
#[cfg(feature = "bevy_picking")]
|
||||
pub use bevy_picking as picking;
|
||||
pub use bevy_ptr as ptr;
|
||||
pub use bevy_reflect as reflect;
|
||||
#[cfg(feature = "bevy_render")]
|
||||
|
|
27
crates/bevy_picking/Cargo.toml
Normal file
27
crates/bevy_picking/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "bevy_picking"
|
||||
version = "0.14.0-dev"
|
||||
edition = "2021"
|
||||
description = "Provides screen picking functionality for Bevy Engine"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
|
||||
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
|
||||
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
|
||||
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
|
||||
|
||||
uuid = { version = "1.1", features = ["v4"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"]
|
||||
all-features = true
|
1
crates/bevy_picking/README.md
Normal file
1
crates/bevy_picking/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# Bevy Picking
|
232
crates/bevy_picking/src/backend.rs
Normal file
232
crates/bevy_picking/src/backend.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
//! This module provides a simple interface for implementing a picking backend.
|
||||
//!
|
||||
//! Don't be dissuaded by terminology like "backend"; the idea is dead simple. `bevy_picking`
|
||||
//! will tell you where pointers are, all you have to do is send an event if the pointers are
|
||||
//! hitting something. That's it. The rest of this documentation explains the requirements in more
|
||||
//! detail.
|
||||
//!
|
||||
//! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as
|
||||
//! many backends as you want. For example, You could use the `rapier` backend to raycast against
|
||||
//! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend
|
||||
//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted,
|
||||
//! and used as a homogeneous input for the picking systems that consume these events.
|
||||
//!
|
||||
//! ## Implementation
|
||||
//!
|
||||
//! - A picking backend only has one job: read [`PointerLocation`](crate::pointer::PointerLocation)
|
||||
//! components and produce [`PointerHits`] events. In plain English, a backend is provided the
|
||||
//! location of pointers, and is asked to provide a list of entities under those pointers.
|
||||
//!
|
||||
//! - The [`PointerHits`] events produced by a backend do **not** need to be sorted or filtered, all
|
||||
//! that is needed is an unordered list of entities and their [`HitData`].
|
||||
//!
|
||||
//! - Backends do not need to consider the [`Pickable`](crate::Pickable) component, though they may
|
||||
//! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy
|
||||
//! may want to early exit if it intersects an entity that blocks lower entities from being
|
||||
//! picked.
|
||||
//!
|
||||
//! ### Raycasting Backends
|
||||
//!
|
||||
//! Backends that require a ray to cast into the scene should use [`ray::RayMap`]. This
|
||||
//! automatically constructs rays in world space for all cameras and pointers, handling details like
|
||||
//! viewports and DPI for you.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::Vec3;
|
||||
use bevy_reflect::Reflect;
|
||||
|
||||
/// Common imports for implementing a picking backend.
|
||||
pub mod prelude {
|
||||
pub use super::{ray::RayMap, HitData, PointerHits};
|
||||
pub use crate::{
|
||||
pointer::{PointerId, PointerLocation},
|
||||
PickSet, Pickable,
|
||||
};
|
||||
}
|
||||
|
||||
/// An event produced by a picking backend after it has run its hit tests, describing the entities
|
||||
/// under a pointer.
|
||||
///
|
||||
/// Some backends may only support providing the topmost entity; this is a valid limitation of some
|
||||
/// backends. For example, a picking shader might only have data on the topmost rendered output from
|
||||
/// its buffer.
|
||||
#[derive(Event, Debug, Clone)]
|
||||
pub struct PointerHits {
|
||||
/// The pointer associated with this hit test.
|
||||
pub pointer: prelude::PointerId,
|
||||
/// An unordered collection of entities and their distance (depth) from the cursor.
|
||||
pub picks: Vec<(Entity, HitData)>,
|
||||
/// Set the order of this group of picks. Normally, this is the
|
||||
/// [`bevy_render::camera::Camera::order`].
|
||||
///
|
||||
/// Used to allow multiple `PointerHits` submitted for the same pointer to be ordered.
|
||||
/// `PointerHits` with a higher `order` will be checked before those with a lower `order`,
|
||||
/// regardless of the depth of each entity pick.
|
||||
///
|
||||
/// In other words, when pick data is coalesced across all backends, the data is grouped by
|
||||
/// pointer, then sorted by order, and checked sequentially, sorting each `PointerHits` by
|
||||
/// entity depth. Events with a higher `order` are effectively on top of events with a lower
|
||||
/// order.
|
||||
///
|
||||
/// ### Why is this an `f32`???
|
||||
///
|
||||
/// Bevy UI is special in that it can share a camera with other things being rendered. in order
|
||||
/// to properly sort them, we need a way to make `bevy_ui`'s order a tiny bit higher, like adding
|
||||
/// 0.5 to the order. We can't use integers, and we want users to be using camera.order by
|
||||
/// default, so this is the best solution at the moment.
|
||||
pub order: f32,
|
||||
}
|
||||
|
||||
impl PointerHits {
|
||||
#[allow(missing_docs)]
|
||||
pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self {
|
||||
Self {
|
||||
pointer,
|
||||
picks,
|
||||
order,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds data from a successful pointer hit test. See [`HitData::depth`] for important details.
|
||||
#[derive(Clone, Debug, PartialEq, Reflect)]
|
||||
pub struct HitData {
|
||||
/// The camera entity used to detect this hit. Useful when you need to find the ray that was
|
||||
/// casted for this hit when using a raycasting backend.
|
||||
pub camera: Entity,
|
||||
/// `depth` only needs to be self-consistent with other [`PointerHits`]s using the same
|
||||
/// [`RenderTarget`](bevy_render::camera::RenderTarget). However, it is recommended to use the
|
||||
/// distance from the pointer to the hit, measured from the near plane of the camera, to the
|
||||
/// point, in world space.
|
||||
pub depth: f32,
|
||||
/// The position of the intersection in the world, if the data is available from the backend.
|
||||
pub position: Option<Vec3>,
|
||||
/// The normal vector of the hit test, if the data is available from the backend.
|
||||
pub normal: Option<Vec3>,
|
||||
}
|
||||
|
||||
impl HitData {
|
||||
#[allow(missing_docs)]
|
||||
pub fn new(camera: Entity, depth: f32, position: Option<Vec3>, normal: Option<Vec3>) -> Self {
|
||||
Self {
|
||||
camera,
|
||||
depth,
|
||||
position,
|
||||
normal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod ray {
|
||||
//! Types and systems for constructing rays from cameras and pointers.
|
||||
|
||||
use crate::backend::prelude::{PointerId, PointerLocation};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::Ray3d;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::camera::Camera;
|
||||
use bevy_transform::prelude::GlobalTransform;
|
||||
use bevy_utils::{hashbrown::hash_map::Iter, HashMap};
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
/// Identifies a ray constructed from some (pointer, camera) combination. A pointer can be over
|
||||
/// multiple cameras, which is why a single pointer may have multiple rays.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Reflect)]
|
||||
pub struct RayId {
|
||||
/// The camera whose projection was used to calculate the ray.
|
||||
pub camera: Entity,
|
||||
/// The pointer whose pixel coordinates were used to calculate the ray.
|
||||
pub pointer: PointerId,
|
||||
}
|
||||
|
||||
impl RayId {
|
||||
/// Construct a [`RayId`].
|
||||
pub fn new(camera: Entity, pointer: PointerId) -> Self {
|
||||
Self { camera, pointer }
|
||||
}
|
||||
}
|
||||
|
||||
/// A map from [`RayId`] to [`Ray3d`].
|
||||
///
|
||||
/// This map is cleared and re-populated every frame before any backends run. Ray-based picking
|
||||
/// backends should use this when possible, as it automatically handles viewports, DPI, and
|
||||
/// other details of building rays from pointer locations.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
/// Iterate over each [`Ray3d`] and its [`RayId`] with [`RayMap::iter`].
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_ecs::prelude::*;
|
||||
/// # use bevy_picking::backend::ray::RayMap;
|
||||
/// # use bevy_picking::backend::PointerHits;
|
||||
/// // My raycasting backend
|
||||
/// pub fn update_hits(ray_map: Res<RayMap>, mut output_events: EventWriter<PointerHits>,) {
|
||||
/// for (&ray_id, &ray) in ray_map.iter() {
|
||||
/// // Run a raycast with each ray, returning any `PointerHits` found.
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct RayMap {
|
||||
map: HashMap<RayId, Ray3d>,
|
||||
}
|
||||
|
||||
impl RayMap {
|
||||
/// Iterates over all world space rays for every picking pointer.
|
||||
pub fn iter(&self) -> Iter<'_, RayId, Ray3d> {
|
||||
self.map.iter()
|
||||
}
|
||||
|
||||
/// The hash map of all rays cast in the current frame.
|
||||
pub fn map(&self) -> &HashMap<RayId, Ray3d> {
|
||||
&self.map
|
||||
}
|
||||
|
||||
/// Clears the [`RayMap`] and re-populates it with one ray for each
|
||||
/// combination of pointer entity and camera entity where the pointer
|
||||
/// intersects the camera's viewport.
|
||||
pub fn repopulate(
|
||||
mut ray_map: ResMut<Self>,
|
||||
primary_window_entity: Query<Entity, With<PrimaryWindow>>,
|
||||
cameras: Query<(Entity, &Camera, &GlobalTransform)>,
|
||||
pointers: Query<(&PointerId, &PointerLocation)>,
|
||||
) {
|
||||
ray_map.map.clear();
|
||||
|
||||
for (camera_entity, camera, camera_tfm) in &cameras {
|
||||
if !camera.is_active {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (&pointer_id, pointer_loc) in &pointers {
|
||||
if let Some(ray) =
|
||||
make_ray(&primary_window_entity, camera, camera_tfm, pointer_loc)
|
||||
{
|
||||
ray_map
|
||||
.map
|
||||
.insert(RayId::new(camera_entity, pointer_id), ray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_ray(
|
||||
primary_window_entity: &Query<Entity, With<PrimaryWindow>>,
|
||||
camera: &Camera,
|
||||
camera_tfm: &GlobalTransform,
|
||||
pointer_loc: &PointerLocation,
|
||||
) -> Option<Ray3d> {
|
||||
let pointer_loc = pointer_loc.location()?;
|
||||
if !pointer_loc.is_in_viewport(camera, primary_window_entity) {
|
||||
return None;
|
||||
}
|
||||
let mut viewport_pos = pointer_loc.position;
|
||||
if let Some(viewport) = &camera.viewport {
|
||||
let viewport_logical = camera.to_logical(viewport.physical_position)?;
|
||||
viewport_pos -= viewport_logical;
|
||||
}
|
||||
camera.viewport_to_world(camera_tfm, viewport_pos)
|
||||
}
|
||||
}
|
212
crates/bevy_picking/src/lib.rs
Normal file
212
crates/bevy_picking/src/lib.rs
Normal file
|
@ -0,0 +1,212 @@
|
|||
//! TODO, write module doc
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub mod backend;
|
||||
pub mod pointer;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::prelude::*;
|
||||
|
||||
/// Used to globally toggle picking features at runtime.
|
||||
#[derive(Clone, Debug, Resource, Reflect)]
|
||||
#[reflect(Resource, Default)]
|
||||
pub struct PickingPluginsSettings {
|
||||
/// Enables and disables all picking features.
|
||||
pub is_enabled: bool,
|
||||
/// Enables and disables input collection.
|
||||
pub is_input_enabled: bool,
|
||||
/// Enables and disables updating interaction states of entities.
|
||||
pub is_focus_enabled: bool,
|
||||
}
|
||||
|
||||
impl PickingPluginsSettings {
|
||||
/// Whether or not input collection systems should be running.
|
||||
pub fn input_should_run(state: Res<Self>) -> bool {
|
||||
state.is_input_enabled && state.is_enabled
|
||||
}
|
||||
/// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction)
|
||||
/// component should be running.
|
||||
pub fn focus_should_run(state: Res<Self>) -> bool {
|
||||
state.is_focus_enabled && state.is_enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PickingPluginsSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_enabled: true,
|
||||
is_input_enabled: true,
|
||||
is_focus_enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An optional component that overrides default picking behavior for an entity, allowing you to
|
||||
/// make an entity non-hoverable, or allow items below it to be hovered. See the documentation on
|
||||
/// the fields for more details.
|
||||
#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct Pickable {
|
||||
/// Should this entity block entities below it from being picked?
|
||||
///
|
||||
/// This is useful if you want picking to continue hitting entities below this one. Normally,
|
||||
/// only the topmost entity under a pointer can be hovered, but this setting allows the pointer
|
||||
/// to hover multiple entities, from nearest to farthest, stopping as soon as it hits an entity
|
||||
/// that blocks lower entities.
|
||||
///
|
||||
/// Note that the word "lower" here refers to entities that have been reported as hit by any
|
||||
/// picking backend, but are at a lower depth than the current one. This is different from the
|
||||
/// concept of event bubbling, as it works irrespective of the entity hierarchy.
|
||||
///
|
||||
/// For example, if a pointer is over a UI element, as well as a 3d mesh, backends will report
|
||||
/// hits for both of these entities. Additionally, the hits will be sorted by the camera order,
|
||||
/// so if the UI is drawing on top of the 3d mesh, the UI will be "above" the mesh. When focus
|
||||
/// is computed, the UI element will be checked first to see if it this field is set to block
|
||||
/// lower entities. If it does (default), the focus system will stop there, and only the UI
|
||||
/// element will be marked as hovered. However, if this field is set to `false`, both the UI
|
||||
/// element *and* the mesh will be marked as hovered.
|
||||
///
|
||||
/// Entities without the [`Pickable`] component will block by default.
|
||||
pub should_block_lower: bool,
|
||||
/// Should this entity be added to the [`HoverMap`](focus::HoverMap) and thus emit events when
|
||||
/// targeted?
|
||||
///
|
||||
/// If this is set to `false` and `should_block_lower` is set to true, this entity will block
|
||||
/// lower entities from being interacted and at the same time will itself not emit any events.
|
||||
///
|
||||
/// Note that the word "lower" here refers to entities that have been reported as hit by any
|
||||
/// picking backend, but are at a lower depth than the current one. This is different from the
|
||||
/// concept of event bubbling, as it works irrespective of the entity hierarchy.
|
||||
///
|
||||
/// For example, if a pointer is over a UI element, and this field is set to `false`, it will
|
||||
/// not be marked as hovered, and consequently will not emit events nor will any picking
|
||||
/// components mark it as hovered. This can be combined with the other field
|
||||
/// [`Self::should_block_lower`], which is orthogonal to this one.
|
||||
///
|
||||
/// Entities without the [`Pickable`] component are hoverable by default.
|
||||
pub is_hoverable: bool,
|
||||
}
|
||||
|
||||
impl Pickable {
|
||||
/// This entity will not block entities beneath it, nor will it emit events.
|
||||
///
|
||||
/// If a backend reports this entity as being hit, the picking plugin will completely ignore it.
|
||||
pub const IGNORE: Self = Self {
|
||||
should_block_lower: false,
|
||||
is_hoverable: false,
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for Pickable {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
should_block_lower: true,
|
||||
is_hoverable: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Components needed to build a pointer. Multiple pointers can be active at once, with each pointer
|
||||
/// being an entity.
|
||||
///
|
||||
/// `Mouse` and `Touch` pointers are automatically spawned as needed. Use this bundle if you are
|
||||
/// spawning a custom `PointerId::Custom` pointer, either for testing, as a software controlled
|
||||
/// pointer, or if you are replacing the default touch and mouse inputs.
|
||||
#[derive(Bundle)]
|
||||
pub struct PointerBundle {
|
||||
/// The pointer's unique [`PointerId`](pointer::PointerId).
|
||||
pub id: pointer::PointerId,
|
||||
/// Tracks the pointer's location.
|
||||
pub location: pointer::PointerLocation,
|
||||
/// Tracks the pointer's button press state.
|
||||
pub click: pointer::PointerPress,
|
||||
/// The interaction state of any hovered entities.
|
||||
pub interaction: pointer::PointerInteraction,
|
||||
}
|
||||
|
||||
impl PointerBundle {
|
||||
/// Create a new pointer with the provided [`PointerId`](pointer::PointerId).
|
||||
pub fn new(id: pointer::PointerId) -> Self {
|
||||
PointerBundle {
|
||||
id,
|
||||
location: pointer::PointerLocation::default(),
|
||||
click: pointer::PointerPress::default(),
|
||||
interaction: pointer::PointerInteraction::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the location of the pointer bundle
|
||||
pub fn with_location(mut self, location: pointer::Location) -> Self {
|
||||
self.location.location = Some(location);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Groups the stages of the picking process under shared labels.
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum PickSet {
|
||||
/// Produces pointer input events. In the [`First`] schedule.
|
||||
Input,
|
||||
/// Runs after input events are generated but before commands are flushed. In the [`First`]
|
||||
/// schedule.
|
||||
PostInput,
|
||||
/// Receives and processes pointer input events. In the [`PreUpdate`] schedule.
|
||||
ProcessInput,
|
||||
/// Reads inputs and produces [`backend::PointerHits`]s. In the [`PreUpdate`] schedule.
|
||||
Backend,
|
||||
/// Reads [`backend::PointerHits`]s, and updates focus, selection, and highlighting states. In
|
||||
/// the [`PreUpdate`] schedule.
|
||||
Focus,
|
||||
/// Runs after all the focus systems are done, before event listeners are triggered. In the
|
||||
/// [`PreUpdate`] schedule.
|
||||
PostFocus,
|
||||
/// Runs after all other picking sets. In the [`PreUpdate`] schedule.
|
||||
Last,
|
||||
}
|
||||
|
||||
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
|
||||
/// types used by other picking plugins.
|
||||
pub struct PickingPlugin;
|
||||
|
||||
impl Plugin for PickingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<PickingPluginsSettings>()
|
||||
.init_resource::<pointer::PointerMap>()
|
||||
.init_resource::<backend::ray::RayMap>()
|
||||
.add_event::<pointer::InputPress>()
|
||||
.add_event::<pointer::InputMove>()
|
||||
.add_event::<backend::PointerHits>()
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
pointer::update_pointer_map,
|
||||
pointer::InputMove::receive,
|
||||
pointer::InputPress::receive,
|
||||
backend::ray::RayMap::repopulate,
|
||||
)
|
||||
.in_set(PickSet::ProcessInput),
|
||||
)
|
||||
.configure_sets(First, (PickSet::Input, PickSet::PostInput).chain())
|
||||
.configure_sets(
|
||||
PreUpdate,
|
||||
(
|
||||
PickSet::ProcessInput.run_if(PickingPluginsSettings::input_should_run),
|
||||
PickSet::Backend,
|
||||
PickSet::Focus.run_if(PickingPluginsSettings::focus_should_run),
|
||||
PickSet::PostFocus,
|
||||
// Eventually events will need to be dispatched here
|
||||
PickSet::Last,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.register_type::<pointer::PointerId>()
|
||||
.register_type::<pointer::PointerLocation>()
|
||||
.register_type::<pointer::PointerPress>()
|
||||
.register_type::<pointer::PointerInteraction>()
|
||||
.register_type::<Pickable>()
|
||||
.register_type::<PickingPluginsSettings>()
|
||||
.register_type::<backend::ray::RayId>();
|
||||
}
|
||||
}
|
311
crates/bevy_picking/src/pointer.rs
Normal file
311
crates/bevy_picking/src/pointer.rs
Normal file
|
@ -0,0 +1,311 @@
|
|||
//! Types and systems for pointer inputs, such as position and buttons.
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{Rect, Vec2};
|
||||
use bevy_reflect::prelude::*;
|
||||
use bevy_render::camera::{Camera, NormalizedRenderTarget};
|
||||
use bevy_utils::HashMap;
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::backend::HitData;
|
||||
|
||||
/// Identifies a unique pointer entity. `Mouse` and `Touch` pointers are automatically spawned.
|
||||
///
|
||||
/// This component is needed because pointers can be spawned and despawned, but they need to have a
|
||||
/// stable ID that persists regardless of the Entity they are associated with.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Component, Reflect)]
|
||||
#[reflect(Component, Default)]
|
||||
pub enum PointerId {
|
||||
/// The mouse pointer.
|
||||
#[default]
|
||||
Mouse,
|
||||
/// A touch input, usually numbered by window touch events from `winit`.
|
||||
Touch(u64),
|
||||
/// A custom, uniquely identified pointer. Useful for mocking inputs or implementing a software
|
||||
/// controlled cursor.
|
||||
#[reflect(ignore)]
|
||||
Custom(Uuid),
|
||||
}
|
||||
|
||||
impl PointerId {
|
||||
/// Returns true if the pointer is a touch input.
|
||||
pub fn is_touch(&self) -> bool {
|
||||
matches!(self, PointerId::Touch(_))
|
||||
}
|
||||
/// Returns true if the pointer is the mouse.
|
||||
pub fn is_mouse(&self) -> bool {
|
||||
matches!(self, PointerId::Mouse)
|
||||
}
|
||||
/// Returns true if the pointer is a custom input.
|
||||
pub fn is_custom(&self) -> bool {
|
||||
matches!(self, PointerId::Custom(_))
|
||||
}
|
||||
/// Returns the touch id if the pointer is a touch input.
|
||||
pub fn get_touch_id(&self) -> Option<u64> {
|
||||
if let PointerId::Touch(id) = self {
|
||||
Some(*id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds a list of entities this pointer is currently interacting with, sorted from nearest to
|
||||
/// farthest.
|
||||
#[derive(Debug, Default, Clone, Component, Reflect)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct PointerInteraction {
|
||||
pub(crate) sorted_entities: Vec<(Entity, HitData)>,
|
||||
}
|
||||
|
||||
/// A resource that maps each [`PointerId`] to their [`Entity`] for easy lookups.
|
||||
#[derive(Debug, Clone, Default, Resource)]
|
||||
pub struct PointerMap {
|
||||
inner: HashMap<PointerId, Entity>,
|
||||
}
|
||||
|
||||
impl PointerMap {
|
||||
/// Get the [`Entity`] of the supplied [`PointerId`].
|
||||
pub fn get_entity(&self, pointer_id: PointerId) -> Option<Entity> {
|
||||
self.inner.get(&pointer_id).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the [`PointerMap`] resource with the current frame's data.
|
||||
pub fn update_pointer_map(pointers: Query<(Entity, &PointerId)>, mut map: ResMut<PointerMap>) {
|
||||
map.inner.clear();
|
||||
for (entity, id) in &pointers {
|
||||
map.inner.insert(*id, entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks the state of the pointer's buttons in response to [`InputPress`]s.
|
||||
#[derive(Debug, Default, Clone, Component, Reflect, PartialEq, Eq)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct PointerPress {
|
||||
primary: bool,
|
||||
secondary: bool,
|
||||
middle: bool,
|
||||
}
|
||||
|
||||
impl PointerPress {
|
||||
/// Returns true if the primary pointer button is pressed.
|
||||
#[inline]
|
||||
pub fn is_primary_pressed(&self) -> bool {
|
||||
self.primary
|
||||
}
|
||||
|
||||
/// Returns true if the secondary pointer button is pressed.
|
||||
#[inline]
|
||||
pub fn is_secondary_pressed(&self) -> bool {
|
||||
self.secondary
|
||||
}
|
||||
|
||||
/// Returns true if the middle (tertiary) pointer button is pressed.
|
||||
#[inline]
|
||||
pub fn is_middle_pressed(&self) -> bool {
|
||||
self.middle
|
||||
}
|
||||
|
||||
/// Returns true if any pointer button is pressed.
|
||||
#[inline]
|
||||
pub fn is_any_pressed(&self) -> bool {
|
||||
self.primary || self.middle || self.secondary
|
||||
}
|
||||
}
|
||||
|
||||
/// Pointer input event for button presses. Fires when a pointer button changes state.
|
||||
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
|
||||
pub struct InputPress {
|
||||
/// The [`PointerId`] of the pointer that pressed a button.
|
||||
pub pointer_id: PointerId,
|
||||
/// Direction of the button press.
|
||||
pub direction: PressDirection,
|
||||
/// Identifies the pointer button changing in this event.
|
||||
pub button: PointerButton,
|
||||
}
|
||||
|
||||
impl InputPress {
|
||||
/// Create a new pointer button down event.
|
||||
pub fn new_down(id: PointerId, button: PointerButton) -> InputPress {
|
||||
Self {
|
||||
pointer_id: id,
|
||||
direction: PressDirection::Down,
|
||||
button,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new pointer button up event.
|
||||
pub fn new_up(id: PointerId, button: PointerButton) -> InputPress {
|
||||
Self {
|
||||
pointer_id: id,
|
||||
direction: PressDirection::Up,
|
||||
button,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the `button` of this pointer was just pressed.
|
||||
#[inline]
|
||||
pub fn is_just_down(&self, button: PointerButton) -> bool {
|
||||
self.button == button && self.direction == PressDirection::Down
|
||||
}
|
||||
|
||||
/// Returns true if the `button` of this pointer was just released.
|
||||
#[inline]
|
||||
pub fn is_just_up(&self, button: PointerButton) -> bool {
|
||||
self.button == button && self.direction == PressDirection::Up
|
||||
}
|
||||
|
||||
/// Receives [`InputPress`] events and updates corresponding [`PointerPress`] components.
|
||||
pub fn receive(
|
||||
mut events: EventReader<InputPress>,
|
||||
mut pointers: Query<(&PointerId, &mut PointerPress)>,
|
||||
) {
|
||||
for input_press_event in events.read() {
|
||||
pointers.iter_mut().for_each(|(pointer_id, mut pointer)| {
|
||||
if *pointer_id == input_press_event.pointer_id {
|
||||
let is_down = input_press_event.direction == PressDirection::Down;
|
||||
match input_press_event.button {
|
||||
PointerButton::Primary => pointer.primary = is_down,
|
||||
PointerButton::Secondary => pointer.secondary = is_down,
|
||||
PointerButton::Middle => pointer.middle = is_down,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The stage of the pointer button press event
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)]
|
||||
pub enum PressDirection {
|
||||
/// The pointer button was just pressed
|
||||
Down,
|
||||
/// The pointer button was just released
|
||||
Up,
|
||||
}
|
||||
|
||||
/// The button that was just pressed or released
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)]
|
||||
pub enum PointerButton {
|
||||
/// The primary pointer button
|
||||
Primary,
|
||||
/// The secondary pointer button
|
||||
Secondary,
|
||||
/// The tertiary pointer button
|
||||
Middle,
|
||||
}
|
||||
|
||||
impl PointerButton {
|
||||
/// Iterator over all buttons that a pointer can have.
|
||||
pub fn iter() -> impl Iterator<Item = PointerButton> {
|
||||
[Self::Primary, Self::Secondary, Self::Middle].into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Component that tracks a pointer's current [`Location`].
|
||||
#[derive(Debug, Default, Clone, Component, Reflect, PartialEq)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct PointerLocation {
|
||||
/// The [`Location`] of the pointer. Note that a location is both the target, and the position
|
||||
/// on the target.
|
||||
#[reflect(ignore)]
|
||||
pub location: Option<Location>,
|
||||
}
|
||||
|
||||
impl PointerLocation {
|
||||
/// Returns `Some(&`[`Location`]`)` if the pointer is active, or `None` if the pointer is
|
||||
/// inactive.
|
||||
pub fn location(&self) -> Option<&Location> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Pointer input event for pointer moves. Fires when a pointer changes location.
|
||||
#[derive(Event, Debug, Clone, Reflect)]
|
||||
pub struct InputMove {
|
||||
/// The [`PointerId`] of the pointer that is moving.
|
||||
pub pointer_id: PointerId,
|
||||
/// The [`Location`] of the pointer.
|
||||
pub location: Location,
|
||||
/// The distance moved (change in `position`) since the last event.
|
||||
pub delta: Vec2,
|
||||
}
|
||||
|
||||
impl InputMove {
|
||||
/// Create a new [`InputMove`] event.
|
||||
pub fn new(id: PointerId, location: Location, delta: Vec2) -> InputMove {
|
||||
Self {
|
||||
pointer_id: id,
|
||||
location,
|
||||
delta,
|
||||
}
|
||||
}
|
||||
|
||||
/// Receives [`InputMove`] events and updates corresponding [`PointerLocation`] components.
|
||||
pub fn receive(
|
||||
mut events: EventReader<InputMove>,
|
||||
mut pointers: Query<(&PointerId, &mut PointerLocation)>,
|
||||
) {
|
||||
for event_pointer in events.read() {
|
||||
pointers.iter_mut().for_each(|(id, mut pointer)| {
|
||||
if *id == event_pointer.pointer_id {
|
||||
pointer.location = Some(event_pointer.location.to_owned());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The location of a pointer, including the current [`NormalizedRenderTarget`], and the x/y
|
||||
/// position of the pointer on this render target.
|
||||
///
|
||||
/// Note that:
|
||||
/// - a pointer can move freely between render targets
|
||||
/// - a pointer is not associated with a [`Camera`] because multiple cameras can target the same
|
||||
/// render target. It is up to picking backends to associate a Pointer's `Location` with a
|
||||
/// specific `Camera`, if any.
|
||||
#[derive(Debug, Clone, Component, Reflect, PartialEq)]
|
||||
pub struct Location {
|
||||
/// The [`NormalizedRenderTarget`] associated with the pointer, usually a window.
|
||||
pub target: NormalizedRenderTarget,
|
||||
/// The position of the pointer in the `target`.
|
||||
pub position: Vec2,
|
||||
}
|
||||
|
||||
impl Location {
|
||||
/// Returns `true` if this pointer's [`Location`] is within the [`Camera`]'s viewport.
|
||||
///
|
||||
/// Note this returns `false` if the location and camera have different render targets.
|
||||
#[inline]
|
||||
pub fn is_in_viewport(
|
||||
&self,
|
||||
camera: &Camera,
|
||||
primary_window: &Query<Entity, With<PrimaryWindow>>,
|
||||
) -> bool {
|
||||
if camera
|
||||
.target
|
||||
.normalize(Some(match primary_window.get_single() {
|
||||
Ok(w) => w,
|
||||
Err(_) => return false,
|
||||
}))
|
||||
.as_ref()
|
||||
!= Some(&self.target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let position = Vec2::new(self.position.x, self.position.y);
|
||||
|
||||
camera
|
||||
.logical_viewport_rect()
|
||||
.map(|Rect { min, max }| {
|
||||
(position - min).min_element() >= 0.0 && (position - max).max_element() <= 0.0
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ The default feature set enables most of the expected features of a game engine,
|
|||
|bevy_gizmos|Adds support for rendering gizmos|
|
||||
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|
||||
|bevy_pbr|Adds PBR rendering|
|
||||
|bevy_picking|Provides picking functionality|
|
||||
|bevy_render|Provides rendering functionality|
|
||||
|bevy_scene|Provides scene functionality|
|
||||
|bevy_sprite|Provides sprite functionality|
|
||||
|
|
|
@ -47,6 +47,7 @@ crates=(
|
|||
bevy_internal
|
||||
bevy_dylib
|
||||
bevy_color
|
||||
bevy_picking
|
||||
)
|
||||
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
|
|
Loading…
Reference in a new issue