viewport_lib/camera/controllers/
orbit.rs1use crate::Camera;
7
8use crate::interaction::input::action::Action;
9use crate::interaction::input::action_frame::ActionFrame;
10use crate::interaction::input::context::ViewportContext;
11use crate::interaction::input::event::ViewportEvent;
12use crate::interaction::input::mode::NavigationMode;
13use crate::interaction::input::preset::{
14 BindingPreset, viewport_all_bindings, viewport_primitives_bindings,
15};
16use crate::interaction::input::viewport_input::ViewportInput;
17
18pub struct OrbitCameraController {
51 input: ViewportInput,
52 pub navigation_mode: NavigationMode,
56 pub fly_speed: f32,
61 pub orbit_sensitivity: f32,
63 pub zoom_sensitivity: f32,
65 pub gesture_sensitivity: f32,
69 viewport_size: [f32; 2],
71}
72
73impl OrbitCameraController {
74 pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.005;
76 pub const DEFAULT_ZOOM_SENSITIVITY: f32 = 0.001;
78 pub const DEFAULT_GESTURE_SENSITIVITY: f32 = 1.0;
80 pub const DEFAULT_FLY_SPEED: f32 = 0.1;
82
83 pub fn new(preset: BindingPreset) -> Self {
85 let bindings = match preset {
86 BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
87 BindingPreset::ViewportAll => viewport_all_bindings(),
88 };
89 Self {
90 input: ViewportInput::new(bindings),
91 navigation_mode: NavigationMode::Arcball,
92 fly_speed: Self::DEFAULT_FLY_SPEED,
93 orbit_sensitivity: Self::DEFAULT_ORBIT_SENSITIVITY,
94 zoom_sensitivity: Self::DEFAULT_ZOOM_SENSITIVITY,
95 gesture_sensitivity: Self::DEFAULT_GESTURE_SENSITIVITY,
96 viewport_size: [1.0, 1.0],
97 }
98 }
99
100 pub fn viewport_primitives() -> Self {
104 Self::new(BindingPreset::ViewportPrimitives)
105 }
106
107 pub fn viewport_all() -> Self {
113 Self::new(BindingPreset::ViewportAll)
114 }
115
116 pub fn begin_frame(&mut self, ctx: ViewportContext) {
125 self.viewport_size = ctx.viewport_size;
126 self.input.begin_frame(ctx);
127 }
128
129 pub fn set_viewport_size(&mut self, viewport_size: [f32; 2]) {
133 self.viewport_size = viewport_size;
134 }
135
136 pub fn push_event(&mut self, event: ViewportEvent) {
141 self.input.push_event(event);
142 }
143
144 pub fn resolve(&self) -> ActionFrame {
151 self.input.resolve()
152 }
153
154 pub fn apply_to_camera(&mut self, camera: &mut Camera) -> ActionFrame {
167 let frame = self.input.resolve();
168 self.apply(camera, &frame);
169 frame
170 }
171
172 pub fn apply(&self, camera: &mut Camera, frame: &ActionFrame) {
185 let nav = &frame.navigation;
186 let h = self.viewport_size[1];
187
188 match self.navigation_mode {
189 NavigationMode::Arcball => {
190 if nav.orbit != glam::Vec2::ZERO {
191 camera.orbit(
192 nav.orbit.x * self.orbit_sensitivity,
193 nav.orbit.y * self.orbit_sensitivity,
194 );
195 }
196 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
197 camera.orbit(nav.twist * self.gesture_sensitivity, 0.0);
198 }
199 if nav.pan != glam::Vec2::ZERO {
200 camera.pan_pixels(nav.pan, h);
201 }
202 if nav.zoom != 0.0 {
203 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
204 }
205 }
206
207 NavigationMode::Turntable => {
208 if nav.orbit != glam::Vec2::ZERO {
209 let yaw = nav.orbit.x * self.orbit_sensitivity;
210 let pitch = nav.orbit.y * self.orbit_sensitivity;
211 apply_turntable(camera, yaw, pitch);
212 }
213 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
214 apply_turntable(camera, nav.twist * self.gesture_sensitivity, 0.0);
216 }
217 if nav.pan != glam::Vec2::ZERO {
218 camera.pan_pixels(nav.pan, h);
219 }
220 if nav.zoom != 0.0 {
221 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
222 }
223 }
224
225 NavigationMode::Planar => {
226 if nav.pan != glam::Vec2::ZERO {
228 camera.pan_pixels(nav.pan, h);
229 }
230 if nav.zoom != 0.0 {
231 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
232 }
233 }
234
235 NavigationMode::Fly => {
236 if nav.orbit != glam::Vec2::ZERO {
238 let yaw = nav.orbit.x * self.orbit_sensitivity;
239 let pitch = nav.orbit.y * self.orbit_sensitivity;
240 apply_fly_look(camera, yaw, pitch);
241 }
242 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
243 apply_fly_look(camera, nav.twist * self.gesture_sensitivity, 0.0);
244 }
245
246 let forward = -(camera.orientation * glam::Vec3::Z);
248 let right = camera.orientation * glam::Vec3::X;
249 let up = camera.orientation * glam::Vec3::Y;
250 let speed = self.fly_speed;
251
252 let mut move_delta = glam::Vec3::ZERO;
253 if frame.is_active(Action::FlyForward) {
254 move_delta += forward * speed;
255 }
256 if frame.is_active(Action::FlyBackward) {
257 move_delta -= forward * speed;
258 }
259 if frame.is_active(Action::FlyRight) {
260 move_delta += right * speed;
261 }
262 if frame.is_active(Action::FlyLeft) {
263 move_delta -= right * speed;
264 }
265 if frame.is_active(Action::FlyUp) {
266 move_delta += up * speed;
267 }
268 if frame.is_active(Action::FlyDown) {
269 move_delta -= up * speed;
270 }
271 camera.center += move_delta;
273 }
274 }
275 }
276}
277
278fn apply_turntable(camera: &mut Camera, yaw: f32, pitch: f32) {
287 if yaw != 0.0 {
289 camera.orientation = (glam::Quat::from_rotation_z(-yaw) * camera.orientation).normalize();
290 }
291
292 if pitch != 0.0 {
293 let proposed = (camera.orientation * glam::Quat::from_rotation_x(-pitch)).normalize();
295
296 let max_sin_el = 89.0_f32.to_radians().sin(); let eye_z = (proposed * glam::Vec3::Z).z;
301
302 if eye_z.abs() <= max_sin_el {
303 camera.orientation = proposed;
304 }
305 }
307}
308
309fn apply_fly_look(camera: &mut Camera, yaw: f32, pitch: f32) {
314 let eye = camera.eye_position();
315 camera.orientation = (glam::Quat::from_rotation_z(-yaw)
316 * camera.orientation
317 * glam::Quat::from_rotation_x(-pitch))
318 .normalize();
319 camera.center = eye - camera.orientation * (glam::Vec3::Z * camera.distance);
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::interaction::input::binding::KeyCode;
327 use crate::interaction::input::event::{ButtonState, ScrollUnits, ViewportEvent};
328
329 fn make_ctx() -> ViewportContext {
330 ViewportContext {
331 hovered: true,
332 focused: true,
333 viewport_size: [800.0, 600.0],
334 }
335 }
336
337 #[test]
338 fn new_defaults() {
339 let ctrl = OrbitCameraController::viewport_primitives();
340 assert_eq!(ctrl.navigation_mode, NavigationMode::Arcball);
341 assert!((ctrl.fly_speed - OrbitCameraController::DEFAULT_FLY_SPEED).abs() < 1e-6);
342 assert!(
343 (ctrl.orbit_sensitivity - OrbitCameraController::DEFAULT_ORBIT_SENSITIVITY).abs()
344 < 1e-6
345 );
346 }
347
348 #[test]
349 fn resolve_no_events_zero_nav() {
350 let mut ctrl = OrbitCameraController::viewport_all();
351 ctrl.begin_frame(make_ctx());
352 let frame = ctrl.resolve();
353 assert_eq!(frame.navigation.orbit, glam::Vec2::ZERO);
354 assert_eq!(frame.navigation.pan, glam::Vec2::ZERO);
355 assert_eq!(frame.navigation.zoom, 0.0);
356 }
357
358 #[test]
359 fn apply_zoom_changes_distance() {
360 let mut ctrl = OrbitCameraController::viewport_primitives();
361 ctrl.begin_frame(make_ctx());
362 let mut cam = Camera::default();
363 let d0 = cam.distance;
364 ctrl.push_event(ViewportEvent::Wheel {
365 delta: glam::Vec2::new(0.0, 100.0),
366 units: ScrollUnits::Pixels,
367 });
368 ctrl.apply_to_camera(&mut cam);
369 assert!(
370 (cam.distance - d0).abs() > 1e-4,
371 "zoom should change camera distance"
372 );
373 }
374
375 #[test]
376 fn planar_mode_ignores_orbit() {
377 let mut ctrl = OrbitCameraController::viewport_primitives();
378 ctrl.navigation_mode = NavigationMode::Planar;
379 ctrl.begin_frame(make_ctx());
380 let mut cam = Camera::default();
381 let orient_before = cam.orientation;
382 ctrl.push_event(ViewportEvent::PointerMoved {
384 position: glam::Vec2::new(100.0, 100.0),
385 });
386 ctrl.push_event(ViewportEvent::MouseButton {
387 button: crate::interaction::input::binding::MouseButton::Left,
388 state: ButtonState::Pressed,
389 });
390 ctrl.push_event(ViewportEvent::PointerMoved {
391 position: glam::Vec2::new(200.0, 200.0),
392 });
393 ctrl.apply_to_camera(&mut cam);
394 assert!(
395 (cam.orientation.x - orient_before.x).abs() < 1e-6
396 && (cam.orientation.y - orient_before.y).abs() < 1e-6
397 && (cam.orientation.z - orient_before.z).abs() < 1e-6
398 && (cam.orientation.w - orient_before.w).abs() < 1e-6,
399 "planar mode should not change orientation"
400 );
401 }
402
403 #[test]
404 fn turntable_pitch_clamped() {
405 let mut cam = Camera::default();
406 for _ in 0..1000 {
408 apply_turntable(&mut cam, 0.0, 0.1);
409 }
410 let eye_z = (cam.orientation * glam::Vec3::Z).z;
412 let max_sin = 89.0_f32.to_radians().sin();
413 assert!(
414 eye_z.abs() <= max_sin + 1e-4,
415 "turntable pitch should be clamped: eye_z={eye_z}"
416 );
417 }
418
419 #[test]
420 fn fly_look_preserves_eye() {
421 let mut cam = Camera::default();
422 let eye_before = cam.eye_position();
423 apply_fly_look(&mut cam, 0.3, 0.2);
424 let eye_after = cam.eye_position();
425 let diff = (eye_after - eye_before).length();
426 assert!(
427 diff < 1e-3,
428 "firstperson look should preserve eye position, diff={diff}"
429 );
430 }
431
432 #[test]
433 fn fly_moves_camera() {
434 let mut ctrl = OrbitCameraController::viewport_all();
435 ctrl.navigation_mode = NavigationMode::Fly;
436 ctrl.fly_speed = 1.0;
437 ctrl.begin_frame(make_ctx());
438 let mut cam = Camera::default();
439 cam.center = glam::Vec3::ZERO;
440 cam.orientation = glam::Quat::IDENTITY;
441 let center_before = cam.center;
442 ctrl.push_event(ViewportEvent::Key {
443 key: KeyCode::W,
444 state: ButtonState::Pressed,
445 repeat: false,
446 });
447 ctrl.apply_to_camera(&mut cam);
448 assert!(
449 (cam.center - center_before).length() > 0.5,
450 "FlyForward should move camera center"
451 );
452 }
453
454 #[test]
455 fn apply_consumes_external_frame() {
456 let ctrl = OrbitCameraController::viewport_primitives();
460 let mut cam = Camera::default();
461 let before = cam.orientation;
462 let mut frame = ActionFrame::default();
463 frame.navigation.orbit = glam::Vec2::new(20.0, 0.0);
464 ctrl.apply(&mut cam, &frame);
465 assert_ne!(
466 cam.orientation, before,
467 "apply(&frame) should orbit from the frame's nav delta"
468 );
469 }
470}