1use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
9use bevy_camera::{Camera, RenderTarget};
10use bevy_ecs::prelude::*;
11use bevy_input::keyboard::KeyCode;
12use bevy_input::mouse::{AccumulatedMouseScroll, MouseScrollPixelsPerLine};
13use bevy_input::ButtonInput;
14use bevy_math::{Vec2, Vec3};
15use bevy_picking::{
16 events::{PointerDrag, PointerDragEnd, PointerDragStart},
17 pointer::PointerButton,
18};
19use bevy_time::{Real, Time};
20use bevy_transform::components::GlobalTransform;
21use bevy_transform::prelude::Transform;
22use bevy_window::{PrimaryWindow, Window, WindowRef};
23use core::{f32::consts::*, fmt};
24
25pub struct PanCameraPlugin;
30
31impl Plugin for PanCameraPlugin {
32 fn build(&self, app: &mut App) {
33 app.add_systems(
34 RunFixedMainLoop,
35 (
36 run_pancamera_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
37 run_pan_to_cursor_on_zoom.after(run_pancamera_controller),
38 ),
39 )
40 .add_message::<ZoomEvent>()
41 .add_observer(add_window_observer)
42 .add_observer(remove_window_observer);
43 }
44}
45
46#[derive(impl bevy_ecs::component::Component for PanCamera where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {
const STORAGE_TYPE: bevy_ecs::component::StorageType =
bevy_ecs::component::StorageType::Table;
type Mutability = bevy_ecs::component::Mutable;
fn register_required_components(_requiree:
bevy_ecs::component::ComponentId,
required_components:
&mut bevy_ecs::component::RequiredComponentsRegistrator) {}
fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
use bevy_ecs::component::{
DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
};
(&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
}
fn relationship_accessor()
->
::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
::core::option::Option::None
}
}Component, #[automatically_derived]impl ::core::clone::Clone for PanCamera {
#[inline]
fn clone(&self) -> Self {
PanCamera {
enabled: ::core::clone::Clone::clone(&self.enabled),
zoom_factor: ::core::clone::Clone::clone(&self.zoom_factor),
min_zoom: ::core::clone::Clone::clone(&self.min_zoom),
max_zoom: ::core::clone::Clone::clone(&self.max_zoom),
zoom_speed: ::core::clone::Clone::clone(&self.zoom_speed),
zoom_target: ::core::clone::Clone::clone(&self.zoom_target),
key_zoom_in: ::core::clone::Clone::clone(&self.key_zoom_in),
key_zoom_out: ::core::clone::Clone::clone(&self.key_zoom_out),
pan_speed: ::core::clone::Clone::clone(&self.pan_speed),
key_up: ::core::clone::Clone::clone(&self.key_up),
key_down: ::core::clone::Clone::clone(&self.key_down),
key_left: ::core::clone::Clone::clone(&self.key_left),
key_right: ::core::clone::Clone::clone(&self.key_right),
rotation_speed: ::core::clone::Clone::clone(&self.rotation_speed),
key_rotate_ccw: ::core::clone::Clone::clone(&self.key_rotate_ccw),
key_rotate_cw: ::core::clone::Clone::clone(&self.key_rotate_cw),
mouse_pan_settings: ::core::clone::Clone::clone(&self.mouse_pan_settings),
}
}
}Clone)]
51pub struct PanCamera {
52 pub enabled: bool,
54 pub zoom_factor: f32,
56 pub min_zoom: f32,
58 pub max_zoom: f32,
60 pub zoom_speed: f32,
62 pub zoom_target: ZoomTarget,
64 pub key_zoom_in: Option<KeyCode>,
66 pub key_zoom_out: Option<KeyCode>,
68 pub pan_speed: f32,
70 pub key_up: Option<KeyCode>,
72 pub key_down: Option<KeyCode>,
74 pub key_left: Option<KeyCode>,
76 pub key_right: Option<KeyCode>,
78 pub rotation_speed: f32,
80 pub key_rotate_ccw: Option<KeyCode>,
82 pub key_rotate_cw: Option<KeyCode>,
84 pub mouse_pan_settings: MousePanSettings,
86}
87
88#[derive(#[automatically_derived]impl ::core::clone::Clone for MousePanSettings {
#[inline]
fn clone(&self) -> Self {
MousePanSettings {
enabled: ::core::clone::Clone::clone(&self.enabled),
button: ::core::clone::Clone::clone(&self.button),
}
}
}Clone)]
90pub struct MousePanSettings {
91 pub enabled: bool,
93 pub button: PointerButton,
95}
96
97#[derive(#[automatically_derived]impl ::core::clone::Clone for ZoomTarget {
#[inline]
fn clone(&self) -> Self {
match self {
Self::Center => ZoomTarget::Center,
Self::Cursor => ZoomTarget::Cursor,
}
}
}Clone, #[automatically_derived]impl ::core::cmp::Eq for ZoomTarget { }Eq, #[automatically_derived]impl ::core::marker::StructuralPartialEq for
ZoomTarget {
}
#[automatically_derived]impl ::core::cmp::PartialEq for ZoomTarget {
#[inline]
fn eq(&self, other: &Self) -> bool {
::core::intrinsics::discriminant_value(self) ==
::core::intrinsics::discriminant_value(other)
}
}PartialEq)]
98pub enum ZoomTarget {
100 Center,
102 Cursor,
104}
105
106#[derive(impl bevy_ecs::message::Message for ZoomEvent where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Message)]
108pub struct ZoomEvent(f32);
109
110impl Default for PanCamera {
125 fn default() -> Self {
130 Self {
131 enabled: true,
132 zoom_factor: 1.0,
133 min_zoom: 0.1,
134 max_zoom: 5.0,
135 zoom_speed: 0.1,
136 zoom_target: ZoomTarget::Center,
137 key_zoom_in: Some(KeyCode::Equal),
138 key_zoom_out: Some(KeyCode::Minus),
139 pan_speed: 500.0,
140 key_up: Some(KeyCode::KeyW),
141 key_down: Some(KeyCode::KeyS),
142 key_left: Some(KeyCode::KeyA),
143 key_right: Some(KeyCode::KeyD),
144 rotation_speed: PI,
145 key_rotate_ccw: Some(KeyCode::KeyQ),
146 key_rotate_cw: Some(KeyCode::KeyE),
147 mouse_pan_settings: MousePanSettings {
148 enabled: true,
149 button: PointerButton::Primary,
150 },
151 }
152 }
153}
154
155impl PanCamera {
156 fn key_to_string(key: &Option<KeyCode>) -> String {
157 key.map_or("None".to_string(), |k| ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0:?}", k))
})format!("{:?}", k))
158 }
159}
160
161impl fmt::Display for PanCamera {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_fmt(format_args!("\nPanCamera Controls:\n Move Up / Down - {0} / {1}\n Move Left / Right - {2} / {3}\n Rotate CCW / CW - {4} / {5}\n Zoom - Mouse Scroll + {6} / {7}\n",
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)))write!(
164 f,
165 "
166PanCamera Controls:
167 Move Up / Down - {} / {}
168 Move Left / Right - {} / {}
169 Rotate CCW / CW - {} / {}
170 Zoom - Mouse Scroll + {} / {}
171",
172 Self::key_to_string(&self.key_up),
173 Self::key_to_string(&self.key_down),
174 Self::key_to_string(&self.key_left),
175 Self::key_to_string(&self.key_right),
176 Self::key_to_string(&self.key_rotate_ccw),
177 Self::key_to_string(&self.key_rotate_cw),
178 Self::key_to_string(&self.key_zoom_in),
179 Self::key_to_string(&self.key_zoom_out),
180 )
181 }
182}
183
184fn run_pan_to_cursor_on_zoom(
192 window: Single<&Window>,
193 query: Single<(&Camera, &GlobalTransform, &mut Transform), With<PanCamera>>,
194 mut reader: MessageReader<ZoomEvent>,
195) {
196 let (camera, global, mut transform) = query.into_inner();
197
198 for ZoomEvent(delta_zoom) in reader.read() {
199 if let Some(cursor_pos) = window.cursor_position()
203 && let Ok(world_pos) = camera.viewport_to_world_2d(global, cursor_pos)
204 {
205 let cursor_vec = world_pos - transform.translation.truncate();
206 let delta_cursor_vec = (1. - delta_zoom) * cursor_vec;
207 transform.translation += delta_cursor_vec.extend(0.);
208 }
209 }
210}
211
212fn run_pancamera_controller(
220 time: Res<Time<Real>>,
221 key_input: Res<ButtonInput<KeyCode>>,
222 accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
223 mouse_scroll_conversion: Res<MouseScrollPixelsPerLine>,
224 mut query: Query<(&mut Transform, &mut PanCamera), With<Camera>>,
225 mut writer: MessageWriter<ZoomEvent>,
226) {
227 let dt = time.delta_secs();
228
229 let Ok((mut transform, mut controller)) = query.single_mut() else {
230 return;
231 };
232
233 if !controller.enabled {
234 return;
235 }
236
237 let mut movement = Vec2::ZERO;
239 if let Some(key) = controller.key_left {
240 if key_input.pressed(key) {
241 movement.x -= 1.0;
242 }
243 }
244 if let Some(key) = controller.key_right {
245 if key_input.pressed(key) {
246 movement.x += 1.0;
247 }
248 }
249 if let Some(key) = controller.key_down {
250 if key_input.pressed(key) {
251 movement.y -= 1.0;
252 }
253 }
254 if let Some(key) = controller.key_up {
255 if key_input.pressed(key) {
256 movement.y += 1.0;
257 }
258 }
259
260 if movement != Vec2::ZERO {
261 let right = transform.right();
262 let up = transform.up();
263
264 let delta = (right * movement.x + up * movement.y).normalize() * controller.pan_speed * dt;
265
266 transform.translation.x += delta.x;
267 transform.translation.y += delta.y;
268 }
269
270 if let Some(key) = controller.key_rotate_ccw {
272 if key_input.pressed(key) {
273 transform.rotate_z(controller.rotation_speed * dt);
274 }
275 }
276 if let Some(key) = controller.key_rotate_cw {
277 if key_input.pressed(key) {
278 transform.rotate_z(-controller.rotation_speed * dt);
279 }
280 }
281
282 let mut zoom_amount = 0.0;
284
285 if let Some(key) = controller.key_zoom_in {
287 if key_input.pressed(key) {
288 zoom_amount -= controller.zoom_speed;
289 }
290 }
291 if let Some(key) = controller.key_zoom_out {
292 if key_input.pressed(key) {
293 zoom_amount += controller.zoom_speed;
294 }
295 }
296
297 let mouse_scroll = accumulated_mouse_scroll
299 .to_lines(&mouse_scroll_conversion)
300 .delta
301 .y;
302 zoom_amount += mouse_scroll * controller.zoom_speed;
303
304 let prev_zoom = controller.zoom_factor;
305
306 controller.zoom_factor =
307 (controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);
308
309 if controller.zoom_target == ZoomTarget::Cursor {
312 writer.write(ZoomEvent(controller.zoom_factor / prev_zoom));
313 }
314
315 transform.scale = Vec3::splat(controller.zoom_factor);
316}
317
318#[derive(impl bevy_ecs::component::Component for HandleMousePanObserver where
Self: ::core::marker::Send + ::core::marker::Sync + 'static {
const STORAGE_TYPE: bevy_ecs::component::StorageType =
bevy_ecs::component::StorageType::Table;
type Mutability = bevy_ecs::component::Mutable;
fn register_required_components(_requiree:
bevy_ecs::component::ComponentId,
required_components:
&mut bevy_ecs::component::RequiredComponentsRegistrator) {}
fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
use bevy_ecs::component::{
DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
};
(&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
}
fn relationship_accessor()
->
::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
::core::option::Option::None
}
}Component)]
322struct HandleMousePanObserver(Option<Entity>);
323
324fn add_window_observer(
325 drag_start: On<PointerDragStart>,
326 mut commands: Commands,
327 render_targets: Query<&RenderTarget, With<PanCamera>>,
328 primary_window: Single<Entity, With<PrimaryWindow>>,
329) {
330 for render_target in render_targets.iter() {
331 if let RenderTarget::Window(window) = render_target {
332 let entity = match window {
333 WindowRef::Primary => primary_window.entity(),
334 WindowRef::Entity(entity) => *entity,
335 };
336 if entity == drag_start.entity {
337 let observer_id = commands
338 .spawn(Observer::new(handle_mouse_pan).with_entity(entity))
339 .id();
340 commands
341 .entity(entity)
342 .insert(HandleMousePanObserver(Some(observer_id)));
343 }
344 }
345 }
346}
347
348fn remove_window_observer(
349 drag_end: On<PointerDragEnd>,
350 mut commands: Commands,
351 render_targets: Query<&RenderTarget, With<PanCamera>>,
352 mut handle_mouse_pan_observer: Query<&mut HandleMousePanObserver>,
353 primary_window: Single<Entity, With<PrimaryWindow>>,
354) {
355 for render_target in render_targets.iter() {
356 if let RenderTarget::Window(window) = render_target {
357 let entity = match window {
358 WindowRef::Primary => primary_window.entity(),
359 WindowRef::Entity(entity) => *entity,
360 };
361 if entity == drag_end.entity
362 && let Ok(mut observer) = handle_mouse_pan_observer.get_mut(entity)
363 && let Some(observer_entity) = observer.0.take()
364 {
365 commands.entity(observer_entity).despawn();
366 }
367 }
368 }
369}
370
371fn handle_mouse_pan(
372 drag: On<PointerDrag>,
373 mut pan_cameras: Query<(&Camera, &GlobalTransform, &mut Transform, &PanCamera)>,
374) {
375 for (camera, global_transform, mut transform, pan_camera_controller) in pan_cameras.iter_mut() {
376 if !pan_camera_controller.enabled
377 || !pan_camera_controller.mouse_pan_settings.enabled
378 || drag.button != pan_camera_controller.mouse_pan_settings.button
379 {
380 return;
381 }
382
383 let Ok(camera_screen_position) =
384 camera.world_to_viewport(global_transform, transform.translation)
385 else {
386 continue;
387 };
388
389 let offset_camera_screen_position = camera_screen_position + drag.delta * -1.; let Ok(new_camera_position) =
392 camera.viewport_to_world_2d(global_transform, offset_camera_screen_position)
393 else {
394 continue;
395 };
396
397 transform.translation = new_camera_position.extend(transform.translation.z);
398 }
399}