mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
274 lines
8.1 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|