A crates/unified/assets/textures/outline.png => crates/unified/assets/textures/outline.png +0 -0
A crates/unified/assets/vector_textures/outline.svg => crates/unified/assets/vector_textures/outline.svg +24 -0
@@ 0,0 1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="544"
+ height="544"
+ viewBox="0 0 143.93333 143.93334"
+ version="1.1"
+ id="svg1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs1" />
+ <g
+ id="layer1">
+ <rect
+ style="fill:#f9f9f9;stroke-width:0.26679"
+ id="rect1"
+ width="143.93333"
+ height="143.93333"
+ x="0"
+ y="0" />
+ </g>
+</svg>
M crates/unified/src/ship_editor/components.rs => crates/unified/src/ship_editor/components.rs +12 -2
@@ 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<Entity>,
+ pub is_selected: bool,
+}
+#[derive(Component, Default, Clone, Copy, ExtractComponent)]
+pub struct OutlineCamera;
#[derive(Component)]
pub struct MainCamera;
#[derive(Component)]
M crates/unified/src/ship_editor/input.rs => crates/unified/src/ship_editor/input.rs +35 -13
@@ 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<Entity>,
peer: Option<Entity>,
+ pub can_select: bool,
}
fn on_scroll(
mut scroll_events: MessageReader<MouseWheel>,
- mut main_camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<MainCamera>)>,
- mut ghost_camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<GhostCamera>, Without<MainCamera>)>,
+ mut outline_camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<OutlineCamera>)>,
+ mut main_camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<MainCamera>, Without<OutlineCamera>)>,
+ mut ghost_camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<GhostCamera>, Without<MainCamera>, Without<OutlineCamera>)>,
window: Single<&Window, With<PrimaryWindow>>,
) {
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::<RenderLayers>()
+ .insert(MAIN_RENDER_LAYER)
+ .insert(Pickable::default())
+ .insert(Selectable::default())
+ .observe(click_select)
+ // .remove::<RenderLayers>()
.remove::<GhostModule>();
}
}
}
fn camera_drag(
- drag: ResMut<ShipEditorDrag>,
- mut main_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<MainCamera>)>,
- mut ghost_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<GhostCamera>, Without<MainCamera>)>,
+ mut drag: ResMut<ShipEditorDrag>,
+ mut outline_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<OutlineCamera>)>,
+ mut main_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<MainCamera>, Without<OutlineCamera>)>,
+ mut ghost_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<GhostCamera>, Without<MainCamera>, Without<OutlineCamera>)>,
window: Single<&Window, With<PrimaryWindow>>,
) {
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<ShipEditorDrag>,
M crates/unified/src/ship_editor/mod.rs => crates/unified/src/ship_editor/mod.rs +23 -4
@@ 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<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
- 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::<SpawnPartRequest>();
debug!("spawned part");
spawn_joints(strong_part_config, entity, commands.reborrow());
A crates/unified/src/ship_editor/select.rs => crates/unified/src/ship_editor/select.rs +34 -0
@@ 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<Pointer<Click>>,
+ mut parts: Query<(Entity, &Transform, &PartConfigHolder, &mut Selectable), With<Part>>,
+ drag: Res<ShipEditorDrag>,
+ mut commands: Commands,
+ asset_server: Res<AssetServer>,
+) {
+ 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