From 6adf31babf20b71ed1e61e8d5ea77d138758d194 Mon Sep 17 00:00:00 2001 From: TotalKrill Date: Thu, 15 Aug 2024 16:43:55 +0200 Subject: [PATCH] hooking up observers and clicking for ui node (#14695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the newly merged picking usable for UI elements. currently it both triggers the events, as well as sends them as throught commands.trigger_targets. We should probably figure out if this is needed for them all. # Objective Hooks up obserers and picking for a very simple example ## Solution upstreamed the UI picking backend from bevy_mod_picking ## Testing tested with the new example picking/simple_picking.rs --- --------- Co-authored-by: Lixou <82600264+DasLixou@users.noreply.github.com> Co-authored-by: Alice Cecile Co-authored-by: Kristoffer Søholm --- Cargo.toml | 12 ++ crates/bevy_internal/Cargo.toml | 2 +- crates/bevy_internal/src/default_plugins.rs | 2 + crates/bevy_internal/src/prelude.rs | 4 + crates/bevy_picking/Cargo.toml | 3 + crates/bevy_picking/src/events.rs | 131 ++++++------ crates/bevy_picking/src/lib.rs | 47 ++++- crates/bevy_ui/Cargo.toml | 2 + crates/bevy_ui/src/lib.rs | 6 + crates/bevy_ui/src/picking_backend.rs | 214 ++++++++++++++++++++ examples/README.md | 7 + examples/asset/multi_asset_sync.rs | 1 + examples/picking/simple_picking.rs | 90 ++++++++ 13 files changed, 455 insertions(+), 66 deletions(-) create mode 100644 crates/bevy_ui/src/picking_backend.rs create mode 100644 examples/picking/simple_picking.rs diff --git a/Cargo.toml b/Cargo.toml index f8cb386c32..2b09032015 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3352,6 +3352,18 @@ description = "Demonstrates how to rotate the skybox and the environment map sim category = "3D Rendering" wasm = false +[[example]] +name = "simple_picking" +path = "examples/picking/simple_picking.rs" +doc-scrape-examples = true +required-features = ["bevy_picking"] + +[package.metadata.example.simple_picking] +name = "Showcases simple picking events and usage" +description = "Demonstrates how to use picking events to spawn simple objects" +category = "Picking" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index b3679e0936..7505c8f0be 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -191,7 +191,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"] bevy_dev_tools = ["dep:bevy_dev_tools"] # Provides a picking functionality -bevy_picking = ["dep:bevy_picking"] +bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index c2dd44045c..a84bbdb573 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -56,6 +56,8 @@ plugin_group! { bevy_gizmos:::GizmoPlugin, #[cfg(feature = "bevy_state")] bevy_state::app:::StatesPlugin, + #[cfg(feature = "bevy_picking")] + bevy_picking:::DefaultPickingPlugins, #[cfg(feature = "bevy_dev_tools")] bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 7201e170d3..812a674ed9 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -66,3 +66,7 @@ pub use crate::state::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_gltf")] pub use crate::gltf::prelude::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_picking")] +pub use crate::picking::prelude::*; diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index 30601f4631..eadd869e2d 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -20,6 +20,9 @@ bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } + uuid = { version = "1.1", features = ["v4"] } [lints] diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index efb685d55a..092fa6ac26 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -205,6 +205,7 @@ pub struct Drop { /// Generates pointer events from input and focus data #[allow(clippy::too_many_arguments)] pub fn pointer_events( + mut commands: Commands, // Input mut input_presses: EventReader, mut input_moves: EventReader, @@ -237,12 +238,14 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { - pointer_move.send(Pointer::new( + let event = Pointer::new( pointer_id, location.clone(), hovered_entity, Move { hit, delta }, - )); + ); + commands.trigger_targets(event.clone(), event.target); + pointer_move.send(event); } } @@ -264,12 +267,14 @@ pub fn pointer_events( ); continue; }; - pointer_up.send(Pointer::new( + let event = Pointer::new( press_event.pointer_id, location, hovered_entity, Up { button, hit }, - )); + ); + commands.trigger_targets(event.clone(), event.target); + pointer_up.send(event); } } for (hovered_entity, hit) in hover_map @@ -285,12 +290,14 @@ pub fn pointer_events( ); continue; }; - pointer_down.send(Pointer::new( + let event = Pointer::new( press_event.pointer_id, location, hovered_entity, Down { button, hit }, - )); + ); + commands.trigger_targets(event.clone(), event.target); + pointer_down.send(event); } } } @@ -313,12 +320,9 @@ pub fn pointer_events( ); continue; }; - pointer_over.send(Pointer::new( - pointer_id, - location, - hovered_entity, - Over { hit }, - )); + let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit }); + commands.trigger_targets(event.clone(), event.target); + pointer_over.send(event); } } @@ -340,12 +344,9 @@ pub fn pointer_events( ); continue; }; - pointer_out.send(Pointer::new( - pointer_id, - location, - hovered_entity, - Out { hit }, - )); + let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit }); + commands.trigger_targets(event.clone(), event.target); + pointer_out.send(event); } } } @@ -366,6 +367,11 @@ pub struct DragEntry { /// Uses pointer events to determine when click and drag events occur. #[allow(clippy::too_many_arguments)] pub fn send_click_and_drag_events( + // for triggering observers + // - Pointer + // - Pointer + // - Pointer + mut commands: Commands, // Input mut pointer_down: EventReader>, mut pointer_up: EventReader>, @@ -377,12 +383,9 @@ pub fn send_click_and_drag_events( mut down_map: Local< HashMap<(PointerId, PointerButton), HashMap, Instant)>>, >, - // Output + // Outputs used for further processing mut drag_map: ResMut, - mut pointer_click: EventWriter>, - mut pointer_drag_start: EventWriter>, mut pointer_drag_end: EventWriter>, - mut pointer_drag: EventWriter>, ) { let pointer_location = |pointer_id: PointerId| { pointer_map @@ -415,7 +418,7 @@ pub fn send_click_and_drag_events( latest_pos: down.pointer_location.position, }, ); - pointer_drag_start.send(Pointer::new( + let event = Pointer::new( pointer_id, down.pointer_location.clone(), down.target, @@ -423,7 +426,8 @@ pub fn send_click_and_drag_events( button, hit: down.hit.clone(), }, - )); + ); + commands.trigger_targets(event, down.target); } for (dragged_entity, drag) in drag_list.iter_mut() { @@ -433,12 +437,9 @@ pub fn send_click_and_drag_events( delta: location.position - drag.latest_pos, }; drag.latest_pos = location.position; - pointer_drag.send(Pointer::new( - pointer_id, - location.clone(), - *dragged_entity, - drag_event, - )); + let target = *dragged_entity; + let event = Pointer::new(pointer_id, location.clone(), target, drag_event); + commands.trigger_targets(event, target); } } } @@ -458,7 +459,7 @@ pub fn send_click_and_drag_events( .and_then(|down| down.get(&target)) { let duration = now - *down_instant; - pointer_click.send(Pointer::new( + let event = Pointer::new( pointer_id, pointer_location, target, @@ -467,7 +468,8 @@ pub fn send_click_and_drag_events( hit, duration, }, - )); + ); + commands.trigger_targets(event, target); } } @@ -501,12 +503,9 @@ pub fn send_click_and_drag_events( button: press.button, distance: drag.latest_pos - drag.start_pos, }; - pointer_drag_end.send(Pointer::new( - press.pointer_id, - location.clone(), - drag_target, - drag_end, - )); + let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end); + commands.trigger_targets(event.clone(), event.target); + pointer_drag_end.send(event); } } } @@ -514,6 +513,12 @@ pub fn send_click_and_drag_events( /// Uses pointer events to determine when drag-over events occur #[allow(clippy::too_many_arguments)] pub fn send_drag_over_events( + // uses this to trigger the following + // - Pointer, + // - Pointer, + // - Pointer, + // - Pointer, + mut commands: Commands, // Input drag_map: Res, mut pointer_over: EventReader>, @@ -522,12 +527,6 @@ pub fn send_drag_over_events( mut pointer_drag_end: EventReader>, // Local mut drag_over_map: Local>>, - - // Output - mut pointer_drag_enter: EventWriter>, - mut pointer_drag_over: EventWriter>, - mut pointer_drag_leave: EventWriter>, - mut pointer_drop: EventWriter>, ) { // Fire PointerDragEnter events. for Pointer { @@ -548,17 +547,17 @@ pub fn send_drag_over_events( { let drag_entry = drag_over_map.entry((pointer_id, button)).or_default(); drag_entry.insert(target, hit.clone()); - let event = DragEnter { - button, - dragged: *drag_target, - hit: hit.clone(), - }; - pointer_drag_enter.send(Pointer::new( + let event = Pointer::new( pointer_id, pointer_location.clone(), target, - event, - )); + DragEnter { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); } } } @@ -580,7 +579,7 @@ pub fn send_drag_over_events( |&&drag_target| target != drag_target, /* can't drag over itself */ ) { - pointer_drag_over.send(Pointer::new( + let event = Pointer::new( pointer_id, pointer_location.clone(), target, @@ -589,7 +588,8 @@ pub fn send_drag_over_events( dragged: *drag_target, hit: hit.clone(), }, - )); + ); + commands.trigger_targets(event, target); } } } @@ -598,7 +598,7 @@ pub fn send_drag_over_events( for Pointer { pointer_id, pointer_location, - target, + target: drag_end_target, event: DragEnd { button, distance: _, @@ -609,26 +609,30 @@ pub fn send_drag_over_events( continue; }; for (dragged_over, hit) in drag_over_set.drain() { - pointer_drag_leave.send(Pointer::new( + let target = dragged_over; + let event = Pointer::new( pointer_id, pointer_location.clone(), dragged_over, DragLeave { button, - dragged: target, + dragged: drag_end_target, hit: hit.clone(), }, - )); - pointer_drop.send(Pointer::new( + ); + commands.trigger_targets(event, target); + + let event = Pointer::new( pointer_id, pointer_location.clone(), - dragged_over, + target, Drop { button, dropped: target, hit: hit.clone(), }, - )); + ); + commands.trigger_targets(event, target); } } @@ -651,7 +655,7 @@ pub fn send_drag_over_events( continue; }; for drag_target in drag_list.keys() { - pointer_drag_leave.send(Pointer::new( + let event = Pointer::new( pointer_id, pointer_location.clone(), target, @@ -660,7 +664,8 @@ pub fn send_drag_over_events( dragged: *drag_target, hit: hit.clone(), }, - )); + ); + commands.trigger_targets(event, target); } } } diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 522b1ec7d9..ba0ef8eee9 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -12,6 +12,15 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +/// common exports for picking interaction +pub mod prelude { + #[doc(hidden)] + pub use crate::{ + events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins, + InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings, + }; +} + /// Used to globally toggle picking features at runtime. #[derive(Clone, Debug, Resource, Reflect)] #[reflect(Resource, Default)] @@ -167,8 +176,27 @@ pub enum PickSet { Last, } +/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`], +/// this is probably the plugin that will be most used. +/// Note: for any of these plugins to work, they require a picking backend to be active, +/// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +#[derive(Default)] +pub struct DefaultPickingPlugins; + +impl Plugin for DefaultPickingPlugins { + fn build(&self, app: &mut App) { + app.add_plugins(( + input::InputPlugin::default(), + PickingPlugin, + InteractionPlugin, + )); + } +} + /// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared /// types used by other picking plugins. +#[derive(Default)] pub struct PickingPlugin; impl Plugin for PickingPlugin { @@ -185,11 +213,18 @@ impl Plugin for PickingPlugin { pointer::update_pointer_map, pointer::InputMove::receive, pointer::InputPress::receive, - backend::ray::RayMap::repopulate, + backend::ray::RayMap::repopulate.after(pointer::InputMove::receive), ) .in_set(PickSet::ProcessInput), ) - .configure_sets(First, (PickSet::Input, PickSet::PostInput).chain()) + .configure_sets( + First, + (PickSet::Input, PickSet::PostInput) + .after(bevy_time::TimeSystem) + .ambiguous_with(bevy_asset::handle_internal_asset_events) + .after(bevy_ecs::event::EventUpdates) + .chain(), + ) .configure_sets( PreUpdate, ( @@ -200,6 +235,7 @@ impl Plugin for PickingPlugin { // Eventually events will need to be dispatched here PickSet::Last, ) + .ambiguous_with(bevy_asset::handle_internal_asset_events) .chain(), ) .register_type::() @@ -213,6 +249,7 @@ impl Plugin for PickingPlugin { } /// Generates [`Pointer`](events::Pointer) events and handles event bubbling. +#[derive(Default)] pub struct InteractionPlugin; impl Plugin for InteractionPlugin { @@ -224,6 +261,12 @@ impl Plugin for InteractionPlugin { .init_resource::() .init_resource::() .add_event::() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() .add_systems( PreUpdate, ( diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 7c6c634026..de8909337e 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -26,6 +26,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" } bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true } +bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } @@ -40,6 +41,7 @@ smallvec = "1.11" [features] serialize = ["serde", "smallvec/serde", "bevy_math/serialize"] +bevy_picking = ["dep:bevy_picking"] [lints] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index aedb5e51e8..fcfbc4065f 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -17,6 +17,9 @@ pub mod ui_material; pub mod update; pub mod widget; +#[cfg(feature = "bevy_picking")] +pub mod picking_backend; + use bevy_derive::{Deref, DerefMut}; use bevy_reflect::Reflect; #[cfg(feature = "bevy_text")] @@ -202,6 +205,9 @@ impl Plugin for UiPlugin { build_text_interop(app); build_ui_render(app); + + #[cfg(feature = "bevy_picking")] + app.add_plugins(picking_backend::UiPickingBackend); } fn finish(&self, app: &mut App) { diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs new file mode 100644 index 0000000000..5cb31e2098 --- /dev/null +++ b/crates/bevy_ui/src/picking_backend.rs @@ -0,0 +1,214 @@ +//! A picking backend for UI nodes. +//! +//! # Usage +//! +//! This backend does not require markers on cameras or entities to function. It will look for any +//! pointers using the same render target as the UI camera, and run hit tests on the UI node tree. +//! +//! ## Important Note +//! +//! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s +//! focus systems and the picking plugin are not compatible. Instead, use the [`Pickable`] component +//! to customize how an entity responds to picking focus. Nodes without the [`Pickable`] component +//! will not trigger events. +//! +//! ## Implementation Notes +//! +//! - `bevy_ui` can only render to the primary window +//! - `bevy_ui` can render on any camera with a flag, it is special, and is not tied to a particular +//! camera. +//! - To correctly sort picks, the order of `bevy_ui` is set to be the camera order plus 0.5. + +#![allow(clippy::type_complexity)] +#![allow(clippy::too_many_arguments)] +#![deny(missing_docs)] + +use crate::{prelude::*, UiStack}; +use bevy_app::prelude::*; +use bevy_ecs::{prelude::*, query::QueryData}; +use bevy_math::Vec2; +use bevy_render::prelude::*; +use bevy_transform::prelude::*; +use bevy_utils::hashbrown::HashMap; +use bevy_window::PrimaryWindow; + +use bevy_picking::backend::prelude::*; + +/// A plugin that adds picking support for UI nodes. +#[derive(Clone)] +pub struct UiPickingBackend; +impl Plugin for UiPickingBackend { + fn build(&self, app: &mut App) { + app.add_systems(PreUpdate, ui_picking.in_set(PickSet::Backend)); + } +} + +/// Main query from bevy's `ui_focus_system` +#[derive(QueryData)] +#[query_data(mutable)] +pub struct NodeQuery { + entity: Entity, + node: &'static Node, + global_transform: &'static GlobalTransform, + pickable: Option<&'static Pickable>, + calculated_clip: Option<&'static CalculatedClip>, + view_visibility: Option<&'static ViewVisibility>, + target_camera: Option<&'static TargetCamera>, +} + +/// Computes the UI node entities under each pointer. +/// +/// Bevy's [`UiStack`] orders all nodes in the order they will be rendered, which is the same order +/// we need for determining picking. +pub fn ui_picking( + pointers: Query<(&PointerId, &PointerLocation)>, + camera_query: Query<(Entity, &Camera, Has)>, + default_ui_camera: DefaultUiCamera, + primary_window: Query>, + ui_scale: Res, + ui_stack: Res, + mut node_query: Query, + mut output: EventWriter, +) { + // For each camera, the pointer and its position + let mut pointer_pos_by_camera = HashMap::>::new(); + + for (pointer_id, pointer_location) in + pointers.iter().filter_map(|(pointer, pointer_location)| { + Some(*pointer).zip(pointer_location.location().cloned()) + }) + { + // This pointer is associated with a render target, which could be used by multiple + // cameras. We want to ensure we return all cameras with a matching target. + for camera in camera_query + .iter() + .map(|(entity, camera, _)| { + ( + entity, + camera.target.normalize(primary_window.get_single().ok()), + ) + }) + .filter_map(|(entity, target)| Some(entity).zip(target)) + .filter(|(_entity, target)| target == &pointer_location.target) + .map(|(cam_entity, _target)| cam_entity) + { + let Ok((_, camera_data, _)) = camera_query.get(camera) else { + continue; + }; + let mut pointer_pos = pointer_location.position; + if let Some(viewport) = camera_data.logical_viewport_rect() { + pointer_pos -= viewport.min; + } + let scaled_pointer_pos = pointer_pos / **ui_scale; + pointer_pos_by_camera + .entry(camera) + .or_default() + .insert(pointer_id, scaled_pointer_pos); + } + } + + // The list of node entities hovered for each (camera, pointer) combo + let mut hit_nodes = HashMap::<(Entity, PointerId), Vec>::new(); + + // prepare an iterator that contains all the nodes that have the cursor in their rect, + // from the top node to the bottom one. this will also reset the interaction to `None` + // for all nodes encountered that are no longer hovered. + for node_entity in ui_stack + .uinodes + .iter() + // reverse the iterator to traverse the tree from closest nodes to furthest + .rev() + { + let Ok(node) = node_query.get_mut(*node_entity) else { + continue; + }; + + // Nodes that are not rendered should not be interactable + if node + .view_visibility + .map(|view_visibility| view_visibility.get()) + != Some(true) + { + continue; + } + let Some(camera_entity) = node + .target_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) + else { + continue; + }; + + let node_rect = node.node.logical_rect(node.global_transform); + + // Nodes with Display::None have a (0., 0.) logical rect and can be ignored + if node_rect.size() == Vec2::ZERO { + continue; + } + + // Intersect with the calculated clip rect to find the bounds of the visible region of the node + let visible_rect = node + .calculated_clip + .map(|clip| node_rect.intersect(clip.clip)) + .unwrap_or(node_rect); + + let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); + + // The mouse position relative to the node + // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Coordinates are relative to the entire node, not just the visible region. + for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { + let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); + + if visible_rect + .normalize(node_rect) + .contains(relative_cursor_position) + { + hit_nodes + .entry((camera_entity, *pointer_id)) + .or_default() + .push(*node_entity); + } + } + } + + for ((camera, pointer), hovered_nodes) in hit_nodes.iter() { + // As soon as a node with a `Block` focus policy is detected, the iteration will stop on it + // because it "captures" the interaction. + let mut iter = node_query.iter_many_mut(hovered_nodes.iter()); + let mut picks = Vec::new(); + let mut depth = 0.0; + + while let Some(node) = iter.fetch_next() { + let Some(camera_entity) = node + .target_camera + .map(TargetCamera::entity) + .or(default_ui_camera.get()) + else { + continue; + }; + + picks.push((node.entity, HitData::new(camera_entity, depth, None, None))); + + if let Some(pickable) = node.pickable { + // If an entity has a `Pickable` component, we will use that as the source of truth. + if pickable.should_block_lower { + break; + } + } else { + // If the Pickable component doesn't exist, default behavior is to block. + break; + } + + depth += 0.00001; // keep depth near 0 for precision + } + + let order = camera_query + .get(*camera) + .map(|(_, cam, _)| cam.order) + .unwrap_or_default() as f32 + + 0.5; // bevy ui can run on any camera, it's a special case + + output.send(PointerHits::new(*pointer, picks, order)); + } +} diff --git a/examples/README.md b/examples/README.md index 52f7b1d5ae..85b56b4d8e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -54,6 +54,7 @@ git checkout v0.4.0 - [Input](#input) - [Math](#math) - [Movement](#movement) + - [Picking](#picking) - [Reflection](#reflection) - [Scene](#scene) - [Shaders](#shaders) @@ -350,6 +351,12 @@ Example | Description --- | --- [Run physics in a fixed timestep](../examples/movement/physics_in_fixed_timestep.rs) | Handles input, physics, and rendering in an industry-standard way by using a fixed timestep +## Picking + +Example | Description +--- | --- +[Showcases simple picking events and usage](../examples/picking/simple_picking.rs) | Demonstrates how to use picking events to spawn simple objects + ## Reflection Example | Description diff --git a/examples/asset/multi_asset_sync.rs b/examples/asset/multi_asset_sync.rs index db2306ae75..2b2386dad3 100644 --- a/examples/asset/multi_asset_sync.rs +++ b/examples/asset/multi_asset_sync.rs @@ -2,6 +2,7 @@ use std::{ f32::consts::PI, + ops::Drop, sync::{ atomic::{AtomicBool, AtomicU32, Ordering}, Arc, diff --git a/examples/picking/simple_picking.rs b/examples/picking/simple_picking.rs new file mode 100644 index 0000000000..d417e42558 --- /dev/null +++ b/examples/picking/simple_picking.rs @@ -0,0 +1,90 @@ +//! A simple scene to demonstrate picking events + +use bevy::{color::palettes::css::*, prelude::*}; + +fn main() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins); + + app.add_systems(Startup, setup); + + app.run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands + .spawn(( + TextBundle { + text: Text::from_section("Click Me to get a box", TextStyle::default()), + style: Style { + position_type: PositionType::Absolute, + top: Val::Percent(10.0), + left: Val::Percent(10.0), + ..default() + }, + ..Default::default() + }, + Pickable::default(), + )) + .observe( + |_click: Trigger>, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut num: Local| { + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::srgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5 + 1.1 * *num as f32, 0.0), + ..default() + }); + *num += 1; + }, + ) + .observe(|evt: Trigger>, mut texts: Query<&mut Text>| { + let mut text = texts.get_mut(evt.entity()).unwrap(); + let first = text.sections.first_mut().unwrap(); + first.style.color = WHITE.into(); + }) + .observe(|evt: Trigger>, mut texts: Query<&mut Text>| { + let mut text = texts.get_mut(evt.entity()).unwrap(); + let first = text.sections.first_mut().unwrap(); + first.style.color = BLUE.into(); + }); + // circular base + commands + .spawn(( + PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x( + -std::f32::consts::FRAC_PI_2, + )), + ..default() + }, + Pickable::default(), + )) + .observe(|click: Trigger>| { + let click = click.event(); + println!("{click:?}"); + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + // camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +}