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:
Miles Silberling-Cook 2024-06-15 06:59:57 -05:00 committed by GitHub
parent eb3c81374a
commit aaccbe88aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 799 additions and 4 deletions

View file

@ -63,6 +63,7 @@ default = [
"bevy_winit", "bevy_winit",
"bevy_core_pipeline", "bevy_core_pipeline",
"bevy_pbr", "bevy_pbr",
"bevy_picking",
"bevy_gltf", "bevy_gltf",
"bevy_render", "bevy_render",
"bevy_sprite", "bevy_sprite",
@ -123,6 +124,9 @@ bevy_pbr = [
"bevy_core_pipeline", "bevy_core_pipeline",
] ]
# Provides picking functionality
bevy_picking = ["bevy_internal/bevy_picking"]
# Provides rendering functionality # Provides rendering functionality
bevy_render = ["bevy_internal/bevy_render", "bevy_color"] bevy_render = ["bevy_internal/bevy_render", "bevy_color"]

View file

@ -180,6 +180,9 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"]
# Provides a collection of developer tools # Provides a collection of developer tools
bevy_dev_tools = ["dep:bevy_dev_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 # Enable support for the ios_simulator by downgrading some rendering capabilities
ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] 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_audio = { path = "../bevy_audio", optional = true, version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", 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_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_gltf = { path = "../bevy_gltf", optional = true, version = "0.14.0-dev" }
bevy_pbr = { path = "../bevy_pbr", 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_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_scene = { path = "../bevy_scene", optional = true, version = "0.14.0-dev" }
bevy_sprite = { path = "../bevy_sprite", 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_text = { path = "../bevy_text", optional = true, version = "0.14.0-dev" }
bevy_ui = { path = "../bevy_ui", 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_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] [lints]
workspace = true workspace = true

View file

@ -44,6 +44,8 @@ pub use bevy_log as log;
pub use bevy_math as math; pub use bevy_math as math;
#[cfg(feature = "bevy_pbr")] #[cfg(feature = "bevy_pbr")]
pub use bevy_pbr as 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_ptr as ptr;
pub use bevy_reflect as reflect; pub use bevy_reflect as reflect;
#[cfg(feature = "bevy_render")] #[cfg(feature = "bevy_render")]

View 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

View file

@ -0,0 +1 @@
# Bevy Picking

View 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)
}
}

View 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>();
}
}

View 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)
}
}

View file

@ -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_gizmos|Adds support for rendering gizmos|
|bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|
|bevy_pbr|Adds PBR rendering| |bevy_pbr|Adds PBR rendering|
|bevy_picking|Provides picking functionality|
|bevy_render|Provides rendering functionality| |bevy_render|Provides rendering functionality|
|bevy_scene|Provides scene functionality| |bevy_scene|Provides scene functionality|
|bevy_sprite|Provides sprite functionality| |bevy_sprite|Provides sprite functionality|

View file

@ -47,6 +47,7 @@ crates=(
bevy_internal bevy_internal
bevy_dylib bevy_dylib
bevy_color bevy_color
bevy_picking
) )
if [ -n "$(git status --porcelain)" ]; then if [ -n "$(git status --porcelain)" ]; then