use bevy::prelude::*; use bevy_mod_outline::{ AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume, }; use bevy_mod_picking::{prelude::Pointer, selection::Select, PickableBundle}; use crate::{ input::{self, Action, InputAction, Lifecycle}, scene::{SceneElement, SceneSpawnerSystem, SceneState}, StudioMode, }; pub struct SelectionPlugin; impl Plugin for SelectionPlugin { fn build(&self, app: &mut App) { app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin)) .add_systems(Update, update_selection) .add_systems( PostUpdate, draw_selection_outline.run_if(resource_changed_or_removed::()), ) .add_systems( OnEnter(SceneState::Active), (apply_deferred, tag_scene_elements) .chain() .after(SceneSpawnerSystem), ); #[cfg(feature = "wasm")] app.add_systems( PostUpdate, wasm::notify_entity_selection_change .map(bevy::utils::error) .run_if(resource_changed_or_removed::()), ); } } #[derive(Component, Clone, Copy)] pub struct Selectable; #[derive(Component, Clone, Copy)] pub struct Selected; #[derive(Resource, Deref, DerefMut, Debug)] pub struct Selection(Entity); fn tag_scene_elements( mut cmd: Commands, q_scene_elements: Query<(Entity, &Children), (With, Without)>, q_primitives: Query<&Handle>, r_studio_mode : Res ) { for (ent, children) in q_scene_elements.iter() { cmd.entity(ent).insert(Selectable); for &ent_child in children .iter() .filter(|child| q_primitives.contains(**child)) { if let StudioMode::Editor = *r_studio_mode { cmd.entity(ent_child).insert(PickableBundle::default()); } } } } fn update_selection( mut cmd: Commands, // FIXME: These events aren't aware of our input actions, so we receive a // selection event when LMB is released after orbiting the camera. // Should drop bevy_mod_picking's Selection plugin/feature and just // use our own conditional click listener. mut ev_select: EventReader>, mut ev_input: EventReader, q_parents: Query<&Parent>, q_selectable: Query<&Selectable>, q_selected: Query>, ) { if let Some(event) = ev_select.read().last() { // Clear any existing `Selected` components for ent in q_selected.iter() { cmd.entity(ent).remove::(); } // Find the nearest Selectable parent of the target entity let mut ent = event.target; let ent_selected = loop { if q_selectable.get(ent).is_ok() { break Some(ent); } if let Ok(parent) = q_parents.get(ent) { ent = parent.get(); } else { break None; } }; if let Some(ent) = ent_selected { cmd.insert_resource(Selection(ent)); cmd.entity(ent).insert(Selected); } else { cmd.remove_resource::(); } } else if ev_input.read().any(|action| { matches!( action, InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear)) ) }) { cmd.remove_resource::(); for ent in q_selected.iter() { cmd.entity(ent).remove::(); } } } fn draw_selection_outline( mut cmd: Commands, q_selected: Query<&Children, With>, q_meshes: Query>, Without)>, q_outlines: Query>, ) { // Remove existing outlines for ent in q_outlines.iter() { cmd.entity(ent).remove::(); } for children in q_selected.iter() { for &child in children.iter() { if q_meshes.contains(child) { cmd.entity(child).insert(OutlineBundle { outline: OutlineVolume { visible: true, width: 4., colour: Color::YELLOW, }, ..default() }); } } } } #[cfg(feature = "wasm")] mod wasm { use bevy::{ prelude::{Query, Res, With}, window::{PrimaryWindow, Window}, }; use wasm_bindgen::prelude::*; use super::Selection; use crate::wasm; #[wasm_bindgen(typescript_custom_section)] const EVENT_DECLARATION: &str = r#" /** * An event indicating that an entity has been selected or that the selection * has been cleared. */ export interface EntitySelectEvent extends CustomEvent { /** The selected entity ID. If `null`, the selection has been cleared. */ detail: string | null; } declare global { export interface HTMLElementEventMap { "entity-select": EntitySelectEvent; } } "#; pub(super) fn notify_entity_selection_change( r_selection: Option>, q_window: Query<&Window, With>, ) -> Result<(), String> { let entity = r_selection.map(|selection| **selection); wasm::dispatch_to_js(q_window.single(), "entity-select", &entity) } }