Files
Space-Game/apps/game/src/camera.rs
francy51 1852cafcac Flip orbit camera vertical drag to iPhone-style
Drag down now raises the camera so the galaxy shifts down with the cursor
(content follows finger). Horizontal was already correct (drag right moves
the scene right). Matches the feel of panning a map on iOS.
2026-06-04 12:33:04 -04:00

106 lines
3.8 KiB
Rust

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<ButtonInput<MouseButton>>,
primary_window: Query<&Window, With<PrimaryWindow>>,
mut mouse_motion: EventReader<bevy::input::mouse::MouseMotion>,
mut scroll_events: EventReader<bevy::input::mouse::MouseWheel>,
mut query: Query<(&mut Transform, &mut OrbitCamera), With<MainCamera>>,
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);
}