From 94336e646a19dd15e9b88289757a2057676450a5 Mon Sep 17 00:00:00 2001 From: ghostly_zsh Date: Thu, 18 Jun 2026 13:53:36 -0500 Subject: [PATCH] ship editor feat: attachment --- crates/unified/src/ship_editor/components.rs | 8 +- crates/unified/src/ship_editor/input.rs | 112 +++++++++++++++++-- crates/unified/src/ship_editor/mod.rs | 5 +- 3 files changed, 109 insertions(+), 16 deletions(-) diff --git a/crates/unified/src/ship_editor/components.rs b/crates/unified/src/ship_editor/components.rs index 5f6587e6d105561ecf6efc2d95a08a409cca87e1..0d6feedc7cc9de0b5f4964214117ee5d5e94b310 100644 --- a/crates/unified/src/ship_editor/components.rs +++ b/crates/unified/src/ship_editor/components.rs @@ -1,5 +1,5 @@ use bevy::camera::visibility::RenderLayers; -use crate::prelude::{Component, Handle, Resource}; +use crate::prelude::{Component, Handle, Entity, Resource}; use crate::shared::config::part::PartConfig; use crate::shared::config::ship_editor::ShipEditorConfig; @@ -8,6 +8,10 @@ pub const GHOST_RENDER_LAYER: RenderLayers = RenderLayers::layer(1); #[derive(Component)] pub struct GhostModule; +#[derive(Component)] +pub struct Part; +#[derive(Component)] +pub struct PartConfigHolder(pub PartConfig); #[derive(Component)] pub struct MainCamera; @@ -17,8 +21,6 @@ pub struct GhostCamera; pub struct PlayerPartRequest; #[derive(Component)] pub struct SpawnPartRequest(pub Handle); -#[derive(Component)] -pub struct Part(pub PartConfig); #[derive(Resource, Default)] pub struct ShipEditorConfigHolder { diff --git a/crates/unified/src/ship_editor/input.rs b/crates/unified/src/ship_editor/input.rs index 623c0695efd78785664f7ff29149cc1311099f46..99f91eb47b598e4ff7ce7e1512c27d76832a9e72 100644 --- a/crates/unified/src/ship_editor/input.rs +++ b/crates/unified/src/ship_editor/input.rs @@ -1,10 +1,15 @@ +use std::f64::consts::PI; +use avian2d::physics_transform::{Position, Rotation}; +use avian2d::prelude::{AngularVelocity, LinearVelocity}; +use bevy::camera::visibility::RenderLayers; use bevy::input::mouse::{MouseScrollUnit, MouseWheel}; use bevy::input_focus::InputFocus; use bevy::prelude::*; use bevy::window::PrimaryWindow; 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::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::spawn_joints; use crate::ship_editor::ui::PartEntry; @@ -24,6 +29,8 @@ struct ShipEditorDrag { is_holding_module: bool, init_cursor_pos: Vec2, init_camera_pos: Vec2, + target: Option, + peer: Option, } fn on_scroll( @@ -71,6 +78,21 @@ fn on_click( mut drag: ResMut, camera: Single<&Transform, (With, With)>, window: Single<&Window, With>, + mut ghost_module: Query<(Entity, &mut Transform, &mut Sprite, &Joints), (With, Without)>, + snaps: Query<(&SnapOf, &SnapOfJoint)>, + joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity), (Without, Without)>, + mut parts: Query< + ( + &mut Transform, + Option<&PartInShip>, + Entity, + &Joints, + &Part, + ), + (Without, Without, Without), + >, + asset_server: Res, + mut commands: Commands, ) { let Some(cursor_pos) = window.cursor_position() else { return }; if ev.just_pressed(MouseButton::Left) { @@ -80,6 +102,63 @@ fn on_click( } if ev.just_released(MouseButton::Left) { drag.is_dragging = false; + + if let Some(snap_target) = drag.target && let Some(peer_snap) = drag.peer { + let Ok((ghost_entity, mut ghost_transform, mut ghost_sprite, ghost_joints)) = ghost_module.single_mut() else { return }; + let Ok((target_snap_part, target_snap_joint)) = snaps.get(snap_target) else { + return; + }; + let Ok((source_snap_part, source_snap_joint)) = snaps.get(peer_snap) else { + return; + }; + + let Ok((target_jt, target_jt_of, target_jt_xform, target_jt_peer, target_jt_id)) = joints.get(target_snap_joint.0) else { + return; + }; + let Ok((source_jt, source_jt_of, _source_jt_xform, source_jt_peer, source_jt_id)) = joints.get(source_snap_joint.0) else { + return; + }; + + if target_jt_peer.is_some() { + warn!("drag request: cannot attach to a joint that already has a peer, ignoring request"); + return; + } + if source_jt_peer.is_some() { + warn!("drag request: dragging from a part that is already attached is currently not supported, ignoring request"); + return; + } + + let Ok((target_xform, target_in_ship, target_entity, _, _)) = parts.get(target_jt_of.0) else { + return; + }; + + // attached (housing) + /*let Ok((_, _, source_entity, source_joints, _)) = parts.get(source_jt_of.0) else { + return; + };*/ + + commands.entity(source_jt_id).insert(Peer { + peer_joint_entity_id: target_jt_id, + physics_joint: Entity::PLACEHOLDER, // reusing components and physics joint is not used in the ship editor + }); + commands.entity(target_jt_id).insert(Peer { + peer_joint_entity_id: source_jt_id, + physics_joint: Entity::PLACEHOLDER, + }); + let target_position = target_xform.mul_transform(*target_jt_xform); + ghost_transform.translation = target_position.translation; + ghost_transform.rotation = target_position.rotation + * source_jt.transform.rotation.inverse() + * Quat::from_rotation_z(PI as f32); + + debug!("spawning part"); + ghost_sprite.color = Color::srgb(1.0, 1.0, 1.0); + commands.entity(ghost_entity) + .insert(Part) + .insert(MAIN_LAYER) + .remove::() + .remove::(); + } } } @@ -100,13 +179,13 @@ fn camera_drag( ghost_transform.translation = main_transform.translation; } fn ghost_drag( - drag: Res, + mut drag: ResMut, window: Single<&Window, 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>, + joints: Query<(&Transform, &JointOf, Entity), (Without, Without)>, parts: Query<&GlobalTransform, (Or<(With, With)>)>, ) { const SNAP_CUTOFF: f32 = 25.0; @@ -124,8 +203,10 @@ fn ghost_drag( return }; - let mut closest_snap = f32::INFINITY; + let mut closest_distance = f32::INFINITY; let mut closest_snap_transform = None; + let mut closest_snap = None; + let mut closest_ghost_snap = None; for (snap_local_transform, snap_joint, snap_part, snap_id) in snaps { if snap_part.0 == ghost_entity { continue } @@ -135,7 +216,7 @@ fn ghost_drag( let distance_to_cursor = global_cursor_position.distance(snap_global_translation.translation().xy()); - if distance_to_cursor > closest_snap { + if distance_to_cursor > closest_distance { continue; } if distance_to_cursor > SNAP_CUTOFF { @@ -185,14 +266,18 @@ fn ghost_drag( 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_distance = distance_to_cursor; closest_snap_transform = Some(target_transform); + closest_snap = Some(snap_id); + closest_ghost_snap = closest_peer_snap; } if let Some(transform) = closest_snap_transform { *ghost_transform = transform; } else { ghost_transform.translation = ghost_position.extend(0.0); } + drag.target = closest_snap; + drag.peer = closest_ghost_snap; } fn part_button_interaction( @@ -213,16 +298,20 @@ fn part_button_interaction( Interaction::None => { if !mouse_ev.pressed(MouseButton::Left) { drag.is_holding_module = false; - for ghost_entity in ghost_part_query.iter() { - commands.entity(ghost_entity).despawn(); + if drag.target.is_none() { + for ghost_entity in ghost_part_query.iter() { + commands.entity(ghost_entity).despawn(); + } } } } Interaction::Hovered => { if !mouse_ev.pressed(MouseButton::Left) { drag.is_holding_module = false; - for ghost_entity in ghost_part_query.iter() { - commands.entity(ghost_entity).despawn(); + if drag.target.is_none() { + for ghost_entity in ghost_part_query.iter() { + commands.entity(ghost_entity).despawn(); + } } } } @@ -242,6 +331,7 @@ fn part_button_interaction( sprite, Transform::from_translation(ghost_position.extend(0.0)), GhostModule, + PartConfigHolder(part_entry.0.clone()), 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 29fff4c7680f2578813e063bc14a240dc86b3123..fd23b4546dd02c227780de423f40d983fc9524ec 100644 --- a/crates/unified/src/ship_editor/mod.rs +++ b/crates/unified/src/ship_editor/mod.rs @@ -9,7 +9,7 @@ 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, PlayerPartRequest, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; +use crate::ship_editor::components::{GhostCamera, MainCamera, Part, PartConfigHolder, PlayerPartRequest, ShipEditorConfigHolder, SpawnPartRequest, GHOST_RENDER_LAYER, MAIN_RENDER_LAYER}; use crate::ship_editor::input::input_plugin; use crate::ship_editor::ui::{ui_plugin, PendingPart}; @@ -89,7 +89,8 @@ fn spawn_parts( commands.entity(entity) .insert(sprite) - .insert(Part(strong_part_config.clone())) + .insert(Part) + .insert(PartConfigHolder(strong_part_config.clone())) .remove::(); debug!("spawned part"); spawn_joints(strong_part_config, entity, commands.reborrow());