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