viewport_lib/interaction/input/
controller.rs1use crate::Camera;
7
8use super::action::Action;
9use super::action_frame::ActionFrame;
10use super::context::ViewportContext;
11use super::event::ViewportEvent;
12use super::mode::NavigationMode;
13use super::preset::{BindingPreset, viewport_all_bindings, viewport_primitives_bindings};
14use super::viewport_input::ViewportInput;
15
16pub struct OrbitCameraController {
49 input: ViewportInput,
50 pub navigation_mode: NavigationMode,
54 pub fly_speed: f32,
59 pub orbit_sensitivity: f32,
61 pub zoom_sensitivity: f32,
63 pub gesture_sensitivity: f32,
67 viewport_size: [f32; 2],
69}
70
71impl OrbitCameraController {
72 pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.005;
74 pub const DEFAULT_ZOOM_SENSITIVITY: f32 = 0.001;
76 pub const DEFAULT_GESTURE_SENSITIVITY: f32 = 1.0;
78 pub const DEFAULT_FLY_SPEED: f32 = 0.1;
80
81 pub fn new(preset: BindingPreset) -> Self {
83 let bindings = match preset {
84 BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
85 BindingPreset::ViewportAll => viewport_all_bindings(),
86 };
87 Self {
88 input: ViewportInput::new(bindings),
89 navigation_mode: NavigationMode::Arcball,
90 fly_speed: Self::DEFAULT_FLY_SPEED,
91 orbit_sensitivity: Self::DEFAULT_ORBIT_SENSITIVITY,
92 zoom_sensitivity: Self::DEFAULT_ZOOM_SENSITIVITY,
93 gesture_sensitivity: Self::DEFAULT_GESTURE_SENSITIVITY,
94 viewport_size: [1.0, 1.0],
95 }
96 }
97
98 pub fn viewport_primitives() -> Self {
102 Self::new(BindingPreset::ViewportPrimitives)
103 }
104
105 pub fn viewport_all() -> Self {
111 Self::new(BindingPreset::ViewportAll)
112 }
113
114 pub fn begin_frame(&mut self, ctx: ViewportContext) {
123 self.viewport_size = ctx.viewport_size;
124 self.input.begin_frame(ctx);
125 }
126
127 pub fn push_event(&mut self, event: ViewportEvent) {
132 self.input.push_event(event);
133 }
134
135 pub fn resolve(&self) -> ActionFrame {
142 self.input.resolve()
143 }
144
145 pub fn apply_to_camera(&mut self, camera: &mut Camera) -> ActionFrame {
158 let frame = self.input.resolve();
159 let nav = &frame.navigation;
160 let h = self.viewport_size[1];
161
162 match self.navigation_mode {
163 NavigationMode::Arcball => {
164 if nav.orbit != glam::Vec2::ZERO {
165 camera.orbit(
166 nav.orbit.x * self.orbit_sensitivity,
167 nav.orbit.y * self.orbit_sensitivity,
168 );
169 }
170 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
171 camera.orbit(nav.twist * self.gesture_sensitivity, 0.0);
172 }
173 if nav.pan != glam::Vec2::ZERO {
174 camera.pan_pixels(nav.pan, h);
175 }
176 if nav.zoom != 0.0 {
177 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
178 }
179 }
180
181 NavigationMode::Turntable => {
182 if nav.orbit != glam::Vec2::ZERO {
183 let yaw = nav.orbit.x * self.orbit_sensitivity;
184 let pitch = nav.orbit.y * self.orbit_sensitivity;
185 apply_turntable(camera, yaw, pitch);
186 }
187 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
188 apply_turntable(camera, nav.twist * self.gesture_sensitivity, 0.0);
190 }
191 if nav.pan != glam::Vec2::ZERO {
192 camera.pan_pixels(nav.pan, h);
193 }
194 if nav.zoom != 0.0 {
195 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
196 }
197 }
198
199 NavigationMode::Planar => {
200 if nav.pan != glam::Vec2::ZERO {
202 camera.pan_pixels(nav.pan, h);
203 }
204 if nav.zoom != 0.0 {
205 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
206 }
207 }
208
209 NavigationMode::FirstPerson => {
210 if nav.orbit != glam::Vec2::ZERO {
212 let yaw = nav.orbit.x * self.orbit_sensitivity;
213 let pitch = nav.orbit.y * self.orbit_sensitivity;
214 apply_firstperson_look(camera, yaw, pitch);
215 }
216 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
217 apply_firstperson_look(camera, nav.twist * self.gesture_sensitivity, 0.0);
218 }
219
220 let forward = -(camera.orientation * glam::Vec3::Z);
222 let right = camera.orientation * glam::Vec3::X;
223 let up = camera.orientation * glam::Vec3::Y;
224 let speed = self.fly_speed;
225
226 let mut move_delta = glam::Vec3::ZERO;
227 if frame.is_active(Action::FlyForward) {
228 move_delta += forward * speed;
229 }
230 if frame.is_active(Action::FlyBackward) {
231 move_delta -= forward * speed;
232 }
233 if frame.is_active(Action::FlyRight) {
234 move_delta += right * speed;
235 }
236 if frame.is_active(Action::FlyLeft) {
237 move_delta -= right * speed;
238 }
239 if frame.is_active(Action::FlyUp) {
240 move_delta += up * speed;
241 }
242 if frame.is_active(Action::FlyDown) {
243 move_delta -= up * speed;
244 }
245 camera.center += move_delta;
247 }
248 }
249
250 frame
251 }
252}
253
254fn apply_turntable(camera: &mut Camera, yaw: f32, pitch: f32) {
263 if yaw != 0.0 {
265 camera.orientation = (glam::Quat::from_rotation_z(-yaw) * camera.orientation).normalize();
266 }
267
268 if pitch != 0.0 {
269 let proposed = (camera.orientation * glam::Quat::from_rotation_x(-pitch)).normalize();
271
272 let max_sin_el = 89.0_f32.to_radians().sin(); let eye_z = (proposed * glam::Vec3::Z).z;
277
278 if eye_z.abs() <= max_sin_el {
279 camera.orientation = proposed;
280 }
281 }
283}
284
285fn apply_firstperson_look(camera: &mut Camera, yaw: f32, pitch: f32) {
290 let eye = camera.eye_position();
291 camera.orientation = (glam::Quat::from_rotation_z(-yaw)
292 * camera.orientation
293 * glam::Quat::from_rotation_x(-pitch))
294 .normalize();
295 camera.center = eye - camera.orientation * (glam::Vec3::Z * camera.distance);
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::interaction::input::binding::KeyCode;
303 use crate::interaction::input::event::{ButtonState, ScrollUnits, ViewportEvent};
304
305 fn make_ctx() -> ViewportContext {
306 ViewportContext {
307 hovered: true,
308 focused: true,
309 viewport_size: [800.0, 600.0],
310 }
311 }
312
313 #[test]
314 fn new_defaults() {
315 let ctrl = OrbitCameraController::viewport_primitives();
316 assert_eq!(ctrl.navigation_mode, NavigationMode::Arcball);
317 assert!((ctrl.fly_speed - OrbitCameraController::DEFAULT_FLY_SPEED).abs() < 1e-6);
318 assert!(
319 (ctrl.orbit_sensitivity - OrbitCameraController::DEFAULT_ORBIT_SENSITIVITY).abs()
320 < 1e-6
321 );
322 }
323
324 #[test]
325 fn resolve_no_events_zero_nav() {
326 let mut ctrl = OrbitCameraController::viewport_all();
327 ctrl.begin_frame(make_ctx());
328 let frame = ctrl.resolve();
329 assert_eq!(frame.navigation.orbit, glam::Vec2::ZERO);
330 assert_eq!(frame.navigation.pan, glam::Vec2::ZERO);
331 assert_eq!(frame.navigation.zoom, 0.0);
332 }
333
334 #[test]
335 fn apply_zoom_changes_distance() {
336 let mut ctrl = OrbitCameraController::viewport_primitives();
337 ctrl.begin_frame(make_ctx());
338 let mut cam = Camera::default();
339 let d0 = cam.distance;
340 ctrl.push_event(ViewportEvent::Wheel {
341 delta: glam::Vec2::new(0.0, 100.0),
342 units: ScrollUnits::Pixels,
343 });
344 ctrl.apply_to_camera(&mut cam);
345 assert!(
346 (cam.distance - d0).abs() > 1e-4,
347 "zoom should change camera distance"
348 );
349 }
350
351 #[test]
352 fn planar_mode_ignores_orbit() {
353 let mut ctrl = OrbitCameraController::viewport_primitives();
354 ctrl.navigation_mode = NavigationMode::Planar;
355 ctrl.begin_frame(make_ctx());
356 let mut cam = Camera::default();
357 let orient_before = cam.orientation;
358 ctrl.push_event(ViewportEvent::PointerMoved {
360 position: glam::Vec2::new(100.0, 100.0),
361 });
362 ctrl.push_event(ViewportEvent::MouseButton {
363 button: crate::interaction::input::binding::MouseButton::Left,
364 state: ButtonState::Pressed,
365 });
366 ctrl.push_event(ViewportEvent::PointerMoved {
367 position: glam::Vec2::new(200.0, 200.0),
368 });
369 ctrl.apply_to_camera(&mut cam);
370 assert!(
371 (cam.orientation.x - orient_before.x).abs() < 1e-6
372 && (cam.orientation.y - orient_before.y).abs() < 1e-6
373 && (cam.orientation.z - orient_before.z).abs() < 1e-6
374 && (cam.orientation.w - orient_before.w).abs() < 1e-6,
375 "planar mode should not change orientation"
376 );
377 }
378
379 #[test]
380 fn turntable_pitch_clamped() {
381 let mut cam = Camera::default();
382 for _ in 0..1000 {
384 apply_turntable(&mut cam, 0.0, 0.1);
385 }
386 let eye_z = (cam.orientation * glam::Vec3::Z).z;
388 let max_sin = 89.0_f32.to_radians().sin();
389 assert!(
390 eye_z.abs() <= max_sin + 1e-4,
391 "turntable pitch should be clamped: eye_z={eye_z}"
392 );
393 }
394
395 #[test]
396 fn firstperson_look_preserves_eye() {
397 let mut cam = Camera::default();
398 let eye_before = cam.eye_position();
399 apply_firstperson_look(&mut cam, 0.3, 0.2);
400 let eye_after = cam.eye_position();
401 let diff = (eye_after - eye_before).length();
402 assert!(
403 diff < 1e-3,
404 "firstperson look should preserve eye position, diff={diff}"
405 );
406 }
407
408 #[test]
409 fn firstperson_fly_moves_camera() {
410 let mut ctrl = OrbitCameraController::viewport_all();
411 ctrl.navigation_mode = NavigationMode::FirstPerson;
412 ctrl.fly_speed = 1.0;
413 ctrl.begin_frame(make_ctx());
414 let mut cam = Camera::default();
415 cam.center = glam::Vec3::ZERO;
416 cam.orientation = glam::Quat::IDENTITY;
417 let center_before = cam.center;
418 ctrl.push_event(ViewportEvent::Key {
419 key: KeyCode::W,
420 state: ButtonState::Pressed,
421 repeat: false,
422 });
423 ctrl.apply_to_camera(&mut cam);
424 assert!(
425 (cam.center - center_before).length() > 0.5,
426 "FlyForward should move camera center"
427 );
428 }
429}