Files
storyteller-bevy/studio/src/interaction/selection.rs
T
2024-02-28 19:42:39 -05:00

274 lines
8.1 KiB
Rust

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::<Selection>()),
),
)
.add_systems(
PostUpdate,
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
)
.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::<Selection>()),
);
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<Entity, (Added<Selected>, Without<space_editor::prelude::Selected>)>,
q_editor_selected: Query<Entity, With<space_editor::prelude::Selected>>,
q_selected: Query<Entity, With<Selected>>,
q_added_editor_selected: Query<
Entity,
(Without<Selected>, Added<space_editor::prelude::Selected>),
>,
) {
for ent in q_new_selected.iter() {
for ed_ent in q_editor_selected.iter() {
cmd.entity(ed_ent)
.remove::<space_editor::prelude::Selected>();
}
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::<Selected>();
}
}
}
fn tag_scene_elements(
mut cmd: Commands,
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
q_primitives: Query<&Handle<Mesh>>,
) {
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<Pointer<Select>>,
mut ev_input: EventReader<InputAction>,
q_parents: Query<&Parent>,
q_selectable: Query<&Selectable>,
q_selected: Query<Entity, With<Selected>>,
) {
if let Some(event) = ev_select.read().last() {
// Clear any existing `Selected` components
for ent in q_selected.iter() {
cmd.entity(ent).remove::<Selected>();
}
// 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::<Selection>();
}
} else if ev_input.read().any(|action| {
matches!(
action,
InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Clear))
)
}) {
cmd.remove_resource::<Selection>();
for ent in q_selected.iter() {
cmd.entity(ent).remove::<Selected>();
}
}
}
fn process_delete_inputs(
mut cmd: Commands,
mut er_input: EventReader<InputAction>,
mut ew_changes: EventWriter<space_undo::NewChange>,
r_selection: Res<Selection>,
) {
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::<Selection>();
ew_changes.send(NewChange {
change: Arc::new(RemovedEntity { entity }),
});
}
}
fn draw_selection_outline(
mut cmd: Commands,
q_selected: Query<&Children, With<Selected>>,
q_meshes: Query<
Entity,
(
With<Handle<Mesh>>,
Without<OutlineVolume>,
Without<NoSelectionOutline>,
// FIXME: It shouldn't be necessary to filter out SkinnedMesh here
Without<SkinnedMesh>,
),
>,
q_outlines: Query<Entity, With<OutlineVolume>>,
) {
// Remove existing outlines
for ent in q_outlines.iter() {
cmd.entity(ent).remove::<OutlineBundle>();
}
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<Res<Selection>>,
q_window: Query<&Window, With<PrimaryWindow>>,
) -> Result<(), String> {
let entity = r_selection.map(|selection| **selection);
wasm::dispatch_to_js(q_window.single(), "entity-select", &entity)
}
static SELECTION_REQUEST: WasmMessageBuffer<Entity> = 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);
}
}
}