use bevy::input::mouse::MouseButton; use bevy::prelude::*; use bevy::window::PrimaryWindow; use crate::ui::util::cursor_over_ui; #[derive(Component)] pub struct MainCamera; /// Orbit-style camera: rotates around `target` at `distance`, controlled by mouse. /// Used for inspection scenes like GalaxyCreation where there is no player to follow. #[derive(Component, Debug, Clone, Copy)] pub struct OrbitCamera { pub target: Vec3, pub distance: f32, /// Yaw around the Y axis (radians). 0 = camera at +Z looking toward origin. pub yaw: f32, /// Pitch above the horizontal plane (radians). 0 = horizontal, π/2 = straight down. pub pitch: f32, } impl Default for OrbitCamera { fn default() -> Self { Self { target: Vec3::ZERO, distance: 420.0, yaw: 0.0, // ~36° above horizontal — roughly matches docs GalaxyScene camera position. pitch: 0.625, } } } /// Initial camera spawn. The same entity is reused across states; control systems /// decide how it moves depending on which state is active. pub fn spawn_camera(mut commands: Commands) { commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 260.0, 360.0).looking_at(Vec3::ZERO, Vec3::Y), MainCamera, OrbitCamera::default(), )); } const ORBIT_ROTATE_SENSITIVITY: f32 = 0.005; const ORBIT_ZOOM_SENSITIVITY: f32 = 0.1; const ORBIT_MIN_PITCH: f32 = 0.15; // ~9° above horizontal — never fully edge-on const ORBIT_MAX_PITCH: f32 = 1.4; // ~80° — never straight down (gimbal safeguard) const ORBIT_MIN_DISTANCE: f32 = 40.0; const ORBIT_MAX_DISTANCE: f32 = 1500.0; /// Left-drag rotates around the orbit target; scroll wheel zooms. /// /// Drag is suppressed when the cursor is over any UI node — otherwise clicking /// buttons or panels would also rotate the camera. pub fn orbit_camera_control( mouse_input: Res>, primary_window: Query<&Window, With>, mut mouse_motion: EventReader, mut scroll_events: EventReader, mut query: Query<(&mut Transform, &mut OrbitCamera), With>, ui_nodes: Query<(&bevy::ui::ComputedNode, &GlobalTransform)>, ) { let Ok((mut transform, mut orbit)) = query.single_mut() else { // Drain pending input to avoid stale buildup when there's no camera. mouse_motion.clear(); scroll_events.clear(); return; }; let cursor_over_ui = primary_window .single() .ok() .map(|w| cursor_over_ui(w, &ui_nodes)) .unwrap_or(false); if mouse_input.pressed(MouseButton::Left) && !cursor_over_ui { for event in mouse_motion.read() { orbit.yaw -= event.delta.x * ORBIT_ROTATE_SENSITIVITY; // Vertical is flipped relative to a "trackball" feel so the content // follows the finger (iPhone-style): drag down → camera rises → // scene appears to shift down with the cursor. orbit.pitch += event.delta.y * ORBIT_ROTATE_SENSITIVITY; } } else { mouse_motion.clear(); } for event in scroll_events.read() { // Scroll up (positive y) → decrease distance (zoom in). orbit.distance *= 1.0 - event.y * ORBIT_ZOOM_SENSITIVITY; } orbit.pitch = orbit.pitch.clamp(ORBIT_MIN_PITCH, ORBIT_MAX_PITCH); orbit.distance = orbit.distance.clamp(ORBIT_MIN_DISTANCE, ORBIT_MAX_DISTANCE); let cos_p = orbit.pitch.cos(); let pos = orbit.target + Vec3::new( orbit.distance * cos_p * orbit.yaw.sin(), orbit.distance * orbit.pitch.sin(), orbit.distance * cos_p * orbit.yaw.cos(), ); *transform = Transform::from_translation(pos).looking_at(orbit.target, Vec3::Y); }