use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
use bevy_camera::{Camera, RenderTarget};
use bevy_ecs::prelude::*;
use bevy_input::keyboard::KeyCode;
use bevy_input::mouse::{AccumulatedMouseScroll, MouseButton, MouseScrollUnit};
use bevy_input::ButtonInput;
use bevy_math::{Vec2, Vec3};
use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer};
use bevy_time::{Real, Time};
use bevy_transform::components::GlobalTransform;
use bevy_transform::prelude::Transform;
use bevy_window::{PrimaryWindow, WindowRef};
use core::{f32::consts::*, fmt};
pub struct PanCameraPlugin;
impl Plugin for PanCameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
RunFixedMainLoop,
run_pancamera_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
)
.add_observer(add_window_observer)
.add_observer(remove_window_observer);
}
}
#[derive(Component)]
pub struct PanCamera {
pub enabled: bool,
pub zoom_factor: f32,
pub min_zoom: f32,
pub max_zoom: f32,
pub zoom_speed: f32,
pub key_zoom_in: Option<KeyCode>,
pub key_zoom_out: Option<KeyCode>,
pub pan_speed: f32,
pub key_up: Option<KeyCode>,
pub key_down: Option<KeyCode>,
pub key_left: Option<KeyCode>,
pub key_right: Option<KeyCode>,
pub rotation_speed: f32,
pub key_rotate_ccw: Option<KeyCode>,
pub key_rotate_cw: Option<KeyCode>,
pub mouse_pan_settings: MousePanSettings,
}
pub struct MousePanSettings {
pub enabled: bool,
pub button: MouseButton,
}
impl Default for PanCamera {
fn default() -> Self {
Self {
enabled: true,
zoom_factor: 1.0,
min_zoom: 0.1,
max_zoom: 5.0,
zoom_speed: 0.1,
key_zoom_in: Some(KeyCode::Equal),
key_zoom_out: Some(KeyCode::Minus),
pan_speed: 500.0,
key_up: Some(KeyCode::KeyW),
key_down: Some(KeyCode::KeyS),
key_left: Some(KeyCode::KeyA),
key_right: Some(KeyCode::KeyD),
rotation_speed: PI,
key_rotate_ccw: Some(KeyCode::KeyQ),
key_rotate_cw: Some(KeyCode::KeyE),
mouse_pan_settings: MousePanSettings {
enabled: true,
button: MouseButton::Left,
},
}
}
}
impl PanCamera {
fn key_to_string(key: &Option<KeyCode>) -> String {
key.map_or("None".to_string(), |k| format!("{:?}", k))
}
}
impl fmt::Display for PanCamera {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"
PanCamera Controls:
Move Up / Down - {} / {}
Move Left / Right - {} / {}
Rotate CCW / CW - {} / {}
Zoom - Mouse Scroll + {} / {}
",
Self::key_to_string(&self.key_up),
Self::key_to_string(&self.key_down),
Self::key_to_string(&self.key_left),
Self::key_to_string(&self.key_right),
Self::key_to_string(&self.key_rotate_ccw),
Self::key_to_string(&self.key_rotate_cw),
Self::key_to_string(&self.key_zoom_in),
Self::key_to_string(&self.key_zoom_out),
)
}
}
fn run_pancamera_controller(
time: Res<Time<Real>>,
key_input: Res<ButtonInput<KeyCode>>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mut query: Query<(&mut Transform, &mut PanCamera), With<Camera>>,
) {
let dt = time.delta_secs();
let Ok((mut transform, mut controller)) = query.single_mut() else {
return;
};
if !controller.enabled {
return;
}
let mut movement = Vec2::ZERO;
if let Some(key) = controller.key_left {
if key_input.pressed(key) {
movement.x -= 1.0;
}
}
if let Some(key) = controller.key_right {
if key_input.pressed(key) {
movement.x += 1.0;
}
}
if let Some(key) = controller.key_down {
if key_input.pressed(key) {
movement.y -= 1.0;
}
}
if let Some(key) = controller.key_up {
if key_input.pressed(key) {
movement.y += 1.0;
}
}
if movement != Vec2::ZERO {
let right = transform.right();
let up = transform.up();
let delta = (right * movement.x + up * movement.y).normalize() * controller.pan_speed * dt;
transform.translation.x += delta.x;
transform.translation.y += delta.y;
}
if let Some(key) = controller.key_rotate_ccw {
if key_input.pressed(key) {
transform.rotate_z(controller.rotation_speed * dt);
}
}
if let Some(key) = controller.key_rotate_cw {
if key_input.pressed(key) {
transform.rotate_z(-controller.rotation_speed * dt);
}
}
let mut zoom_amount = 0.0;
if let Some(key) = controller.key_zoom_in {
if key_input.pressed(key) {
zoom_amount -= controller.zoom_speed;
}
}
if let Some(key) = controller.key_zoom_out {
if key_input.pressed(key) {
zoom_amount += controller.zoom_speed;
}
}
let mouse_scroll = match accumulated_mouse_scroll.unit {
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
MouseScrollUnit::Pixel => {
accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
}
};
zoom_amount += mouse_scroll * controller.zoom_speed;
controller.zoom_factor =
(controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);
transform.scale = Vec3::splat(controller.zoom_factor);
}
#[derive(Component)]
struct HandleMousePanObserver(Option<Entity>);
fn add_window_observer(
drag_start: On<Pointer<DragStart>>,
mut commands: Commands,
render_targets: Query<&RenderTarget, With<PanCamera>>,
primary_window: Single<Entity, With<PrimaryWindow>>,
) {
for render_target in render_targets.iter() {
if let RenderTarget::Window(window) = render_target {
let entity = match window {
WindowRef::Primary => primary_window.entity(),
WindowRef::Entity(entity) => *entity,
};
if entity == drag_start.entity {
let observer_id = commands
.spawn(Observer::new(handle_mouse_pan).with_entity(entity))
.id();
commands
.entity(entity)
.insert(HandleMousePanObserver(Some(observer_id)));
}
}
}
}
fn remove_window_observer(
drag_end: On<Pointer<DragEnd>>,
mut commands: Commands,
render_targets: Query<&RenderTarget, With<PanCamera>>,
mut handle_mouse_pan_observer: Query<&mut HandleMousePanObserver>,
primary_window: Single<Entity, With<PrimaryWindow>>,
) {
for render_target in render_targets.iter() {
if let RenderTarget::Window(window) = render_target {
let entity = match window {
WindowRef::Primary => primary_window.entity(),
WindowRef::Entity(entity) => *entity,
};
if entity == drag_end.entity
&& let Ok(mut observer) = handle_mouse_pan_observer.get_mut(entity)
&& let Some(observer_entity) = observer.0.take()
{
commands.entity(observer_entity).despawn();
}
}
}
}
fn handle_mouse_pan(
drag: On<Pointer<Drag>>,
mut pan_cameras: Query<(&Camera, &GlobalTransform, &mut Transform, &PanCamera)>,
) {
for (camera, global_transform, mut transform, pan_camera_controller) in pan_cameras.iter_mut() {
if !pan_camera_controller.enabled || !pan_camera_controller.mouse_pan_settings.enabled {
return;
}
let Ok(camera_screen_position) =
camera.world_to_viewport(global_transform, transform.translation)
else {
continue;
};
let offset_camera_screen_position = camera_screen_position + drag.delta * -1.;
let Ok(new_camera_position) =
camera.viewport_to_world_2d(global_transform, offset_camera_screen_position)
else {
continue;
};
transform.translation = new_camera_position.extend(transform.translation.z);
}
}