1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
//! Provides a default input plugin for the camera. See [`DefaultInputPlugin`].
use bevy_app::prelude::*;
use bevy_camera::{prelude::*, RenderTarget};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_input::{
mouse::{MouseScrollUnit, MouseWheel},
prelude::*,
};
use bevy_math::{prelude::*, DVec2, DVec3};
use bevy_platform::collections::HashMap;
use bevy_reflect::prelude::*;
use bevy_transform::prelude::*;
use bevy_window::{PrimaryWindow, Window};
use bevy_picking::pointer::{
PointerAction, PointerId, PointerInput, PointerInteraction, PointerLocation, PointerMap,
};
use crate::prelude::{component::EditorCam, inputs::MotionInputs};
/// The type of mutually exclusive camera motion.
#[derive(Debug, Clone, Copy, Reflect, PartialEq, Eq)]
pub enum MotionKind {
/// The camera is orbiting and zooming.
OrbitZoom,
/// The camera is panning and zooming.
PanZoom,
/// The camera is only zooming.
Zoom,
}
impl From<&MotionInputs> for MotionKind {
fn from(value: &MotionInputs) -> Self {
match value {
MotionInputs::OrbitZoom { .. } => MotionKind::OrbitZoom,
MotionInputs::PanZoom { .. } => MotionKind::PanZoom,
MotionInputs::Zoom { .. } => MotionKind::Zoom,
}
}
}
/// A plugin that provides a default input mapping. Intended to be replaced by users with their own
/// version of this code, if needed.
///
/// The input plugin is responsible for starting motions, sending inputs, and ending motions. See
/// [`EditorCam`] for more details on how to implement this yourself.
pub struct DefaultInputPlugin;
impl Plugin for DefaultInputPlugin {
fn build(&self, app: &mut App) {
app.add_message::<crate::input::EditorCamInputMessage>()
.init_resource::<crate::input::CameraPointerMap>()
.add_systems(
PreUpdate,
(
default_camera_inputs,
EditorCamInputMessage::receive_messages,
EditorCamInputMessage::send_pointer_inputs,
)
.chain()
.after(bevy_picking::PickingSystems::Last)
.before(crate::controller::component::EditorCam::update_camera_positions),
);
}
}
/// A default implementation of an input system
pub fn default_camera_inputs(
pointers: Query<(&PointerId, &PointerLocation)>,
pointer_map: Res<CameraPointerMap>,
mut controller: MessageWriter<EditorCamInputMessage>,
mut mouse_wheel: MessageReader<MouseWheel>,
mouse_input: Res<ButtonInput<MouseButton>>,
cameras: Query<(Entity, &Camera, &RenderTarget, &EditorCam)>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
let orbit_start = MouseButton::Right;
let pan_start = MouseButton::Left;
let zoom_stop = 0.0;
if let Some(&camera) = pointer_map.get(&PointerId::Mouse) {
let camera_query = cameras.get(camera).ok();
let is_in_zoom_mode = camera_query
.map(|(.., editor_cam)| editor_cam.current_motion.is_zooming_only())
.unwrap_or_default();
let zoom_amount_abs = camera_query
.and_then(|(.., editor_cam)| {
editor_cam
.current_motion
.inputs()
.map(|inputs| inputs.zoom_velocity_abs(editor_cam.smoothing.zoom.mul_f32(2.0)))
})
.unwrap_or(0.0);
let should_zoom_end = is_in_zoom_mode && zoom_amount_abs <= zoom_stop;
if mouse_input.any_just_released([orbit_start, pan_start]) || should_zoom_end {
controller.write(EditorCamInputMessage::End { camera });
}
}
for (&pointer, pointer_location) in pointers
.iter()
.filter_map(|(id, loc)| loc.location().map(|loc| (id, loc)))
{
match pointer {
PointerId::Mouse => {
let Some((camera, ..)) = cameras.iter().find(|(_, camera, render_target, _)| {
pointer_location.is_in_viewport(camera, render_target, &primary_window)
}) else {
continue; // Pointer must be in viewport to start a motion.
};
if mouse_input.just_pressed(orbit_start) {
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::OrbitZoom,
camera,
pointer,
});
} else if mouse_input.just_pressed(pan_start) {
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::PanZoom,
camera,
pointer,
});
} else if mouse_wheel.read().map(|mw| mw.y.abs()).sum::<f32>() > 0.0 {
// Note we can't just check if the mouse wheel inputs are empty, we need to
// check if the y value abs greater than zero, otherwise we get a bunch of false
// positives, which can cause issues with figuring out what the user is trying
// to do.
controller.write(EditorCamInputMessage::Start {
kind: MotionKind::Zoom,
camera,
pointer,
});
}
}
PointerId::Touch(_) => continue,
PointerId::Custom(_) => continue,
}
}
// This must be cleared manually because reading these inputs is conditional - we are not
// guaranteed to be flushing the events every frame.
mouse_wheel.clear();
}
/// Maps pointers to the camera they are currently controlling.
///
/// This is needed so we can automatically track pointer movements and update camera movement after
/// a [`EditorCamInputMessage::Start`] has been received.
#[derive(Debug, Clone, Default, Deref, DerefMut, Reflect, Resource)]
pub struct CameraPointerMap(HashMap<PointerId, Entity>);
/// Messages used when implementing input systems for the [`EditorCam`].
#[derive(Debug, Clone, Reflect, Message)]
pub enum EditorCamInputMessage {
/// Send this event to start moving the camera. The anchor and inputs will be computed
/// automatically until the [`EditorCamInputMessage::End`] event is received.
Start {
/// The kind of camera movement that is being started.
kind: MotionKind,
/// The camera to move.
camera: Entity,
/// The pointer that will be controlling the camera. The rotation anchor point in the world
/// will be automatically computed using picking backends.
pointer: PointerId,
},
/// Send this event when a user's input ends, e.g. the button is released.
End {
/// The entity of the camera that should end its current input motion.
camera: Entity,
},
}
impl EditorCamInputMessage {
/// Get the camera entity associated with this event.
pub fn camera(&self) -> Entity {
match self {
EditorCamInputMessage::Start { camera, .. } => *camera,
EditorCamInputMessage::End { camera } => *camera,
}
}
/// Receive [`EditorCamInputMessage`]s, and use these to start and end moves on the [`EditorCam`].
#[allow(clippy::too_many_arguments)]
pub fn receive_messages(
mut events: MessageReader<Self>,
mut controllers: Query<(&mut EditorCam, &GlobalTransform)>,
mut camera_map: ResMut<CameraPointerMap>,
pointer_map: Res<PointerMap>,
pointer_interactions: Query<&PointerInteraction>,
pointer_locations: Query<&PointerLocation>,
cameras: Query<(&Camera, &Projection)>,
windows: Query<&Window>,
) {
for event in events.read() {
let Ok((mut controller, cam_transform)) = controllers.get_mut(event.camera()) else {
continue;
};
match event {
EditorCamInputMessage::Start { kind, pointer, .. } => {
if controller.is_actively_controlled() {
continue;
}
let anchor = pointer_map
.get_entity(*pointer)
.and_then(|entity| pointer_interactions.get(entity).ok())
.and_then(|interaction| interaction.get_nearest_hit())
// Since `bevy` 0.17.3:
//
// If the current hit is on a window, we cannot use the `hit.position` as an anchor
// as the `hit.position` is in viewport coordinates.
.filter(|(entity, _hit)| !windows.contains(*entity))
.and_then(|(_, hit)| hit.position)
.map(|world_space_hit| {
// Convert the world space hit to view (camera) space
cam_transform
.to_matrix()
.as_dmat4()
.inverse()
.transform_point3(world_space_hit.into())
})
.filter(|p| {
#[cfg(debug_assertions)]
if !p.is_finite() {
bevy_log::warn!("Non-finite input fed to camera controller: {p:?}")
}
p.is_finite()
})
.or_else(|| {
let camera = cameras.get(event.camera()).ok();
let pointer_location = pointer_map
.get_entity(*pointer)
.and_then(|entity| pointer_locations.get(entity).ok())
.and_then(|l| l.location());
if let Some(((camera, proj), pointer_location)) =
camera.zip(pointer_location)
{
screen_to_view_space(
camera,
proj,
&controller,
pointer_location.position,
)
} else {
None
}
})
.filter(|p| p.is_finite());
match kind {
MotionKind::OrbitZoom => controller.start_orbit(anchor),
MotionKind::PanZoom => controller.start_pan(anchor),
MotionKind::Zoom => controller.start_zoom(anchor),
}
camera_map.insert(*pointer, event.camera());
}
EditorCamInputMessage::End { .. } => {
controller.end_move();
if let Some(pointer) = camera_map
.iter()
.find(|(.., &camera)| camera == event.camera())
.map(|(&pointer, ..)| pointer)
{
camera_map.remove(&pointer);
}
}
}
}
}
/// While a camera motion is active, this system will take care of sending new pointer motion to
/// the camera controller. The camera controller assumes that pan and orbit movements are tied
/// to screen space pointer motion.
///
/// This is because some of the pixel-perfect features of the controller require that data be
/// passed in as screen space deltas, to compute perfect first-order control. This is also
/// because the plugin uses pointer information to know which camera is being controlled.
///
/// If you want to control the camera with different inputs, you will need to replace this
/// system with one that tracks other input methods, and sends the required zoom and screenspace
/// movement information.
pub fn send_pointer_inputs(
camera_map: Res<CameraPointerMap>,
mut camera_controllers: Query<&mut EditorCam>,
mut mouse_wheel: MessageReader<MouseWheel>,
mut moves: MessageReader<PointerInput>,
) {
let moves_list: Vec<_> = moves.read().collect();
for (pointer, camera) in camera_map.iter() {
let Ok(mut camera_controller) = camera_controllers.get_mut(*camera) else {
continue;
};
let screenspace_input = moves_list
.iter()
.filter(|m| m.pointer_id.eq(pointer))
.filter_map(|m| match m.action {
PointerAction::Move { delta } => Some(delta),
PointerAction::Press { .. } => None,
PointerAction::Cancel => None,
_ => None,
})
.sum();
let zoom_amount = match pointer {
// TODO: add pinch zoom support
PointerId::Mouse => mouse_wheel
.read()
.map(|mw| {
let scroll_multiplier = match mw.unit {
MouseScrollUnit::Line => 150.0,
MouseScrollUnit::Pixel => 1.0,
};
mw.y * scroll_multiplier
})
.sum::<f32>(),
_ => 0.0,
};
camera_controller.send_screenspace_input(screenspace_input);
camera_controller.send_zoom_input(zoom_amount);
}
// This must be cleared manually because reading these inputs is conditional - we are not
// guaranteed to be flushing the events every frame.
mouse_wheel.clear();
}
}
fn screen_to_view_space(
camera: &Camera,
proj: &Projection,
controller: &EditorCam,
target_position: Vec2,
) -> Option<DVec3> {
let mut viewport_position = if let Some(rect) = camera.logical_viewport_rect() {
target_position.as_dvec2() - rect.min.as_dvec2()
} else {
target_position.as_dvec2()
};
let target_size = camera.logical_viewport_size()?.as_dvec2();
// Flip the Y co-ordinate origin from the top to the bottom.
viewport_position.y = target_size.y - viewport_position.y;
let ndc = viewport_position * 2. / target_size - DVec2::ONE;
let ndc_to_view = proj.get_clip_from_view().as_dmat4().inverse();
let view_near_plane = ndc_to_view.project_point3(ndc.extend(1.));
match &proj {
Projection::Perspective(_) | Projection::Custom(_) => {
// Using EPSILON because an NDC with Z = 0 returns NaNs.
let view_far_plane = ndc_to_view.project_point3(ndc.extend(f64::EPSILON));
let direction = (view_far_plane - view_near_plane).normalize();
Some((direction / direction.z) * controller.last_anchor_depth())
}
Projection::Orthographic(_) => Some(DVec3::new(
view_near_plane.x,
view_near_plane.y,
controller.last_anchor_depth(),
)),
}
}