use std::f32::consts::{FRAC_PI_2, PI, TAU};
use bevy::{
input::{
mouse::{MouseMotion, MouseScrollUnit, MouseWheel},
ButtonInput,
},
math::{EulerRot, Quat, Vec2, Vec3},
prelude::{Bundle, Camera3dBundle, Component, DetectChanges, EventReader, KeyCode, Query, Res},
transform::components::Transform,
};
#[derive(Bundle, Default)]
pub struct PanOrbitCameraBundle {
pub camera: Camera3dBundle,
pub state: PanOrbitState,
pub settings: PanOrbitSettings,
}
#[derive(Component)]
pub struct PanOrbitState {
pub center: Vec3,
pub radius: f32,
pub upside_down: bool,
pub pitch: f32,
pub yaw: f32,
}
impl Default for PanOrbitState {
fn default() -> Self {
PanOrbitState {
center: Vec3::ZERO,
radius: 1.0,
upside_down: false,
pitch: 0.0,
yaw: 0.0,
}
}
}
#[derive(Component)]
pub struct PanOrbitSettings {
pub pan_sensitivity: f32,
pub orbit_sensitivity: f32,
pub zoom_sensitivity: f32,
pub pan_key: Option<KeyCode>,
pub orbit_key: Option<KeyCode>,
pub zoom_key: Option<KeyCode>,
pub scroll_action: Option<PanOrbitAction>,
pub scroll_line_sensitivity: f32,
pub scroll_pixel_sensitivity: f32,
pub auto_orbit: bool,
pub auto_orbit_factor: f32,
}
impl Default for PanOrbitSettings {
fn default() -> Self {
PanOrbitSettings {
pan_sensitivity: 0.001, orbit_sensitivity: 0.1f32.to_radians(), zoom_sensitivity: 0.01,
pan_key: Some(KeyCode::ControlLeft),
orbit_key: Some(KeyCode::AltLeft),
zoom_key: Some(KeyCode::ShiftLeft),
scroll_action: Some(PanOrbitAction::Zoom),
scroll_line_sensitivity: 16.0, scroll_pixel_sensitivity: 1.0,
auto_orbit: true,
auto_orbit_factor: 0.003,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PanOrbitAction {
Pan,
Orbit,
Zoom,
}
pub fn toggle_auto_orbit(mut pan_orbit_camera: Query<&mut PanOrbitSettings>) {
match pan_orbit_camera.get_single_mut() {
Ok(mut cam) => {
cam.auto_orbit = !cam.auto_orbit;
}
Err(_) => (),
}
}
pub fn update_pan_orbit_camera(
kbd: Res<ButtonInput<KeyCode>>,
mut evr_motion: EventReader<MouseMotion>,
mut evr_scroll: EventReader<MouseWheel>,
mut q_camera: Query<(&PanOrbitSettings, &mut PanOrbitState, &mut Transform)>,
) {
let mut total_motion: Vec2 = evr_motion.read().map(|ev| ev.delta).sum();
total_motion.y = -total_motion.y;
let mut total_scroll_lines = Vec2::ZERO;
let mut total_scroll_pixels = Vec2::ZERO;
for ev in evr_scroll.read() {
match ev.unit {
MouseScrollUnit::Line => {
total_scroll_lines.x += ev.x;
total_scroll_lines.y -= ev.y;
}
MouseScrollUnit::Pixel => {
total_scroll_pixels.x += ev.x;
total_scroll_pixels.y -= ev.y;
}
}
}
for (settings, mut state, mut transform) in &mut q_camera {
let mut total_pan = Vec2::ZERO;
if settings
.pan_key
.map(|key| kbd.pressed(key))
.unwrap_or(false)
{
total_pan -= total_motion * settings.pan_sensitivity;
}
if settings.scroll_action == Some(PanOrbitAction::Pan) {
total_pan -=
total_scroll_lines * settings.scroll_line_sensitivity * settings.pan_sensitivity;
total_pan -=
total_scroll_pixels * settings.scroll_pixel_sensitivity * settings.pan_sensitivity;
}
let mut total_orbit = Vec2::ZERO;
if settings
.orbit_key
.map(|key| kbd.pressed(key))
.unwrap_or(false)
{
total_orbit -= total_motion * settings.orbit_sensitivity;
}
if settings.scroll_action == Some(PanOrbitAction::Orbit) {
total_orbit -=
total_scroll_lines * settings.scroll_line_sensitivity * settings.orbit_sensitivity;
total_orbit -= total_scroll_pixels
* settings.scroll_pixel_sensitivity
* settings.orbit_sensitivity;
}
let mut total_zoom = Vec2::ZERO;
if settings
.zoom_key
.map(|key| kbd.pressed(key))
.unwrap_or(false)
{
total_zoom -= total_motion * settings.zoom_sensitivity;
}
if settings.scroll_action == Some(PanOrbitAction::Zoom) {
total_zoom -=
total_scroll_lines * settings.scroll_line_sensitivity * settings.zoom_sensitivity;
total_zoom -=
total_scroll_pixels * settings.scroll_pixel_sensitivity * settings.zoom_sensitivity;
}
if settings
.orbit_key
.map(|key| kbd.just_pressed(key))
.unwrap_or(false)
{
state.upside_down = state.pitch < -FRAC_PI_2 || state.pitch > FRAC_PI_2;
}
if state.upside_down {
total_orbit.x = -total_orbit.x;
}
let mut any_change = false;
if total_zoom != Vec2::ZERO {
any_change = true;
state.radius *= (-total_zoom.y).exp();
}
if total_pan != Vec2::ZERO {
any_change = true;
let radius = state.radius;
state.center += transform.right() * total_pan.x * radius;
state.center += transform.up() * total_pan.y * radius;
}
if total_orbit != Vec2::ZERO {
any_change = true;
}
if settings.auto_orbit && !any_change {
total_orbit.x += settings.auto_orbit_factor;
}
if total_orbit != Vec2::ZERO {
state.yaw += total_orbit.x;
state.pitch += total_orbit.y;
if state.yaw > PI {
state.yaw -= TAU; }
if state.yaw < -PI {
state.yaw += TAU; }
if state.pitch > PI {
state.pitch -= TAU; }
if state.pitch < -PI {
state.pitch += TAU; }
}
if settings.auto_orbit || any_change || state.is_added() {
transform.rotation = Quat::from_euler(EulerRot::YXZ, state.yaw, state.pitch, 0.0);
transform.translation = state.center + transform.back() * state.radius;
}
}
}