diff --git a/Cargo.toml b/Cargo.toml index 27ebefbe940ba..53e5114acedb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 64264595a2e0e..36f3597fbf3c1 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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 diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index f7f828c986bc4..1f03cf09dc29d 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -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")] diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml new file mode 100644 index 0000000000000..3f5a6a7773423 --- /dev/null +++ b/crates/bevy_picking/Cargo.toml @@ -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 diff --git a/crates/bevy_picking/README.md b/crates/bevy_picking/README.md new file mode 100644 index 0000000000000..fc60deb46cecd --- /dev/null +++ b/crates/bevy_picking/README.md @@ -0,0 +1 @@ +# Bevy Picking diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs new file mode 100644 index 0000000000000..606459b18ee1d --- /dev/null +++ b/crates/bevy_picking/src/backend.rs @@ -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, + /// The normal vector of the hit test, if the data is available from the backend. + pub normal: Option, +} + +impl HitData { + #[allow(missing_docs)] + pub fn new(camera: Entity, depth: f32, position: Option, normal: Option) -> 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, mut output_events: EventWriter,) { + /// 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, + } + + 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 { + &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, + primary_window_entity: Query>, + 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>, + camera: &Camera, + camera_tfm: &GlobalTransform, + pointer_loc: &PointerLocation, + ) -> Option { + 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) + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs new file mode 100644 index 0000000000000..e19b9ce415dca --- /dev/null +++ b/crates/bevy_picking/src/lib.rs @@ -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) -> 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) -> 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::() + .init_resource::() + .init_resource::() + .add_event::() + .add_event::() + .add_event::() + .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::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::(); + } +} diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs new file mode 100644 index 0000000000000..3d1991c1ec246 --- /dev/null +++ b/crates/bevy_picking/src/pointer.rs @@ -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 { + 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, +} + +impl PointerMap { + /// Get the [`Entity`] of the supplied [`PointerId`]. + pub fn get_entity(&self, pointer_id: PointerId) -> Option { + 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) { + 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, + 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 { + [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, +} + +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, + 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>, + ) -> 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) + } +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8ccd806a6431d..8674294ec3efd 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -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| diff --git a/tools/publish.sh b/tools/publish.sh index 99dcf1e27522c..be02f607046d8 100644 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -47,6 +47,7 @@ crates=( bevy_internal bevy_dylib bevy_color + bevy_picking ) if [ -n "$(git status --porcelain)" ]; then