~starkingdoms/starkingdoms

1018826380f46e306958358cc687d9a5302b2be6 — ghostly_zsh 2 hours ago f85bb4d
ship editor feat: selection and deselection
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