~starkingdoms/starkingdoms

32ccd92d40813fe55af0ae24206f1fa81cf56a15 — ghostly_zsh 5 hours ago daf8de5 ship-editor
ship editor feat: ghost snapping
2 files changed, 142 insertions(+), 21 deletions(-)

M crates/unified/src/ship_editor/input.rs
M crates/unified/src/ship_editor/mod.rs
M crates/unified/src/ship_editor/input.rs => crates/unified/src/ship_editor/input.rs +112 -20
@@ 2,7 2,10 @@ use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::input_focus::InputFocus;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, GHOST_RENDER_LAYER};
use crate::client::components::Me;
use crate::shared::attachment::{JointOf, PartInShip, Peer, SnapOf, SnapOfJoint};
use crate::ship_editor::components::{GhostCamera, GhostModule, MainCamera, Part, GHOST_RENDER_LAYER};
use crate::ship_editor::spawn_joints;
use crate::ship_editor::ui::PartEntry;

pub fn input_plugin(app: &mut App) {


@@ 25,34 28,42 @@ struct ShipEditorDrag {

fn on_scroll(
    mut scroll_events: MessageReader<MouseWheel>,
    mut camera: Single<(&mut Transform, &mut Projection), (With<Camera2d>, With<MainCamera>)>,
    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>)>,
    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 transform = camera.0.clone();
    let Projection::Orthographic(ref mut projection) = *camera.1 else { return };
    let mut main_transform = main_camera.0.clone();
    let mut ghost_transform = ghost_camera.0.clone();
    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() {
        match ev.unit {
            MouseScrollUnit::Line | MouseScrollUnit::Pixel => {
                if ev.y > 0.0 {
                    let mut rel_pos = vec2(window.width() / 2.0, -window.height() / 2.0) - cursor_pos;
                    rel_pos *= projection.scale;
                    projection.scale *= 0.95;
                    rel_pos *= main_projection.scale;
                    main_projection.scale *= 0.95;
                    ghost_projection.scale = main_projection.scale;
                    let scaled_rel_pos = rel_pos * 0.95;
                    transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0);
                    main_transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0);
                    ghost_transform.translation = main_transform.translation;
                } else {
                    let mut rel_pos = vec2(window.width() / 2.0, -window.height() / 2.0) - cursor_pos;
                    rel_pos *= projection.scale;
                    projection.scale *= 1.05;
                    rel_pos *= main_projection.scale;
                    main_projection.scale *= 1.05;
                    ghost_projection.scale = main_projection.scale;
                    let scaled_rel_pos = rel_pos * 1.05;
                    transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0);
                    main_transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0);
                    ghost_transform.translation = main_transform.translation;
                }
            }
        }
    }
    *camera.0 = transform;
    *main_camera.0 = main_transform;
    *ghost_camera.0 = ghost_transform;
}

fn on_click(


@@ 74,32 85,112 @@ fn on_click(

fn camera_drag(
    drag: ResMut<ShipEditorDrag>,
    mut camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<MainCamera>)>,
    mut main_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<MainCamera>)>,
    mut ghost_camera: Single<(&mut Transform, &Projection), (With<Camera2d>, With<GhostCamera>, Without<MainCamera>)>,
    window: Single<&Window, With<PrimaryWindow>>,
) {
    if !drag.is_dragging || drag.is_holding_module { return }
    let Some(cursor) = window.cursor_position() else { return };
    let projection = camera.1.clone();
    let projection = main_camera.1.clone();
    let Projection::Orthographic(projection) = projection else { return };
    let transform = &mut camera.0;
    let main_transform = &mut main_camera.0;
    let ghost_transform = &mut ghost_camera.0;

    transform.translation = drag.init_camera_pos.extend(0.0) - (cursor.with_y(-cursor.y) - drag.init_cursor_pos).extend(0.0)*projection.scale;
    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;
}
fn ghost_drag(
    drag: Res<ShipEditorDrag>,
    window: Single<&Window, With<PrimaryWindow>>,
    mut ghost_module: Query<&mut Transform, With<GhostModule>>,
    mut ghost_module: Single<(Entity, &mut Transform), With<GhostModule>>,
    mut main_camera: Single<(&Camera, &GlobalTransform), (With<Camera2d>, With<MainCamera>)>,
    mut ghost_camera: Single<(&Camera, &GlobalTransform), (With<Camera2d>, With<GhostCamera>)>,
    snaps: Query<(&Transform, &SnapOfJoint, &SnapOf, Entity), Without<GhostModule>>,
    joints: Query<(&Transform, &JointOf, Entity), Without<GhostModule>>,
    parts: Query<&GlobalTransform, (Or<(With<Part>, With<GhostModule>)>)>,
) {
    const SNAP_CUTOFF: f32 = 25.0;
    if !drag.is_dragging || !drag.is_holding_module { return }
    let (ghost_entity, mut ghost_transform) = ghost_module.into_inner();
    let Some(cursor) = window.cursor_position() else { return };
    let (ghost_camera, camera_transform) = *ghost_camera;
    let Ok(ghost_position) = ghost_camera.viewport_to_world_2d(camera_transform, cursor) else {
    let (ghost_camera, ghost_camera_transform) = *ghost_camera;
    let (main_camera, main_camera_transform) = *main_camera;
    let Ok(ghost_position) = ghost_camera.viewport_to_world_2d(ghost_camera_transform, cursor) else {
        error!("Unable to convert cursor position to world space");
        return // return because this error happens when something is invalid with the camera
    };
    let Ok(global_cursor_position) = ghost_camera.viewport_to_world_2d(main_camera_transform, cursor) else {
        error!("Unable to convert cursor position to world space");
        return
    };

    let mut closest_snap = f32::INFINITY;
    let mut closest_snap_transform = None;
    for (snap_local_transform, snap_joint, snap_part, snap_id) in snaps {
        if snap_part.0 == ghost_entity { continue }

        let Ok(part_transform) = parts.get(snap_part.0) else { continue };

        let snap_global_translation = part_transform.mul_transform(*snap_local_transform);

        let distance_to_cursor = global_cursor_position.distance(snap_global_translation.translation().xy());

        if distance_to_cursor > closest_snap {
            continue;
        }
        if distance_to_cursor > SNAP_CUTOFF {
            continue;
        }


    for mut ghost_transform in ghost_module.iter_mut() {
        let Ok((offset, _, _)) = joints.get(snap_joint.0) else {
            continue;
        };
        let global_joint_offset = part_transform.transform_point(offset.translation);

        let mut target_transform = Transform {
            translation: global_joint_offset,
            rotation: ghost_transform.rotation,
            scale: Vec3::splat(1.0),
        };

        // figure out the ghost's best snap to use
        let mut closest_peer_snap_distance = f32::INFINITY;
        let mut closest_peer_snap = None;
        let mut closest_joint_transform = None;
        for (our_snap_local_transform, our_snap_joint, our_snap_part, our_snap_id) in &snaps {
            if ghost_entity != our_snap_part.0 {
                continue;
            }

            let our_snap_global_translation =
                target_transform.mul_transform(*our_snap_local_transform);

            let distance = our_snap_global_translation
                .translation
                .distance(snap_global_translation.translation());
            if distance > closest_peer_snap_distance {
                continue;
            }

            let Ok((our_joint, _, _)) = joints.get(our_snap_joint.0) else {
                continue;
            };

            closest_peer_snap_distance = distance;
            closest_peer_snap = Some(our_snap_id);
            closest_joint_transform = Some(our_joint);
        }

        target_transform.rotation = part_transform.rotation()
            * (offset.rotation * closest_joint_transform.unwrap().rotation.inverse())
            * Quat::from_rotation_z(180.0f32.to_radians());
        closest_snap = distance_to_cursor;
        closest_snap_transform = Some(target_transform);
    }
    if let Some(transform) = closest_snap_transform {
        *ghost_transform = transform;
    } else {
        ghost_transform.translation = ghost_position.extend(0.0);
    }
}


@@ 147,12 238,13 @@ fn part_button_interaction(
                    error!("Unable to convert cursor position to world space");
                    return // return because this error happens when something is invalid with the camera
                };
                commands.spawn((
                let entity = commands.spawn((
                    sprite,
                    Transform::from_translation(ghost_position.extend(0.0)),
                    GhostModule,
                    GHOST_RENDER_LAYER,
                ));
                spawn_joints(&part_entry.0, entity.id(), commands.reborrow());
            }
        }
    }

M crates/unified/src/ship_editor/mod.rs => crates/unified/src/ship_editor/mod.rs +30 -1
@@ 6,7 6,8 @@ pub mod components;
use bevy::input_focus::InputFocus;
use crate::client::colors;
use crate::prelude::*;
use crate::shared::config::part::PartConfig;
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, PlayerPartRequest, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER};
use crate::ship_editor::input::input_plugin;


@@ 90,6 91,34 @@ fn spawn_parts(
                .insert(sprite)
                .insert(Part(strong_part_config.clone()))
                .remove::<SpawnPartRequest>();
            debug!("spawned part");
            spawn_joints(strong_part_config, entity, commands.reborrow());
        }
    }
}

fn spawn_joint_bundle(joint: &JointConfig, part: &PartConfig, parent: &Entity) -> impl Bundle {
    let j_comp = Joint {
        id: JointId::from_part_and_joint_id(part.part.name.clone(), joint.id.clone()),
        transform: joint.target.into(),
    };
    let joint_transform: Transform = j_comp.transform;
    let joint_of = JointOf(*parent);

    (j_comp, joint_transform, joint_of)
}
fn spawn_snap_bundle(joint: &JointConfig, parent: &Entity, p_joint: &Entity) -> impl Bundle {
    let snap_transform: Transform = joint.snap.into();
    let snap_for = SnapOf(*parent);
    let snap_of = SnapOfJoint(*p_joint);

    (snap_transform, snap_for, snap_of)
}
fn spawn_joints(config: &PartConfig, parent: Entity, mut commands: Commands) {
    for joint in &config.joints {
        let joint_id = commands
            .spawn(spawn_joint_bundle(joint, config, &parent))
            .id();
        commands.spawn(spawn_snap_bundle(joint, &parent, &joint_id));
    }
}
\ No newline at end of file