From f75c2e1acc0e58683b4295dc68dc1fb5ee9ba217 Mon Sep 17 00:00:00 2001 From: core Date: Thu, 10 Jul 2025 10:36:23 -0400 Subject: [PATCH] feat: drag ghosts and snapping --- crates/unified/src/attachment.rs | 2 +- crates/unified/src/client/key_input.rs | 10 ++- crates/unified/src/client/parts.rs | 88 +++++++++++++++++++++++--- crates/unified/src/config/part.rs | 2 +- crates/unified/src/server/player.rs | 1 + 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/crates/unified/src/attachment.rs b/crates/unified/src/attachment.rs index fb8026c61147e256c64f1f3f51efc954685b9b53..ba47d5999c1095fdc0254c6d421e54ed776b1dde 100644 --- a/crates/unified/src/attachment.rs +++ b/crates/unified/src/attachment.rs @@ -12,7 +12,7 @@ pub struct Parts(#[entities] Vec); #[derive(Component, Serialize, Deserialize, MapEntities)] #[relationship(relationship_target = Parts)] -pub struct PartInShip(#[entities] Entity); +pub struct PartInShip(#[entities] pub Entity); #[derive(Component, Serialize, Deserialize)] #[require(Transform)] diff --git a/crates/unified/src/client/key_input.rs b/crates/unified/src/client/key_input.rs index 141b5b187a9421db70007e8417e6e855947a4f4b..ff67c3b6d5a105017930fba4e9d7844c3ec406a8 100644 --- a/crates/unified/src/client/key_input.rs +++ b/crates/unified/src/client/key_input.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use crate::attachment::{Joint, JointOf, SnapOf, SnapOfJoint}; use crate::ecs::{Part, ThrustEvent}; use bevy::color::palettes::css::{FUCHSIA, GREEN}; @@ -20,7 +21,14 @@ pub fn key_input_plugin(app: &mut App) { } #[derive(Resource, Default)] -struct AttachmentDebugRes(bool); +pub struct AttachmentDebugRes(pub bool); +impl Deref for AttachmentDebugRes { + type Target = bool; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} fn debug_render_keybind( keys: Res>, diff --git a/crates/unified/src/client/parts.rs b/crates/unified/src/client/parts.rs index fb81be8febed13afcf73b7eb71c5bbc2536fffcc..b20006682a1618a6387f301bfb08250c8b6cc30b 100644 --- a/crates/unified/src/client/parts.rs +++ b/crates/unified/src/client/parts.rs @@ -1,12 +1,16 @@ +use bevy::color::palettes::css::{ORANGE, RED}; use crate::client::Me; use crate::ecs::{CursorWorldCoordinates, DragRequestEvent, Part}; use bevy::prelude::*; use bevy_rapier2d::dynamics::MassProperties; use bevy_rapier2d::prelude::AdditionalMassProperties; +use crate::attachment::{JointOf, PartInShip, SnapOf, SnapOfJoint}; +use crate::client::colors::GREEN; +use crate::client::key_input::AttachmentDebugRes; pub fn parts_plugin(app: &mut App) { app.insert_resource(DragResource(None)); - app.add_systems(Update, (handle_incoming_parts, handle_updated_parts)); + app.add_systems(Update, (handle_incoming_parts, handle_updated_parts, update_drag_ghosts)); app.add_observer(on_part_release); } @@ -55,18 +59,29 @@ fn handle_updated_parts( #[derive(Resource)] struct DragResource(Option); +#[derive(Component)] +struct DragGhost; fn on_part_click( ev: Trigger>, - sprites: Query<&Sprite, Without>, + sprites: Query<(&Sprite, &Transform), Without>, mut drag: ResMut, + mut commands: Commands ) { if ev.button != PointerButton::Primary { return; } - let Ok(_) = sprites.get(ev.target()) else { + let Ok(sprite) = sprites.get(ev.target()) else { return; }; + let mut s = sprite.0.clone(); + s.color = Color::srgba(0.7, 0.7, 0.7, 1.0); + commands.spawn(( + DragGhost, + sprite.1.clone(), + s + )); + drag.0 = Some(ev.target()); } @@ -75,17 +90,74 @@ fn on_part_release( mut drag: ResMut, mut events: EventWriter, cursor: Res, + mut commands: Commands, + ghosts: Query>, ) { if ev.button != PointerButton::Primary { return; } - if let Some(e) = drag.0 - && let Some(c) = cursor.0 - { - debug!(?e, ?c, "sending drag request"); - events.write(DragRequestEvent(e, c)); + if let Some(e) = drag.0 { + for ghost in &ghosts { + commands.entity(ghost).despawn(); + } + + if let Some(c) = cursor.0 { + debug!(?e, ?c, "sending drag request"); + events.write(DragRequestEvent(e, c)); + } } drag.0 = None; } +fn update_drag_ghosts( + mut ghost: Single<&mut Transform, (With, Without, Without, Without)>, + cursor: Res, + snaps: Query<(&Transform, &SnapOfJoint, &SnapOf)>, + joints: Query<(&Transform, &JointOf)>, + parts: Query<&GlobalTransform, With>, + debug: Res, + mut gizmos: Gizmos, +) { + let Some(cursor) = cursor.0 else { return }; + + const CUTOFF: f32 = 25.0; // px + + let mut best_distance = f32::INFINITY; + let mut best_target = Transform::from_xyz(cursor.x, cursor.y, 0.0); + + for (snap_local_transform, snap_joint, snap_part) in &snaps { + let Ok(parent_position) = parts.get(snap_part.0) else { continue; }; + let snap_global_translation = parent_position.transform_point(snap_local_transform.translation).xy(); + + let distance_to_cursor = cursor.distance(snap_global_translation); + + if distance_to_cursor > best_distance { + if debug.0 {gizmos.circle_2d(snap_global_translation, 3.0, RED); } + continue; + } + if distance_to_cursor > CUTOFF { + if debug.0 {gizmos.circle_2d(snap_global_translation, 3.0, ORANGE); } + continue; + } + + if debug.0 { gizmos.circle_2d(snap_global_translation, 3.0, GREEN); } + + let Ok((offset, parent)) = joints.get(snap_joint.0) else { continue; }; + let Ok(parent_pos) = parts.get(parent.0) else { continue; }; + + let joint_target = parent_pos.transform_point(offset.translation); + + if debug.0 { gizmos.circle_2d(joint_target.xy(), 3.0, GREEN); } + + let target_transform = Transform { + translation: joint_target, + rotation: parent_position.rotation().mul_quat(offset.rotation), + scale: offset.scale, + }; + best_distance = distance_to_cursor; + best_target = target_transform; + } + + **ghost = best_target; +} \ No newline at end of file diff --git a/crates/unified/src/config/part.rs b/crates/unified/src/config/part.rs index b01382ec8d6cf8649aa9ae5c30e39eedfef2c48b..3c8a38fbad7c5ddc3fe587251c552bc38b2fd578 100644 --- a/crates/unified/src/config/part.rs +++ b/crates/unified/src/config/part.rs @@ -36,7 +36,7 @@ impl From for Transform { fn from(value: JointOffset) -> Self { Transform { translation: value.translation, - rotation: Quat::from_rotation_z(value.rotation), + rotation: Quat::from_rotation_z(value.rotation.to_radians()), ..Default::default() } } diff --git a/crates/unified/src/server/player.rs b/crates/unified/src/server/player.rs index d3d75fa0373e9a73f4bfe6858189fad724e8828a..269ecd8971dfc81f1cadeb4cdec8a8ae785ec448 100644 --- a/crates/unified/src/server/player.rs +++ b/crates/unified/src/server/player.rs @@ -10,6 +10,7 @@ use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity}; use bevy::prelude::*; use bevy_rapier2d::prelude::ExternalForce; use bevy_replicon::prelude::FromClient; +use crate::attachment::PartInShip; use crate::server::system_sets::PlayerInputSet; pub fn player_management_plugin(app: &mut App) {