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::{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; pub fn input_plugin(app: &mut App) { app .insert_resource(ShipEditorDrag::default()) .add_systems(Update, on_scroll) .add_systems(Update, on_click) .add_systems(Update, camera_drag) .add_systems(Update, ghost_drag) .add_systems(Update, part_button_interaction); } #[derive(Resource, Default)] struct ShipEditorDrag { is_dragging: bool, is_holding_module: bool, init_cursor_pos: Vec2, init_camera_pos: Vec2, target: Option, peer: Option, } fn on_scroll( mut scroll_events: MessageReader, 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 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 *= main_projection.scale; main_projection.scale *= 0.95; ghost_projection.scale = main_projection.scale; let scaled_rel_pos = rel_pos * 0.95; 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 *= main_projection.scale; main_projection.scale *= 1.05; ghost_projection.scale = main_projection.scale; let scaled_rel_pos = rel_pos * 1.05; main_transform.translation += scaled_rel_pos.extend(0.0) - rel_pos.extend(0.0); ghost_transform.translation = main_transform.translation; } } } } *main_camera.0 = main_transform; *ghost_camera.0 = ghost_transform; } fn on_click( ev: Res>, mut drag: ResMut, camera: Single<&Transform, (With, With)>, window: Single<&Window, With>, mut ghost_module: Query<(Entity, &mut Transform, &mut Sprite, &Joints, &PartConfigHolder), (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) { drag.is_dragging = true; drag.init_cursor_pos = cursor_pos.with_y(-cursor_pos.y); drag.init_camera_pos = camera.translation.truncate(); } 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_config)) = 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.image = asset_server.load(ghost_config.0.part.sprite_connected.clone()); ghost_sprite.color = Color::srgb(1.0, 1.0, 1.0); commands.entity(ghost_entity) .insert(Part) .insert(MAIN_LAYER) .remove::() .remove::(); } } } fn camera_drag( drag: ResMut, 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 = main_camera.1.clone(); let Projection::Orthographic(projection) = projection else { return }; let main_transform = &mut main_camera.0; let ghost_transform = &mut ghost_camera.0; 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( 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, 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, 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_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 } 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_distance { continue; } if distance_to_cursor > SNAP_CUTOFF { continue; } 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_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( mouse_ev: Res>, part_button_query: Query<(Entity, &PartEntry, &Interaction), (With