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, MouseScrollPixelsPerLine};
use bevy_input::ButtonInput;
use bevy_math::{Vec2, Vec3};
use bevy_picking::{
events::{PointerDrag, PointerDragEnd, PointerDragStart},
pointer::PointerButton,
};
use bevy_time::{Real, Time};
use bevy_transform::components::GlobalTransform;
use bevy_transform::prelude::Transform;
use bevy_window::{PrimaryWindow, Window, 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),
run_pan_to_cursor_on_zoom.after(run_pancamera_controller),
),
)
.add_message::<ZoomEvent>()
.add_observer(add_window_observer)
.add_observer(remove_window_observer);
}
}
#[derive(Component, Clone)]
pub struct PanCamera {
pub enabled: bool,
pub zoom_factor: f32,
pub min_zoom: f32,
pub max_zoom: f32,
pub zoom_speed: f32,
pub zoom_target: ZoomTarget,
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,
}
#[derive(Clone)]
pub struct MousePanSettings {
pub enabled: bool,
pub button: PointerButton,
}
#[derive(Clone, Eq, PartialEq)]
pub enum ZoomTarget {
Center,
Cursor,
}
#[derive(Message)]
pub struct ZoomEvent(f32);
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,
zoom_target: ZoomTarget::Center,
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: PointerButton::Primary,
},
}
}
}
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_pan_to_cursor_on_zoom(
window: Single<&Window>,
query: Single<(&Camera, &GlobalTransform, &mut Transform), With<PanCamera>>,
mut reader: MessageReader<ZoomEvent>,
) {
let (camera, global, mut transform) = query.into_inner();
for ZoomEvent(delta_zoom) in reader.read() {
if let Some(cursor_pos) = window.cursor_position()
&& let Ok(world_pos) = camera.viewport_to_world_2d(global, cursor_pos)
{
let cursor_vec = world_pos - transform.translation.truncate();
let delta_cursor_vec = (1. - delta_zoom) * cursor_vec;
transform.translation += delta_cursor_vec.extend(0.);
}
}
}
fn run_pancamera_controller(
time: Res<Time<Real>>,
key_input: Res<ButtonInput<KeyCode>>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mouse_scroll_conversion: Res<MouseScrollPixelsPerLine>,
mut query: Query<(&mut Transform, &mut PanCamera), With<Camera>>,
mut writer: MessageWriter<ZoomEvent>,
) {
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 = accumulated_mouse_scroll
.to_lines(&mouse_scroll_conversion)
.delta
.y;
zoom_amount += mouse_scroll * controller.zoom_speed;
let prev_zoom = controller.zoom_factor;
controller.zoom_factor =
(controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);
if controller.zoom_target == ZoomTarget::Cursor {
writer.write(ZoomEvent(controller.zoom_factor / prev_zoom));
}
transform.scale = Vec3::splat(controller.zoom_factor);
}
#[derive(Component)]
struct HandleMousePanObserver(Option<Entity>);
fn add_window_observer(
drag_start: On<PointerDragStart>,
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<PointerDragEnd>,
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<PointerDrag>,
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
|| drag.button != pan_camera_controller.mouse_pan_settings.button
{
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);
}
}