From 1018826380f46e306958358cc687d9a5302b2be6 Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Fri, 19 Jun 2026 15:11:26 -0500 Subject: [PATCH] ship editor feat: selection and deselection --- crates/unified/assets/textures/outline.png | Bin 0 -> 2458 bytes .../assets/vector_textures/outline.svg | 24 +++++++++ crates/unified/src/ship_editor/components.rs | 14 ++++- crates/unified/src/ship_editor/input.rs | 48 +++++++++++++----- crates/unified/src/ship_editor/mod.rs | 27 ++++++++-- crates/unified/src/ship_editor/select.rs | 34 +++++++++++++ 6 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 crates/unified/assets/textures/outline.png create mode 100644 crates/unified/assets/vector_textures/outline.svg create mode 100644 crates/unified/src/ship_editor/select.rs diff --git a/crates/unified/assets/textures/outline.png b/crates/unified/assets/textures/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..21f22a65138ab954379345715690e341efb75c29 GIT binary patch literal 2458 zcmeAS@N?(olHy`uVBq!ia0y~yU{U~K4mP03p3X}$K#H@#BeIx*f$uN~Gak=hkpdKy zEOCt}3C>R|DNig)WhgH%*UQYyE>2D?NY%?PN}v7CMhd7=-P6S}q+-t7Yle&m6a)?& z2>Rbv&m^Asz|Zm6nwdA{85~~hmjzmLyn%til9`F&2@eOu1Y-pThjXI>qv0`{97c1) oXkjo~7LL{pqgCVZZW-`Be#)U$-*=-N*sfymboFyt=akR{02EyxuK)l5 literal 0 HcmV?d00001 diff --git a/crates/unified/assets/vector_textures/outline.svg b/crates/unified/assets/vector_textures/outline.svg new file mode 100644 index 0000000000000000000000000000000000000000..3e7bd64575d509d1de66f46b251d0d4c1af22511 --- /dev/null +++ b/crates/unified/assets/vector_textures/outline.svg @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/crates/unified/src/ship_editor/components.rs b/crates/unified/src/ship_editor/components.rs index 0d6feedc7cc9de0b5f4964214117ee5d5e94b310..70302499b11120e15fdfecd003aae919c38a71b3 100644 --- a/crates/unified/src/ship_editor/components.rs +++ b/crates/unified/src/ship_editor/components.rs @@ -1,10 +1,13 @@ use bevy::camera::visibility::RenderLayers; +use bevy::render::extract_component::ExtractComponent; +use bevy::render::render_resource::ShaderType; use crate::prelude::{Component, Handle, Entity, Resource}; use crate::shared::config::part::PartConfig; use crate::shared::config::ship_editor::ShipEditorConfig; -pub const MAIN_RENDER_LAYER: RenderLayers = RenderLayers::layer(0); -pub const GHOST_RENDER_LAYER: RenderLayers = RenderLayers::layer(1); +pub const OUTLINE_RENDER_LAYER: RenderLayers = RenderLayers::layer(0); +pub const MAIN_RENDER_LAYER: RenderLayers = RenderLayers::layer(1); +pub const GHOST_RENDER_LAYER: RenderLayers = RenderLayers::layer(2); #[derive(Component)] pub struct GhostModule; @@ -12,7 +15,14 @@ pub struct GhostModule; pub struct Part; #[derive(Component)] pub struct PartConfigHolder(pub PartConfig); +#[derive(Component, Default)] +pub struct Selectable { + pub outline_entity: Option, + pub is_selected: bool, +} +#[derive(Component, Default, Clone, Copy, ExtractComponent)] +pub struct OutlineCamera; #[derive(Component)] pub struct MainCamera; #[derive(Component)] diff --git a/crates/unified/src/ship_editor/input.rs b/crates/unified/src/ship_editor/input.rs index ab17bc9999b12d5406ac62303e5d6c66c0d0675c..649c9d0e3f84e2156480c40f94a21cd36972a25e 100644 --- a/crates/unified/src/ship_editor/input.rs +++ b/crates/unified/src/ship_editor/input.rs @@ -8,11 +8,14 @@ use bevy::prelude::*; use bevy::window::PrimaryWindow; use crate::client::components::Me; use crate::shared::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint}; -use crate::shared::ecs::MAIN_LAYER; -use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, Part, PartConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER}; +use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, OutlineCamera, Part, PartConfigHolder, Selectable, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; +use crate::ship_editor::select::click_select; use crate::ship_editor::spawn_joints; use crate::ship_editor::ui::PartEntry; +// how much the cursor can move before drag stops a selection +const DRAG_SELECT_CUTOFF: f32 = 10.0; + pub fn input_plugin(app: &mut App) { app .insert_resource(ShipEditorDrag::default()) @@ -24,26 +27,30 @@ pub fn input_plugin(app: &mut App) { } #[derive(Resource, Default)] -struct ShipEditorDrag { +pub struct ShipEditorDrag { is_dragging: bool, is_holding_module: bool, init_cursor_pos: Vec2, init_camera_pos: Vec2, target: Option, peer: Option, + pub can_select: bool, } fn on_scroll( mut scroll_events: MessageReader, - mut main_camera: Single<(&mut Transform, &mut Projection), (With, With)>, - mut ghost_camera: Single<(&mut Transform, &mut Projection), (With, With, Without)>, + mut outline_camera: Single<(&mut Transform, &mut Projection), (With, With)>, + mut main_camera: Single<(&mut Transform, &mut Projection), (With, With, Without)>, + mut ghost_camera: Single<(&mut Transform, &mut Projection), (With, With, Without, Without)>, window: Single<&Window, With>, ) { let Some(cursor_pos) = window.cursor_position() else { return }; let cursor_pos = cursor_pos.with_y(-cursor_pos.y); + let mut outline_transform = outline_camera.0.clone(); let mut main_transform = main_camera.0.clone(); let mut ghost_transform = ghost_camera.0.clone(); + let Projection::Orthographic(ref mut outline_projection) = *outline_camera.1 else { return }; let Projection::Orthographic(ref mut main_projection) = *main_camera.1 else { return }; let Projection::Orthographic(ref mut ghost_projection) = *ghost_camera.1 else { return }; for ev in scroll_events.read() { @@ -54,23 +61,28 @@ fn on_scroll( rel_pos *= main_projection.scale; main_projection.scale *= 0.95; ghost_projection.scale = main_projection.scale; + outline_projection.scale = main_projection.scale; let scaled_rel_pos = rel_pos * 0.95; main_transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0); ghost_transform.translation = main_transform.translation; + outline_transform.translation = main_transform.translation; } else { let mut rel_pos = vec2(window.width() / 2.0, -window.height() / 2.0) - cursor_pos; rel_pos *= main_projection.scale; main_projection.scale *= 1.05; ghost_projection.scale = main_projection.scale; + outline_projection.scale = main_projection.scale; let scaled_rel_pos = rel_pos * 1.05; main_transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0); ghost_transform.translation = main_transform.translation; + outline_transform.translation = main_transform.translation; } } } } *main_camera.0 = main_transform; *ghost_camera.0 = ghost_transform; + *outline_camera.0 = outline_transform; } fn on_click( @@ -97,6 +109,7 @@ fn on_click( let Some(cursor_pos) = window.cursor_position() else { return }; if ev.just_pressed(MouseButton::Left) { drag.is_dragging = true; + drag.can_select = true; drag.init_cursor_pos = cursor_pos.with_y(-cursor_pos.y); drag.init_camera_pos = camera.translation.truncate(); } @@ -128,6 +141,7 @@ fn on_click( return; } + // target entity (the one being attached to) let Ok((target_xform, target_in_ship, target_entity, _, _)) = parts.get(target_jt_of.0) else { return; }; @@ -156,17 +170,21 @@ fn on_click( ghost_sprite.color = Color::srgb(1.0, 1.0, 1.0); commands.entity(ghost_entity) .insert(Part) - .insert(MAIN_LAYER) - .remove::() + .insert(MAIN_RENDER_LAYER) + .insert(Pickable::default()) + .insert(Selectable::default()) + .observe(click_select) + // .remove::() .remove::(); } } } fn camera_drag( - drag: ResMut, - mut main_camera: Single<(&mut Transform, &Projection), (With, With)>, - mut ghost_camera: Single<(&mut Transform, &Projection), (With, With, Without)>, + mut drag: ResMut, + mut outline_camera: Single<(&mut Transform, &Projection), (With, With)>, + mut main_camera: Single<(&mut Transform, &Projection), (With, With, Without)>, + mut ghost_camera: Single<(&mut Transform, &Projection), (With, With, Without, Without)>, window: Single<&Window, With>, ) { if !drag.is_dragging || drag.is_holding_module { return } @@ -174,10 +192,14 @@ fn camera_drag( let projection = main_camera.1.clone(); let Projection::Orthographic(projection) = projection else { return }; let main_transform = &mut main_camera.0; - let ghost_transform = &mut ghost_camera.0; - main_transform.translation = drag.init_camera_pos.extend(0.0) - (cursor.with_y(-cursor.y) - drag.init_cursor_pos).extend(0.0)*projection.scale; - ghost_transform.translation = main_transform.translation; + let delta = (drag.init_cursor_pos - cursor.with_y(-cursor.y)).extend(0.0); + if delta.length() > DRAG_SELECT_CUTOFF { + drag.can_select = false; + } + main_transform.translation = drag.init_camera_pos.extend(0.0) + delta*projection.scale; + ghost_camera.0.translation = main_transform.translation; + outline_camera.0.translation = main_transform.translation; } fn ghost_drag( mut drag: ResMut, diff --git a/crates/unified/src/ship_editor/mod.rs b/crates/unified/src/ship_editor/mod.rs index fd23b4546dd02c227780de423f40d983fc9524ec..01cd0e2e1f65e67057e4e9448c631b40a7d7145a 100644 --- a/crates/unified/src/ship_editor/mod.rs +++ b/crates/unified/src/ship_editor/mod.rs @@ -2,6 +2,7 @@ pub mod ui; pub mod input; pub mod plugins; pub mod components; +pub mod select; use bevy::input_focus::InputFocus; use crate::client::colors; @@ -9,8 +10,9 @@ use crate::prelude::*; use crate::shared::attachment::{Joint, JointId, JointOf, SnapOf, SnapOfJoint}; use crate::shared::config::part::{JointConfig, PartConfig}; use crate::shared::config::ship_editor::ShipEditorConfig; -use crate::ship_editor::components::{GhostCamera, MainCamera, Part, PartConfigHolder, PlayerPartRequest, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; +use crate::ship_editor::components::{GhostCamera, MainCamera, OutlineCamera, Part, PartConfigHolder, PlayerPartRequest, Selectable, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER, OUTLINE_RENDER_LAYER}; use crate::ship_editor::input::input_plugin; +use crate::ship_editor::select::click_select; use crate::ship_editor::ui::{ui_plugin, PendingPart}; pub struct ShipEditorPlugin; @@ -31,22 +33,35 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, ) { - commands.insert_resource(ClearColor(colors::BASE)); + //commands.insert_resource(ClearColor(colors::BASE)); commands.spawn(( Camera2d, Camera { order: 0, + clear_color: ClearColorConfig::Custom(colors::BASE), + ..Default::default() + }, + Transform::from_xyz(0.0, 0.0, 0.0), + OutlineCamera, + OUTLINE_RENDER_LAYER, + )); + commands.spawn(( + Camera2d, + Camera { + order: 1, + clear_color: ClearColorConfig::None, ..Default::default() }, Transform::from_xyz(0.0, 0.0, 0.0), - MainCamera, IsDefaultUiCamera, + MainCamera, MAIN_RENDER_LAYER, )); commands.spawn(( Camera2d::default(), Camera { - order: 1, + order: 2, + clear_color: ClearColorConfig::None, ..Default::default() }, GhostCamera, @@ -55,6 +70,7 @@ fn setup( let rectangle = meshes.add(Rectangle::new(50.0, 50.0)); commands.spawn(( PlayerPartRequest, + MAIN_RENDER_LAYER, Transform::from_xyz(0.0, 0.0, 0.0), )); } @@ -91,6 +107,9 @@ fn spawn_parts( .insert(sprite) .insert(Part) .insert(PartConfigHolder(strong_part_config.clone())) + .insert(Pickable::default()) + .insert(Selectable::default()) + .observe(click_select) .remove::(); debug!("spawned part"); spawn_joints(strong_part_config, entity, commands.reborrow()); diff --git a/crates/unified/src/ship_editor/select.rs b/crates/unified/src/ship_editor/select.rs new file mode 100644 index 0000000000000000000000000000000000000000..c67a7fa0d98d8bf4d6c44fa05a77447d693be3e1 --- /dev/null +++ b/crates/unified/src/ship_editor/select.rs @@ -0,0 +1,34 @@ +use crate::prelude::*; +use crate::ship_editor::components::{Part, PartConfigHolder, Selectable, OUTLINE_RENDER_LAYER}; +use crate::ship_editor::input::ShipEditorDrag; + +pub fn click_select( + ev: On>, + mut parts: Query<(Entity, &Transform, &PartConfigHolder, &mut Selectable), With>, + drag: Res, + mut commands: Commands, + asset_server: Res, +) { + debug!("click detected"); + if !drag.can_select { return } + let Ok((part_entity, part_transform, part, mut part_selectable)) = parts.get_mut(ev.entity) else { + error!("No Part found upon part selection. The observer probably wasn't removed."); + return; + }; + + if !part_selectable.is_selected { + let mut sprite = Sprite::from_image(asset_server.load("textures/outline.png")); + sprite.custom_size = Some(vec2(part.0.physics.width as f32, part.0.physics.height as f32) * 1.0625); + let outline_entity = commands.spawn(( + part_transform.clone(), + sprite, + )); + part_selectable.outline_entity = Some(outline_entity.id()); + part_selectable.is_selected = true; + } else { + part_selectable.is_selected = false; + if let Some(outline_entity) = part_selectable.outline_entity { + commands.entity(outline_entity).despawn(); + } + } +} \ No newline at end of file