use std::sync::Arc; use bevy::{prelude::*, render::mesh::skinning::SkinnedMesh}; use bevy_mod_outline::{ AutoGenerateOutlineNormalsPlugin, OutlineBundle, OutlinePlugin, OutlineVolume, }; use bevy_mod_picking::{prelude::Pointer, selection::Select, PickableBundle}; use space_editor::prelude::space_undo::{self, NewChange, RemovedEntity}; use crate::{ input::{self, Action, InputAction, Lifecycle}, scene::{SceneElement, SceneSpawnerSystem, SceneState}, }; pub struct SelectionPlugin; impl Plugin for SelectionPlugin { fn build(&self, app: &mut App) { app.add_plugins((OutlinePlugin, AutoGenerateOutlineNormalsPlugin)) .add_systems( Update, ( update_selection, process_delete_inputs.run_if(resource_exists::()), ), ) .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), ); app.add_systems(Update, tag_scene_elements); #[cfg(feature = "wasm")] app.add_systems( Update, wasm::update_selection_from_frontend.run_if(crate::is_editor), ) .add_systems( PostUpdate, wasm::notify_entity_selection_change .map(bevy::utils::error) .run_if(resource_changed_or_removed::()), ); app.add_systems(Update, sync_editor_selected); } } #[derive(Component, Clone, Copy)] pub struct Selectable; #[derive(Component, Clone, Copy)] pub struct Selected; #[derive(Resource, Deref, DerefMut, Debug)] pub struct Selection(Entity); #[derive(Component)] pub struct NoSelectionOutline; fn sync_editor_selected( mut cmd: Commands, q_new_selected: Query, Without)>, q_editor_selected: Query>, q_selected: Query>, q_added_editor_selected: Query< Entity, (Without, Added), >, ) { for ent in q_new_selected.iter() { for ed_ent in q_editor_selected.iter() { cmd.entity(ed_ent) .remove::(); } cmd.entity(ent).insert(space_editor::prelude::Selected); } for ent in q_added_editor_selected.iter() { cmd.entity(ent).insert(Selected); cmd.insert_resource(Selection(ent)); for ent in q_selected.iter() { cmd.entity(ent).remove::(); } } } fn tag_scene_elements( mut cmd: Commands, q_scene_elements: Query<(Entity, &Children), (With, Without)>, q_primitives: Query<&Handle>, ) { 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)) { 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 process_delete_inputs( mut cmd: Commands, mut er_input: EventReader, mut ew_changes: EventWriter, r_selection: Res, ) { if er_input.read().any(|action| { matches!( action, InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Delete)) ) }) { let entity = **r_selection; cmd.entity(entity).despawn_recursive(); cmd.remove_resource::(); ew_changes.send(NewChange { change: Arc::new(RemovedEntity { entity }), }); } } fn draw_selection_outline( mut cmd: Commands, q_selected: Query<&Children, With>, q_meshes: Query< Entity, ( With>, Without, Without, // FIXME: It shouldn't be necessary to filter out SkinnedMesh here 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::*, window::{PrimaryWindow, Window}, }; use wasm_bindgen::prelude::*; use super::{Selected, Selection}; use crate::wasm::{self, FromStr, StaticBuffer, WasmMessageBuffer}; #[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 GlobalEventHandlersEventMap { "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) } static SELECTION_REQUEST: WasmMessageBuffer = WasmMessageBuffer::new(); #[wasm_bindgen] pub fn select(entity: &str) { let entity = Entity::from_str(entity).unwrap(); SELECTION_REQUEST.write_message(entity); } pub(super) fn update_selection_from_frontend(mut cmd: Commands) { if let Some(entity) = SELECTION_REQUEST.take_message() { cmd.insert_resource(Selection(entity)); cmd.entity(entity).insert(Selected); } } }