From 32ccd92d40813fe55af0ae24206f1fa81cf56a15 Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Thu, 18 Jun 2026 01:50:38 -0500 Subject: [PATCH] ship editor feat: ghost snapping --- crates/unified/src/ship_editor/input.rs | 132 ++++++++++++++++++++---- crates/unified/src/ship_editor/mod.rs | 31 +++++- 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/crates/unified/src/ship_editor/input.rs b/crates/unified/src/ship_editor/input.rs index 62d6968de2e68e67bf07f8b8abd9627c520c902f..623c0695efd78785664f7ff29149cc1311099f46 100644 --- a/crates/unified/src/ship_editor/input.rs +++ b/crates/unified/src/ship_editor/input.rs @@ -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, - mut camera: Single<(&mut Transform, &mut Projection), (With, With)>, + mut main_camera: Single<(&mut Transform, &mut Projection), (With, With)>, + mut ghost_camera: Single<(&mut Transform, &mut Projection), (With, With, 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 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, - mut camera: Single<(&mut Transform, &Projection), (With, With)>, + mut main_camera: Single<(&mut Transform, &Projection), (With, With)>, + mut ghost_camera: Single<(&mut Transform, &Projection), (With, With, Without)>, window: Single<&Window, With>, ) { 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, window: Single<&Window, With>, - mut ghost_module: Query<&mut Transform, With>, + mut ghost_module: Single<(Entity, &mut Transform), With>, + mut main_camera: Single<(&Camera, &GlobalTransform), (With, With)>, mut ghost_camera: Single<(&Camera, &GlobalTransform), (With, With)>, + snaps: Query<(&Transform, &SnapOfJoint, &SnapOf, Entity), Without>, + joints: Query<(&Transform, &JointOf, Entity), Without>, + parts: Query<&GlobalTransform, (Or<(With, With)>)>, ) { + 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()); } } } diff --git a/crates/unified/src/ship_editor/mod.rs b/crates/unified/src/ship_editor/mod.rs index b910e4445edc1233c5bf05a94ce176ab407c4b7f..29fff4c7680f2578813e063bc14a240dc86b3123 100644 --- a/crates/unified/src/ship_editor/mod.rs +++ b/crates/unified/src/ship_editor/mod.rs @@ -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::(); + 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