From 9e70dffdd8c869d93630442f61801a7d4b56ee30 Mon Sep 17 00:00:00 2001 From: core Date: Fri, 11 Jul 2025 14:43:13 -0400 Subject: [PATCH] feat: proper part rotation --- .../assets/config/parts/chassis.part.toml | 14 ++--- .../assets/config/parts/hearty.part.toml | 14 ++--- crates/unified/src/attachment.rs | 14 +++-- crates/unified/src/client/parts.rs | 58 ++++++++++++++++--- crates/unified/src/server/part.rs | 48 ++++++++++++++- 5 files changed, 119 insertions(+), 29 deletions(-) diff --git a/crates/unified/assets/config/parts/chassis.part.toml b/crates/unified/assets/config/parts/chassis.part.toml index 70a5e462af49b3a9a03782cad20c626a1f9ffa82..7b13b92f0546d818767a6036374cbe967bca93bf 100644 --- a/crates/unified/assets/config/parts/chassis.part.toml +++ b/crates/unified/assets/config/parts/chassis.part.toml @@ -10,21 +10,21 @@ mass = 100 [[joints]] id = "Top" -target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 0.0 } +target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 180.0 } snap = { translation = [ 0.0, 25.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Right" -target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 90.0 } -snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 90.0 } +target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 270.0 } +snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Bottom" -target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 180.0 } -snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 180.0 } +target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 0.0 } +snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Left" -target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 270.0 } -snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 270.0 } \ No newline at end of file +target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 90.0 } +snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 0.0 } \ No newline at end of file diff --git a/crates/unified/assets/config/parts/hearty.part.toml b/crates/unified/assets/config/parts/hearty.part.toml index d3057597d1f0a47508b3912fb0a16e71036af55e..f361efdfb1a8a87f030efb4d60195ff851eb6dc2 100644 --- a/crates/unified/assets/config/parts/hearty.part.toml +++ b/crates/unified/assets/config/parts/hearty.part.toml @@ -14,21 +14,21 @@ exhaust_speed = 250 [[joints]] id = "Top" -target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 0.0 } +target = { translation = [ 0.0, 50.0, 0.0 ], rotation = 180.0 } snap = { translation = [ 0.0, 25.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Right" -target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 90.0 } -snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 90.0 } +target = { translation = [ 50.0, 0.0, 0.0 ], rotation = 270.0 } +snap = { translation = [ 25.0, 0.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Bottom" -target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 180.0 } -snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 180.0 } +target = { translation = [ 0.0, -50.0, 0.0 ], rotation = 0.0 } +snap = { translation = [ 0.0, -25.0, 0.0 ], rotation = 0.0 } [[joints]] id = "Left" -target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 270.0 } -snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 270.0 } +target = { translation = [ -50.0, 0.0, 0.0 ], rotation = 90.0 } +snap = { translation = [ -25.0, 0.0, 0.0 ], rotation = 0.0 } \ No newline at end of file diff --git a/crates/unified/src/attachment.rs b/crates/unified/src/attachment.rs index 2bd998c87a213fed8bae9fb7a3e046ee164ac879..4ac038b40e7ee2d8a4f97fe8ca048ca7e02ed4c4 100644 --- a/crates/unified/src/attachment.rs +++ b/crates/unified/src/attachment.rs @@ -22,7 +22,7 @@ pub struct Joint { pub transform: Transform, } #[derive(Component, Serialize, Deserialize, MapEntities)] -pub struct Peer(#[entities] Entity); +pub struct Peer(#[entities] pub Entity); #[derive(Component, Serialize, Deserialize, MapEntities)] #[relationship(relationship_target = Joints)] @@ -30,6 +30,12 @@ pub struct JointOf(#[entities] pub Entity); #[derive(Component, Serialize, Deserialize, MapEntities)] #[relationship_target(relationship = JointOf)] pub struct Joints(#[entities] Vec); +impl Deref for Joints { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} #[derive(Component, Serialize, Deserialize, MapEntities)] #[relationship(relationship_target = Snaps)] pub struct SnapOf(#[entities] pub Entity); @@ -44,12 +50,12 @@ impl Deref for Snaps { } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct JointId(pub String); impl JointId { #[must_use] - pub fn from_part_and_joint_id(part: String, joint: String) -> Self { - Self(format!("{part}:{joint}")) + pub fn from_part_and_joint_id(part: impl AsRef, joint: impl AsRef) -> Self { + Self(format!("{}:{}", part.as_ref(), joint.as_ref())) } } diff --git a/crates/unified/src/client/parts.rs b/crates/unified/src/client/parts.rs index 72ced353de1431c1daadc30d899146e3445e0b1d..7db4502121476b5ac9c27be852ec734bbc7c7004 100644 --- a/crates/unified/src/client/parts.rs +++ b/crates/unified/src/client/parts.rs @@ -3,7 +3,7 @@ use crate::client::Me; use crate::client::colors::GREEN; use crate::client::key_input::AttachmentDebugRes; use crate::ecs::{CursorWorldCoordinates, DragRequestEvent, Part}; -use bevy::color::palettes::css::{ORANGE, RED}; +use bevy::color::palettes::css::{ORANGE, PURPLE, RED, YELLOW}; use bevy::prelude::*; use bevy_rapier2d::dynamics::MassProperties; use bevy_rapier2d::prelude::AdditionalMassProperties; @@ -173,6 +173,11 @@ fn update_drag_ghosts( let mut best_target = Transform::from_xyz(cursor.x, cursor.y, 0.0); best_target.rotation = ghost_target.0; let mut snap = None; + let mut best_parent_position = None; + let mut best_snap_position = None; + let mut best_self_snap = None; + let mut best_self_snap_position = None; + let mut best_self_position = None; for (snap_local_transform, snap_joint, snap_part, snap_id) in &snaps { if Some(snap_part.0) == drag.0 { @@ -191,26 +196,25 @@ fn update_drag_ghosts( } let snap_global_translation = parent_position - .transform_point(snap_local_transform.translation) - .xy(); + .mul_transform(*snap_local_transform); - let distance_to_cursor = cursor.distance(snap_global_translation); + let distance_to_cursor = cursor.distance(snap_global_translation.translation().xy()); if distance_to_cursor > best_distance { if debug.0 { - gizmos.circle_2d(snap_global_translation, 3.0, RED); + gizmos.circle_2d(snap_global_translation.translation().xy(), 3.0, RED); } continue; } if distance_to_cursor > CUTOFF { if debug.0 { - gizmos.circle_2d(snap_global_translation, 3.0, ORANGE); + gizmos.circle_2d(snap_global_translation.translation().xy(), 3.0, ORANGE); } continue; } if debug.0 { - gizmos.circle_2d(snap_global_translation, 3.0, GREEN); + gizmos.circle_2d(snap_global_translation.translation().xy(), 3.0, GREEN); } let Ok((offset, _, _)) = joints.get(snap_joint.0) else { @@ -223,17 +227,53 @@ fn update_drag_ghosts( gizmos.circle_2d(joint_target.xy(), 3.0, GREEN); } + snap = Some(snap_id); let target_transform = Transform { translation: joint_target, - rotation: ghost_target.0, + rotation: ghost.rotation, scale: offset.scale, }; best_distance = distance_to_cursor; best_target = target_transform; + best_parent_position = Some(parent_position.translation().xy()); + best_snap_position = Some(snap_global_translation); + + // find, in this particular situation, the best peer on ourselves + let mut best_peer_snap_distance = f32::INFINITY; + let mut best_peer_snap = None; + let mut best_peer_snap_pos = None; + let mut best_peer_target_pos = None; + let mut best_joint_transform = None; + for (our_snap_local_transform, our_snap_joint, our_snap_part, our_snap_id) in &snaps { + if Some(our_snap_part.0) != drag.0 { continue; } + let our_snap_global_translation = best_target + .mul_transform(*our_snap_local_transform); + let distance = our_snap_global_translation.translation.distance(snap_global_translation.translation()); + if distance > best_peer_snap_distance { continue; } + + let Ok((our_joint, _, _)) = joints.get(our_snap_joint.0) else { continue }; + + best_peer_snap = Some(our_snap_id); + best_peer_snap_distance = distance; + best_peer_snap_pos = Some(our_snap_global_translation.translation.xy()); + best_peer_target_pos = Some(best_target.translation.xy()); + best_joint_transform = Some(our_joint); + } + if let Some(our_joint) = best_joint_transform { + best_target.rotation = parent_position.rotation().mul_quat(our_joint.rotation); + } - snap = Some(snap_id); + best_self_snap = best_peer_snap; + best_self_snap_position = best_peer_snap_pos; + best_self_position = best_peer_target_pos; } + if debug.0 && let Some(part) = best_parent_position && let Some(snap) = best_snap_position { + gizmos.arrow_2d(part, snap.translation().xy(), YELLOW); + } + if debug.0 && let Some(part) = best_self_position && let Some(snap) = best_self_snap_position { + gizmos.arrow_2d(part, snap, PURPLE); + } if keys.pressed(KeyCode::KeyQ) { best_target.rotation = best_target.rotation.mul_quat(Quat::from_rotation_z( diff --git a/crates/unified/src/server/part.rs b/crates/unified/src/server/part.rs index 141fa8a9a454847a658b592ff275c7d1ed49fef6..426aba69ae3383c4742e807045174d6bfd5b5c90 100644 --- a/crates/unified/src/server/part.rs +++ b/crates/unified/src/server/part.rs @@ -1,4 +1,5 @@ -use crate::attachment::{Joint, JointId, JointOf, SnapOf, SnapOfJoint}; +use std::cmp::PartialEq; +use crate::attachment::{Joint, JointId, JointOf, Joints, Peer, SnapOf, SnapOfJoint}; use crate::config::part::{JointConfig, PartConfig}; use crate::ecs::{Part, PartHandle}; use bevy::prelude::Component; @@ -36,8 +37,11 @@ fn handle_ready_parts( } } } + fn handle_part_reloading( - existing_parts: Query<(Entity, &PartHandle)>, + existing_parts: Query<(Entity, &PartHandle, &Joints)>, + joints: Query<(&mut Joint, Option<&Peer>, Entity)>, + snaps: Query<(Entity, &SnapOfJoint)>, assets: Res>, mut asset_events: EventReader>, mut commands: Commands, @@ -50,6 +54,46 @@ fn handle_part_reloading( commands .entity(existing_part.0) .insert(calculate_bundle(config, &existing_part.1.0)); + // update all joints + let mut used_joints = vec![]; + for joint_id in &**existing_part.2 { + // find in config + let Ok((joint, peer, _)) = joints.get(*joint_id) else { continue }; + let joint_cfg = config.joints.iter().find(|u| joint.id == JointId::from_part_and_joint_id(&config.part.name, &u.id)); + let Some(joint_cfg) = joint_cfg else { + if let Some(peer_id) = peer { + if let Ok(peer) = joints.get(peer_id.0) { + commands.entity(peer.2).remove::(); + } + } + commands.entity(*joint_id).despawn(); + for snap in &snaps { + if snap.1.0 == *joint_id { + commands.entity(snap.0).despawn(); + } + } + continue; + }; + used_joints.push(joint.id.clone()); + commands + .entity(*joint_id) + .insert(spawn_joint_bundle(joint_cfg, config, &existing_part.0)); + // annihilate all snaps then respawn + for snap in &snaps { + if snap.1.0 == *joint_id { + commands.entity(snap.0).despawn(); + } + } + commands.spawn(spawn_snap_bundle(joint_cfg, &existing_part.0, joint_id)); + } + for joint in &config.joints { + let id = JointId::from_part_and_joint_id(&config.part.name, &joint.id); + if used_joints.contains(&id) { continue }; + let joint_id = commands + .spawn(spawn_joint_bundle(joint, config, &existing_part.0)) + .id(); + commands.spawn(spawn_snap_bundle(joint, &existing_part.0, &joint_id)); + } } } }