diff --git a/Cargo.toml b/Cargo.toml index 4bb5ace1b9729b..28595d915ee56d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2914,6 +2914,17 @@ description = "Demonstrates how to create a node with a border" category = "UI (User Interface)" wasm = true +[[example]] +name = "responding_to_changes" +path = "examples/ecs/responding_to_changes.rs" +doc-scrape-examples = true + +[package.metadata.example.reactivity] +name = "Responding to Changes" +description = "Demonstrates how and when to use change detection and `OnMutate` hooks and observers" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "box_shadow" path = "examples/ui/box_shadow.rs" diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 901ce891a4a622..93cbd089483f8f 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -37,6 +37,7 @@ type ExtractFn = Box; /// /// // Create a sub-app with the same resource and a single schedule. /// let mut sub_app = SubApp::new(); +/// sub_app.update_schedule = Some(Main.intern()); /// sub_app.insert_resource(Val(100)); /// /// // Setup an extract function to copy the resource's value in the main world. diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 9d97f185dede96..3150fdccc036b8 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -151,6 +151,7 @@ pub(crate) fn world_query_impl( } const IS_DENSE: bool = true #(&& <#field_types>::IS_DENSE)*; + const IS_MUTATE: bool = false #(|| <#field_types>::IS_MUTATE)*; /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 6f90e200979576..bba0fa1d2afcb8 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -99,7 +99,7 @@ impl core::ops::Deref for Pointer { impl Pointer { /// Construct a new `Pointer` event. - pub fn new(target: Entity, id: PointerId, location: Location, event: E) -> Self { + pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { Self { target, pointer_id: id, @@ -361,9 +361,9 @@ pub fn pointer_events( { state.dragging_over.insert(hovered_entity, hit.clone()); let drag_enter_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, DragEnter { button, dragged: *drag_target, @@ -377,9 +377,9 @@ pub fn pointer_events( // Always send Over events let over_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Over { hit: hit.clone() }, ); commands.trigger_targets(over_event.clone(), hovered_entity); @@ -409,9 +409,9 @@ pub fn pointer_events( .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) { let down_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Down { button, hit: hit.clone(), @@ -436,9 +436,9 @@ pub fn pointer_events( if let Some((_, press_instant, _)) = state.pressing.get(&hovered_entity) { let click_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Click { button, hit: hit.clone(), @@ -450,9 +450,9 @@ pub fn pointer_events( } // Always send the Up event let up_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Up { button, hit: hit.clone(), @@ -467,9 +467,9 @@ pub fn pointer_events( // Emit DragDrop for (dragged_over, hit) in state.dragging_over.iter() { let drag_drop_event = Pointer::new( - *dragged_over, pointer_id, location.clone(), + *dragged_over, DragDrop { button, dropped: drag_target, @@ -481,9 +481,9 @@ pub fn pointer_events( } // Emit DragEnd let drag_end_event = Pointer::new( - drag_target, pointer_id, location.clone(), + drag_target, DragEnd { button, distance: drag.latest_pos - drag.start_pos, @@ -494,9 +494,9 @@ pub fn pointer_events( // Emit DragLeave for (dragged_over, hit) in state.dragging_over.iter() { let drag_leave_event = Pointer::new( - *dragged_over, pointer_id, location.clone(), + *dragged_over, DragLeave { button, dragged: drag_target, @@ -534,9 +534,9 @@ pub fn pointer_events( }, ); let drag_start_event = Pointer::new( - *press_target, pointer_id, location.clone(), + *press_target, DragStart { button, hit: hit.clone(), @@ -549,9 +549,9 @@ pub fn pointer_events( // Emit Drag events to the entities we are dragging for (drag_target, drag) in state.dragging.iter_mut() { let drag_event = Pointer::new( - *drag_target, pointer_id, location.clone(), + *drag_target, Drag { button, distance: location.position - drag.start_pos, @@ -572,9 +572,9 @@ pub fn pointer_events( .filter(|(hovered_entity, _)| *hovered_entity != *drag_target) { let drag_over_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, DragOver { button, dragged: *drag_target, @@ -594,9 +594,9 @@ pub fn pointer_events( { // Emit Move events to the entities we are hovering let move_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Move { hit: hit.clone(), delta, @@ -615,7 +615,7 @@ pub fn pointer_events( .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { let cancel_event = - Pointer::new(hovered_entity, pointer_id, location.clone(), Cancel { hit }); + Pointer::new(pointer_id, location.clone(), hovered_entity, Cancel { hit }); commands.trigger_targets(cancel_event.clone(), hovered_entity); event_writers.cancel_events.send(cancel_event); } @@ -652,9 +652,9 @@ pub fn pointer_events( // Always send Out events let out_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, Out { hit: hit.clone() }, ); commands.trigger_targets(out_event.clone(), hovered_entity); @@ -666,9 +666,9 @@ pub fn pointer_events( state.dragging_over.remove(&hovered_entity); for drag_target in state.dragging.keys() { let drag_leave_event = Pointer::new( - hovered_entity, pointer_id, location.clone(), + hovered_entity, DragLeave { button, dragged: *drag_target, diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index eefe7753ffd198..45bb5b79de77a6 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -294,6 +294,7 @@ mod render_entities_world_query_impls { } const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; + const IS_MUTATE: bool = <&'static RenderEntity as WorldQuery>::IS_MUTATE; #[inline] unsafe fn set_archetype<'w>( @@ -393,6 +394,7 @@ mod render_entities_world_query_impls { } const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; + const IS_MUTATE: bool = <&'static MainEntity as WorldQuery>::IS_MUTATE; #[inline] unsafe fn set_archetype<'w>( diff --git a/crates/bevy_ui/src/render/box_shadow.wgsl b/crates/bevy_ui/src/render/box_shadow.wgsl index fc78f9490c3745..91d8e026eb0682 100644 --- a/crates/bevy_ui/src/render/box_shadow.wgsl +++ b/crates/bevy_ui/src/render/box_shadow.wgsl @@ -5,7 +5,6 @@ const PI: f32 = 3.14159265358979323846; const SAMPLES: i32 = #SHADOW_SAMPLES; @group(0) @binding(0) var view: View; -@group(0) @binding(1) var globals: Globals; struct BoxShadowVertexOutput { @builtin(position) position: vec4, diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 7adc47157c09f1..db17bbecc0e15a 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -3,7 +3,7 @@ use std::{f32::consts::PI, time::Duration}; use bevy::{ - animation::{animate_targets, AnimationTargetId, RepeatAnimation}, + animation::{AnimationTargetId, RepeatAnimation}, color::palettes::css::WHITE, pbr::CascadeShadowConfigBuilder, prelude::*, @@ -22,7 +22,7 @@ fn main() { .init_resource::() .init_resource::() .add_systems(Startup, setup) - .add_systems(Update, setup_scene_once_loaded.before(animate_targets)) + .add_systems(Update, setup_scene_once_loaded) .add_systems(Update, (keyboard_animation_control, simulate_particles)) .add_observer(observe_on_step) .run(); diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 80c7bf6e4dc88b..8238bfd116c5c2 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -4,7 +4,6 @@ //! playing animations by clicking and dragging left or right within the nodes. use bevy::{ - animation::animate_targets, color::palettes::{ basic::WHITE, css::{ANTIQUE_WHITE, DARK_GREEN}, @@ -83,7 +82,7 @@ fn main() { ..default() })) .add_systems(Startup, (setup_assets, setup_scene, setup_ui)) - .add_systems(Update, init_animations.before(animate_targets)) + .add_systems(Update, init_animations) .add_systems( Update, (handle_weight_drag, update_ui, sync_weights).chain(), diff --git a/examples/ecs/responding_to_changes.rs b/examples/ecs/responding_to_changes.rs new file mode 100644 index 00000000000000..be37fdb9226005 --- /dev/null +++ b/examples/ecs/responding_to_changes.rs @@ -0,0 +1,286 @@ + +//! Bevy has two primary ways to respond to changes in your ECS data: +//! +//! 1. **Change detection:** whenever a component or resource is mutated, it will be flagged as changed. +//! 2. **Hooks and observers:** whenever changes or lifecycle events occur, functions will be called to respond to them. +//! +//! While similar, these two methods have different use cases and performance characteristics. +//! Change detection is fundamentally a polling-based mechanism: changes need to be looked for proactively, +//! and so the cost of change detection is paid every time the system runs (generally every frame), +//! regardless of whether or not any changes have occurred. +//! +//! By contrast, hooks and observers are event-driven: they only run when the event they're watching for occurs. +//! However, each event is processed individually, increasing the overhead when many changes occur. +//! +//! As a result, change detection is better suited to use cases where large volumes of data are being processed, +//! while hooks and observers are better suited to use cases where the data is relatively stable and changes are infrequent. +//! +//! There are two more important differences. Firstly, change detection is triggered immediately when the change occurs, +//! while hooks and observers are deferred until the next synchronization point where exclusive world access is available. +//! In Bevy, systems are run in parallel by default, so synchronizing forces the scheduler to wait until +//! all systems have finished running before proceeding and prevent systems before and after the sync point from running concurrently. +//! +//! Second, while change detection systems only run periodically, +//! hooks and observers are checked after every mutation to the world during sync points. +//! +//! Taken together, this means that change detection is good for periodic updates (but it's harder to avoid invalid state), +//! while hooks and observers are good for immediate updates and can chain into other hooks/observers indefinitely, +//! creating a cascade of reactions (but they need to wait until a sync point). +//! +//! You might use change detection for: +//! +//! - physics simulation +//! - AI action planning +//! - performance optimization in existing systems +//! +//! You might use hooks and observers for: +//! +//! - complex logic in turn-based games +//! - responding to user inputs +//! - adding behavior to UI elements +//! - upholding critical invariants, like hierarchical relationships (hooks are better suited than observers for this) +//! +//! # This example +//! +//! In this example, we're demonstrating the APIs available by creating a simple counter +//! in four different ways: +//! +//! 1. Using a system with a `Changed` filter. +//! 2. Use the `Ref` query type and the `is_changed` method. +//! 3. Using a hook. +//! 4. Using an observer. +//! +//! The counter is incremented by pressing the corresponding button. +//! At this scale, we have neither performance nor complexity concerns: +//! see the discussion above for guidance on when to use each method. +//! +//! Hooks are not suitable for this application (as they represent intrinsic functionality for the type), +//! and cannot sensibly be added to the general-purpose [`Interaction`] component just to make these buttons work. +//! Instead, we demonstrate how to use them by adding a on-mutate hook to the [`CounterValue`] component which will +//! update the text of each button whenever the counter is incremented. + +use bevy::ecs::world::OnMutate; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // Example setup + .add_systems(Startup, setup_ui) + .add_systems( + Update, + (change_button_color_based_on_interaction, update_button_text) + .in_set(ChangeDetectionSet), + ) + // Change detection based methods + .add_systems( + Update, + ( + update_counter_changed_filter.after(ChangeDetectionSet), + update_counter_ref_query, + ), + ) + .add_observer(update_counter_observer) + .run(); +} + +#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)] +struct ChangeDetectionSet; + +/// Tracks the value of the counter for each button. +#[derive(Component)] +struct CounterValue(u32); + +/// A component that differentiates our buttons by their change-response strategy. +#[derive(Component, PartialEq)] +enum ChangeStrategy { + ChangedFilter, + RefQuery, + Observer, +} + +impl ChangeStrategy { + fn color(&self) -> Srgba { + use bevy::color::palettes::tailwind::*; + + match self { + ChangeStrategy::ChangedFilter => RED_500, + ChangeStrategy::RefQuery => ORANGE_500, + ChangeStrategy::Observer => BLUE_500, + } + } + + fn button_string(&self) -> &'static str { + match self { + ChangeStrategy::ChangedFilter => "Changed Filter", + ChangeStrategy::RefQuery => "Ref Query", + ChangeStrategy::Observer => "Observer", + } + } +} + +/// Generates an interactive button with a counter, +/// returning the entity ID of the button spawned. +fn spawn_button_with_counter(commands: &mut Commands, change_strategy: ChangeStrategy) -> Entity { + commands + .spawn(( + Node { + width: Val::Px(250.), + height: Val::Px(120.), + margin: UiRect::all(Val::Px(20.)), + ..default() + }, + Button::default(), + BorderRadius::all(Val::Px(20.)), + BackgroundColor(change_strategy.color().into()), + change_strategy, + CounterValue(0), + )) + .with_children(|parent| { + // We don't need to set the initial value of the Text component here, + // as Changed filters are triggered whenever the value is mutated OR the component is added. + parent.spawn(( + Node { + align_self: AlignSelf::Center, + width: Val::Percent(100.), + ..default() + }, + Text::default(), + )); + }) + .id() +} + +// This system implicitly filters out any entities whose `Interaction` component hasn't changed. +fn update_counter_changed_filter( + mut query: Query<(&Interaction, &ChangeStrategy, &mut CounterValue), Changed>, +) { + for (interaction, change_strategy, mut counter) in query.iter_mut() { + if change_strategy != &ChangeStrategy::ChangedFilter { + continue; + } + + if *interaction == Interaction::Pressed { + counter.0 += 1; + } + } +} + +// This system works just like the one above, except entries that are not changed will be included. +// We can check if the entity has changed by calling the `is_changed` method on the `Ref` type. +// The [`Mut`] and [`ChangeTrackers`] types also have these methods. +fn update_counter_ref_query( + mut query: Query<(Ref, &ChangeStrategy, &mut CounterValue)>, +) { + for (interaction, change_strategy, mut counter) in query.iter_mut() { + println!("ddd"); + if change_strategy != &ChangeStrategy::RefQuery { + continue; + } + + // Being able to check if the entity has changed inside of the system is + // sometimes useful for more complex logic, but Changed filters are generally clearer. + if interaction.is_changed() && *interaction == Interaction::Pressed { + counter.0 += 1; + } + } +} + +// This observer is added to the app using the `observe` method, +// and will run whenever the `Interaction` component is mutated. +// Like above, we're returning early if the button isn't the one we're interested in. +// FIXME: this isn't currently working +fn update_counter_observer( + trigger: Trigger, + mut button_query: Query<(&mut CounterValue, &Interaction, &ChangeStrategy)>, +) { + let Ok(( + mut counter, + interaction, + change_strategy + )) = button_query.get_mut(trigger.entity()) else { + // Other entities may have the Interaction component, but we're only interested in these particular buttons. + return; + }; + + if *change_strategy != ChangeStrategy::Observer { + return; + } + + // OnMutate events will be generated whenever *any* change occurs, + // even if it's not to the value we're interested in. + if *interaction == Interaction::Pressed { + counter.0 += 1; + } +} + +fn setup_ui(mut commands: Commands) { + commands.spawn(Camera2d::default()); + + let root_node = commands + .spawn(( + Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + )) + .id(); + + let changed_filter_button = + spawn_button_with_counter(&mut commands, ChangeStrategy::ChangedFilter); + let ref_query_button = spawn_button_with_counter(&mut commands, ChangeStrategy::RefQuery); + let observer_button = spawn_button_with_counter(&mut commands, ChangeStrategy::Observer); + + commands.entity(root_node).add_children(&[ + changed_filter_button, + ref_query_button, + observer_button, + ]); +} + +// This is another example of a change-detection based system, +// which only acts on buttons whose `Interaction` component has changed to save on work. +// +// Because the operation is idempotent (calling it multiple times has the same effect as calling it once), +// this is purely a performance optimization. +fn change_button_color_based_on_interaction( + mut query: Query<(&mut BackgroundColor, &ChangeStrategy, &Interaction), Changed>, +) { + for (mut background_color, change_strategy, interaction) in query.iter_mut() { + let standard_color = change_strategy.color(); + + *background_color = match interaction { + Interaction::None => standard_color.into(), + Interaction::Hovered => standard_color.darker(0.15).into(), + Interaction::Pressed => standard_color.darker(0.3).into(), + }; + } +} + +// TODO: implement this using hooks +// Like other filters, `Changed` (and `Added`) filters can be composed via `Or` filters. +// The default behavior for both query data and filters is to use AND logic. +// In this case, a truly robust solution should update whenever the counter value +// or the children that point to the text entity change. +fn update_button_text( + counter_query: Query< + (&CounterValue, &ChangeStrategy, &Children), + Or<(Changed, Changed)>, + >, + mut text_query: Query<&mut Text>, +) { + for (counter, change_strategy, children) in counter_query.iter() { + for child in children.iter() { + // By attempting to fetch the Text component on each child and continuing if it fails, + // we can avoid panicking if non-text children are present. + if let Ok(mut text) = text_query.get_mut(*child) { + let string = format!("{}: {}", change_strategy.button_string(), counter.0); + *text = Text::new(string); + } + } + } +} diff --git a/examples/picking/mesh_picking.rs b/examples/picking/mesh_picking.rs index 5193c4ea8faeed..40ba4806b30fc8 100644 --- a/examples/picking/mesh_picking.rs +++ b/examples/picking/mesh_picking.rs @@ -21,38 +21,17 @@ use std::f32::consts::PI; -use bevy::{ - color::palettes::{ - css::{PINK, RED, SILVER}, - tailwind::{CYAN_300, YELLOW_300}, - }, - picking::backend::PointerHits, - prelude::*, -}; +use bevy::{color::palettes::tailwind::*, picking::pointer::PointerInteraction, prelude::*}; fn main() { App::new() - .add_plugins(( - DefaultPlugins, - // The mesh picking plugin is not enabled by default, because raycasting against all - // meshes has a performance cost. - MeshPickingPlugin, - )) - .init_resource::() - .add_systems(Startup, setup) - .add_systems(Update, (on_mesh_hover, rotate)) + // MeshPickingPlugin is not a default plugin + .add_plugins((DefaultPlugins, MeshPickingPlugin)) + .add_systems(Startup, setup_scene) + .add_systems(Update, (draw_mesh_intersections, rotate)) .run(); } -/// Materials for the scene -#[derive(Resource, Default)] -struct SceneMaterials { - pub white: Handle, - pub ground: Handle, - pub hover: Handle, - pub pressed: Handle, -} - /// A marker component for our shapes so we can query them separately from the ground plane. #[derive(Component)] struct Shape; @@ -61,17 +40,16 @@ const SHAPES_X_EXTENT: f32 = 14.0; const EXTRUSION_X_EXTENT: f32 = 16.0; const Z_EXTENT: f32 = 5.0; -fn setup( +fn setup_scene( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, - mut scene_materials: ResMut, ) { // Set up the materials. - scene_materials.white = materials.add(Color::WHITE); - scene_materials.ground = materials.add(Color::from(SILVER)); - scene_materials.hover = materials.add(Color::from(CYAN_300)); - scene_materials.pressed = materials.add(Color::from(YELLOW_300)); + let white_matl = materials.add(Color::WHITE); + let ground_matl = materials.add(Color::from(GRAY_300)); + let hover_matl = materials.add(Color::from(CYAN_300)); + let pressed_matl = materials.add(Color::from(YELLOW_300)); let shapes = [ meshes.add(Cuboid::default()), @@ -102,7 +80,7 @@ fn setup( commands .spawn(( Mesh3d(shape), - MeshMaterial3d(scene_materials.white.clone()), + MeshMaterial3d(white_matl.clone()), Transform::from_xyz( -SHAPES_X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * SHAPES_X_EXTENT, 2.0, @@ -111,10 +89,11 @@ fn setup( .with_rotation(Quat::from_rotation_x(-PI / 4.)), Shape, )) - .observe(on_pointer_over) - .observe(on_pointer_out) - .observe(on_pointer_down) - .observe(on_pointer_up); + .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(white_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) + .observe(rotate_on_drag); } let num_extrusions = extrusions.len(); @@ -123,7 +102,7 @@ fn setup( commands .spawn(( Mesh3d(shape), - MeshMaterial3d(scene_materials.white.clone()), + MeshMaterial3d(white_matl.clone()), Transform::from_xyz( -EXTRUSION_X_EXTENT / 2. + i as f32 / (num_extrusions - 1) as f32 * EXTRUSION_X_EXTENT, @@ -133,17 +112,18 @@ fn setup( .with_rotation(Quat::from_rotation_x(-PI / 4.)), Shape, )) - .observe(on_pointer_over) - .observe(on_pointer_out) - .observe(on_pointer_down) - .observe(on_pointer_up); + .observe(update_material_on::>(hover_matl.clone())) + .observe(update_material_on::>(white_matl.clone())) + .observe(update_material_on::>(pressed_matl.clone())) + .observe(update_material_on::>(hover_matl.clone())) + .observe(rotate_on_drag); } - // Disable picking for the ground plane. + // Ground commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), - MeshMaterial3d(scene_materials.ground.clone()), - PickingBehavior::IGNORE, + MeshMaterial3d(ground_matl.clone()), + PickingBehavior::IGNORE, // Disable picking for the ground plane. )); // Light @@ -166,7 +146,7 @@ fn setup( // Instructions commands.spawn(( - Text::new("Hover over the shapes to pick them"), + Text::new("Hover over the shapes to pick them\nDrag to rotate"), Node { position_type: PositionType::Absolute, top: Val::Px(12.0), @@ -176,80 +156,42 @@ fn setup( )); } -/// Changes the material when the pointer is over the mesh. -fn on_pointer_over( - trigger: Trigger>, - scene_materials: Res, - mut query: Query<&mut MeshMaterial3d>, -) { - if let Ok(mut material) = query.get_mut(trigger.entity()) { - material.0 = scene_materials.hover.clone(); - } -} - -/// Resets the material when the pointer leaves the mesh. -fn on_pointer_out( - trigger: Trigger>, - scene_materials: Res, - mut query: Query<&mut MeshMaterial3d>, -) { - if let Ok(mut material) = query.get_mut(trigger.entity()) { - material.0 = scene_materials.white.clone(); - } -} - -/// Changes the material when the pointer is pressed. -fn on_pointer_down( - trigger: Trigger>, - scene_materials: Res, - mut query: Query<&mut MeshMaterial3d>, -) { - if let Ok(mut material) = query.get_mut(trigger.entity()) { - material.0 = scene_materials.pressed.clone(); +/// Returns an observer that updates the entity's material to the one specified. +fn update_material_on( + new_material: Handle, +) -> impl Fn(Trigger, Query<&mut MeshMaterial3d>) { + // An observer closure that captures `new_material`. We do this to avoid needing to write four + // versions of this observer, each triggered by a different event and with a different hardcoded + // material. Instead, the event type is a generic, and the material is passed in. + move |trigger, mut query| { + if let Ok(mut material) = query.get_mut(trigger.entity()) { + material.0 = new_material.clone(); + } } } -/// Resets the material when the pointer is released. -fn on_pointer_up( - trigger: Trigger>, - scene_materials: Res, - mut query: Query<&mut MeshMaterial3d>, -) { - if let Ok(mut material) = query.get_mut(trigger.entity()) { - material.0 = scene_materials.hover.clone(); +/// A system that draws hit indicators for every pointer. +fn draw_mesh_intersections(pointers: Query<&PointerInteraction>, mut gizmos: Gizmos) { + for (point, normal) in pointers + .iter() + .filter_map(|interaction| interaction.get_nearest_hit()) + .filter_map(|(_entity, hit)| hit.position.zip(hit.normal)) + { + gizmos.sphere(point, 0.05, RED_500); + gizmos.arrow(point, point + normal.normalize() * 0.5, PINK_100); } } -/// Draws the closest point of intersection for pointer hits. -fn on_mesh_hover( - mut pointer_hits: EventReader, - meshes: Query>, - mut gizmos: Gizmos, -) { - for hit in pointer_hits.read() { - // Get the first mesh hit. - // The hits are sorted by distance from the camera, so this is the closest hit. - let Some(closest_hit) = hit - .picks - .iter() - .filter_map(|(entity, hit)| meshes.get(*entity).map(|_| hit).ok()) - .next() - else { - continue; - }; - - let (Some(point), Some(normal)) = (closest_hit.position, closest_hit.normal) else { - return; - }; - - gizmos.sphere(point, 0.05, RED); - gizmos.arrow(point, point + normal * 0.5, PINK); - } -} - -/// Rotates the shapes. +/// A system that rotates all shapes. fn rotate(mut query: Query<&mut Transform, With>, time: Res