#![warn(missing_docs)]
#![doc = include_str!("../README.md")]
use std::f32::consts::{PI, TAU};
use bevy::input::mouse::MouseWheel;
use bevy::prelude::*;
use bevy::render::camera::RenderTarget;
use bevy::window::{PrimaryWindow, WindowRef};
#[cfg(feature = "bevy_egui")]
use bevy_egui::EguiSet;
#[cfg(feature = "bevy_egui")]
pub use crate::egui::EguiWantsFocus;
use crate::input::{mouse_key_tracker, MouseKeyTracker};
pub use crate::touch::TouchControls;
use crate::touch::{touch_tracker, TouchGestures, TouchTracker};
use crate::traits::OptionalClamp;
#[cfg(feature = "bevy_egui")]
mod egui;
mod input;
mod touch;
mod traits;
mod util;
pub struct PanOrbitCameraPlugin;
impl Plugin for PanOrbitCameraPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ActiveCameraData>()
.init_resource::<MouseKeyTracker>()
.init_resource::<TouchTracker>()
.add_systems(
Update,
(
(
active_viewport_data
.run_if(|active_cam: Res<ActiveCameraData>| !active_cam.manual),
mouse_key_tracker,
touch_tracker,
),
pan_orbit_camera,
)
.chain()
.in_set(PanOrbitCameraSystemSet),
);
#[cfg(feature = "bevy_egui")]
{
app.init_resource::<EguiWantsFocus>()
.add_systems(
Update,
egui::check_egui_wants_focus
.after(EguiSet::InitContexts)
.before(PanOrbitCameraSystemSet),
)
.configure_sets(
Update,
PanOrbitCameraSystemSet.run_if(resource_equals(EguiWantsFocus {
prev: false,
curr: false,
})),
);
}
}
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub struct PanOrbitCameraSystemSet;
#[derive(Component, Copy, Clone, Debug, PartialEq)]
pub struct PanOrbitCamera {
pub focus: Vec3,
pub radius: Option<f32>,
pub alpha: Option<f32>,
pub beta: Option<f32>,
pub target_focus: Vec3,
pub target_alpha: f32,
pub target_beta: f32,
pub target_radius: f32,
pub alpha_upper_limit: Option<f32>,
pub alpha_lower_limit: Option<f32>,
pub beta_upper_limit: Option<f32>,
pub beta_lower_limit: Option<f32>,
pub zoom_upper_limit: Option<f32>,
pub zoom_lower_limit: Option<f32>,
pub orbit_sensitivity: f32,
pub orbit_smoothness: f32,
pub pan_sensitivity: f32,
pub pan_smoothness: f32,
pub zoom_sensitivity: f32,
pub zoom_smoothness: f32,
pub button_orbit: MouseButton,
pub button_pan: MouseButton,
pub modifier_orbit: Option<KeyCode>,
pub modifier_pan: Option<KeyCode>,
pub touch_enabled: bool,
pub touch_controls: TouchControls,
pub reversed_zoom: bool,
pub is_upside_down: bool,
pub allow_upside_down: bool,
pub enabled: bool,
pub initialized: bool,
pub force_update: bool,
}
impl Default for PanOrbitCamera {
fn default() -> Self {
PanOrbitCamera {
focus: Vec3::ZERO,
target_focus: Vec3::ZERO,
radius: None,
is_upside_down: false,
allow_upside_down: false,
orbit_sensitivity: 1.0,
orbit_smoothness: 0.1,
pan_sensitivity: 1.0,
pan_smoothness: 0.02,
zoom_sensitivity: 1.0,
zoom_smoothness: 0.1,
button_orbit: MouseButton::Left,
button_pan: MouseButton::Right,
modifier_orbit: None,
modifier_pan: None,
touch_enabled: true,
touch_controls: TouchControls::OneFingerOrbit,
reversed_zoom: false,
enabled: true,
alpha: None,
beta: None,
target_alpha: 0.0,
target_beta: 0.0,
target_radius: 1.0,
initialized: false,
alpha_upper_limit: None,
alpha_lower_limit: None,
beta_upper_limit: None,
beta_lower_limit: None,
zoom_upper_limit: None,
zoom_lower_limit: None,
force_update: false,
}
}
}
#[derive(Resource, Default, Debug, PartialEq)]
pub struct ActiveCameraData {
pub entity: Option<Entity>,
pub viewport_size: Option<Vec2>,
pub window_size: Option<Vec2>,
pub manual: bool,
}
#[allow(clippy::too_many_arguments)]
fn active_viewport_data(
mut active_cam: ResMut<ActiveCameraData>,
mouse_input: Res<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
scroll_events: EventReader<MouseWheel>,
touches: Res<Touches>,
primary_windows: Query<&Window, With<PrimaryWindow>>,
other_windows: Query<&Window, Without<PrimaryWindow>>,
orbit_cameras: Query<(Entity, &Camera, &PanOrbitCamera)>,
) {
let mut new_resource = ActiveCameraData::default();
let mut max_cam_order = 0;
let mut has_input = false;
for (entity, camera, pan_orbit) in orbit_cameras.iter() {
let input_just_activated = input::orbit_just_pressed(pan_orbit, &mouse_input, &key_input)
|| input::pan_just_pressed(pan_orbit, &mouse_input, &key_input)
|| !scroll_events.is_empty()
|| (touches.iter_just_pressed().count() > 0
&& touches.iter_just_pressed().count() == touches.iter().count());
if input_just_activated {
has_input = true;
if let RenderTarget::Window(win_ref) = camera.target {
let Some(window) = (match win_ref {
WindowRef::Primary => primary_windows.get_single().ok(),
WindowRef::Entity(entity) => other_windows.get(entity).ok(),
}) else {
continue;
};
if let Some(input_position) = window.cursor_position().or(touches
.iter_just_pressed()
.collect::<Vec<_>>()
.first()
.map(|touch| touch.position()))
{
if let Some(Rect { min, max }) = camera.logical_viewport_rect() {
let cursor_in_vp = input_position.x > min.x
&& input_position.x < max.x
&& input_position.y > min.y
&& input_position.y < max.y;
if cursor_in_vp && camera.order >= max_cam_order {
new_resource = ActiveCameraData {
entity: Some(entity),
viewport_size: camera.logical_viewport_size(),
window_size: Some(Vec2::new(window.width(), window.height())),
manual: false,
};
max_cam_order = camera.order;
}
}
}
}
}
}
if has_input {
active_cam.set_if_neq(new_resource);
}
}
fn pan_orbit_camera(
active_cam: Res<ActiveCameraData>,
mouse_key_tracker: Res<MouseKeyTracker>,
touch_tracker: Res<TouchTracker>,
mut orbit_cameras: Query<(Entity, &mut PanOrbitCamera, &mut Transform, &mut Projection)>,
time: Res<Time>,
) {
for (entity, mut pan_orbit, mut transform, mut projection) in orbit_cameras.iter_mut() {
let apply_zoom_limits = {
let zoom_upper_limit = pan_orbit.zoom_upper_limit;
let zoom_lower_limit = pan_orbit.zoom_lower_limit;
move |zoom: f32| {
zoom.clamp_optional(zoom_lower_limit, zoom_upper_limit)
.max(0.05)
}
};
let apply_alpha_limits = {
let alpha_upper_limit = pan_orbit.alpha_upper_limit;
let alpha_lower_limit = pan_orbit.alpha_lower_limit;
move |alpha: f32| alpha.clamp_optional(alpha_lower_limit, alpha_upper_limit)
};
let apply_beta_limits = {
let beta_upper_limit = pan_orbit.beta_upper_limit;
let beta_lower_limit = pan_orbit.beta_lower_limit;
move |beta: f32| beta.clamp_optional(beta_lower_limit, beta_upper_limit)
};
if !pan_orbit.initialized {
let (alpha, beta, radius) =
util::calculate_from_translation_and_focus(transform.translation, pan_orbit.focus);
let &mut mut alpha = pan_orbit.alpha.get_or_insert(alpha);
let &mut mut beta = pan_orbit.beta.get_or_insert(beta);
let &mut mut radius = pan_orbit.radius.get_or_insert(radius);
alpha = apply_alpha_limits(alpha);
beta = apply_beta_limits(beta);
radius = apply_zoom_limits(radius);
pan_orbit.alpha = Some(alpha);
pan_orbit.beta = Some(beta);
pan_orbit.radius = Some(radius);
pan_orbit.target_alpha = alpha;
pan_orbit.target_beta = beta;
pan_orbit.target_radius = radius;
pan_orbit.target_focus = pan_orbit.focus;
util::update_orbit_transform(
alpha,
beta,
radius,
pan_orbit.focus,
&mut transform,
&mut projection,
);
pan_orbit.initialized = true;
}
let mut orbit = Vec2::ZERO;
let mut pan = Vec2::ZERO;
let mut scroll_line = 0.0;
let mut scroll_pixel = 0.0;
let mut orbit_button_changed = false;
if pan_orbit.enabled && active_cam.entity == Some(entity) {
let zoom_direction = match pan_orbit.reversed_zoom {
true => -1.0,
false => 1.0,
};
orbit = mouse_key_tracker.orbit * pan_orbit.orbit_sensitivity;
pan = mouse_key_tracker.pan * pan_orbit.pan_sensitivity;
scroll_line =
mouse_key_tracker.scroll_line * zoom_direction * pan_orbit.zoom_sensitivity;
scroll_pixel =
mouse_key_tracker.scroll_pixel * zoom_direction * pan_orbit.zoom_sensitivity;
orbit_button_changed = mouse_key_tracker.orbit_button_changed;
if pan_orbit.touch_enabled {
let (touch_orbit, touch_pan, touch_zoom_pixel) = match pan_orbit.touch_controls {
TouchControls::OneFingerOrbit => match touch_tracker.get_touch_gestures() {
TouchGestures::None => (Vec2::ZERO, Vec2::ZERO, 0.0),
TouchGestures::OneFinger(one_finger_gestures) => {
(one_finger_gestures.motion, Vec2::ZERO, 0.0)
}
TouchGestures::TwoFinger(two_finger_gestures) => (
Vec2::ZERO,
two_finger_gestures.motion,
two_finger_gestures.pinch * 0.015,
),
},
TouchControls::TwoFingerOrbit => match touch_tracker.get_touch_gestures() {
TouchGestures::None => (Vec2::ZERO, Vec2::ZERO, 0.0),
TouchGestures::OneFinger(one_finger_gestures) => {
(Vec2::ZERO, one_finger_gestures.motion, 0.0)
}
TouchGestures::TwoFinger(two_finger_gestures) => (
two_finger_gestures.motion,
Vec2::ZERO,
two_finger_gestures.pinch * 0.015,
),
},
};
orbit += touch_orbit * pan_orbit.orbit_sensitivity;
pan += touch_pan * pan_orbit.pan_sensitivity;
scroll_pixel += touch_zoom_pixel * zoom_direction * pan_orbit.zoom_sensitivity;
}
}
if orbit_button_changed {
let wrapped_beta = (pan_orbit.target_beta % TAU).abs();
pan_orbit.is_upside_down = wrapped_beta > TAU / 4.0 && wrapped_beta < 3.0 * TAU / 4.0;
}
let mut has_moved = false;
if orbit.length_squared() > 0.0 {
if let Some(win_size) = active_cam.window_size {
let delta_x = {
let delta = orbit.x / win_size.x * PI * 2.0;
if pan_orbit.is_upside_down {
-delta
} else {
delta
}
};
let delta_y = orbit.y / win_size.y * PI;
pan_orbit.target_alpha -= delta_x;
pan_orbit.target_beta += delta_y;
has_moved = true;
}
}
if pan.length_squared() > 0.0 {
if let Some(vp_size) = active_cam.viewport_size {
let mut multiplier = 1.0;
match *projection {
Projection::Perspective(ref p) => {
pan *= Vec2::new(p.fov * p.aspect_ratio, p.fov) / vp_size;
if let Some(radius) = pan_orbit.radius {
multiplier = radius;
}
}
Projection::Orthographic(ref p) => {
pan *= Vec2::new(p.area.width(), p.area.height()) / vp_size;
}
}
let right = transform.rotation * Vec3::X * -pan.x;
let up = transform.rotation * Vec3::Y * pan.y;
let translation = (right + up) * multiplier;
pan_orbit.target_focus += translation;
has_moved = true;
}
}
if (scroll_line + scroll_pixel).abs() > 0.0 {
let line_delta = -scroll_line * (pan_orbit.target_radius) * 0.2;
let pixel_delta = -scroll_pixel * (pan_orbit.target_radius) * 0.2;
pan_orbit.target_radius += line_delta + pixel_delta;
pan_orbit.radius = pan_orbit
.radius
.map(|value| apply_zoom_limits(value + pixel_delta));
has_moved = true;
}
pan_orbit.target_alpha = apply_alpha_limits(pan_orbit.target_alpha);
pan_orbit.target_beta = apply_beta_limits(pan_orbit.target_beta);
pan_orbit.target_radius = apply_zoom_limits(pan_orbit.target_radius);
if !pan_orbit.allow_upside_down {
pan_orbit.target_beta = pan_orbit.target_beta.clamp(-PI / 2.0, PI / 2.0);
}
if let (Some(alpha), Some(beta), Some(radius)) =
(pan_orbit.alpha, pan_orbit.beta, pan_orbit.radius)
{
if has_moved
|| pan_orbit.target_alpha != alpha
|| pan_orbit.target_beta != beta
|| pan_orbit.target_radius != radius
|| pan_orbit.target_focus != pan_orbit.focus
|| pan_orbit.force_update
{
let new_alpha = util::lerp_and_snap_f32(
alpha,
pan_orbit.target_alpha,
pan_orbit.orbit_smoothness,
time.delta_seconds(),
);
let new_beta = util::lerp_and_snap_f32(
beta,
pan_orbit.target_beta,
pan_orbit.orbit_smoothness,
time.delta_seconds(),
);
let new_radius = util::lerp_and_snap_f32(
radius,
pan_orbit.target_radius,
pan_orbit.zoom_smoothness,
time.delta_seconds(),
);
let new_focus = util::lerp_and_snap_vec3(
pan_orbit.focus,
pan_orbit.target_focus,
pan_orbit.pan_smoothness,
time.delta_seconds(),
);
util::update_orbit_transform(
new_alpha,
new_beta,
new_radius,
new_focus,
&mut transform,
&mut projection,
);
pan_orbit.alpha = Some(new_alpha);
pan_orbit.beta = Some(new_beta);
pan_orbit.radius = Some(new_radius);
pan_orbit.focus = new_focus;
pan_orbit.force_update = false;
}
}
}
}