diff --git a/crates/fj-viewer/src/camera.rs b/crates/fj-viewer/src/camera.rs index 0a3d5f8a3..5698aca0d 100644 --- a/crates/fj-viewer/src/camera.rs +++ b/crates/fj-viewer/src/camera.rs @@ -4,7 +4,7 @@ use std::f64::consts::FRAC_PI_2; use fj_interop::mesh::Mesh; use fj_math::{Aabb, Point, Scalar, Transform, Triangle, Vector}; -use crate::screen::{Position, Size}; +use crate::screen::NormalizedPosition; /// The camera abstraction /// @@ -104,24 +104,15 @@ impl Camera { .inverse_transform_point(&Point::<3>::origin()) } - /// Transform the position of the cursor on the near plane to model space. + /// Transform a normalized cursor position on the near plane to model space. pub fn cursor_to_model_space( &self, - cursor: Position, - size: Size, + cursor: NormalizedPosition, ) -> Point<3> { - let [width, height] = size.as_f64(); - let aspect_ratio = width / height; - - // Cursor position in normalized coordinates (-1 to +1) with - // aspect ratio taken into account. - let x = cursor.x / width * 2. - 1.; - let y = -(cursor.y / height * 2. - 1.) / aspect_ratio; - // Cursor position in camera space. let f = (self.field_of_view_in_x() / 2.).tan() * self.near_plane(); - let cursor = - Point::origin() + Vector::from([x * f, y * f, -self.near_plane()]); + let cursor = Point::origin() + + Vector::from([cursor.x * f, cursor.y * f, -self.near_plane()]); self.camera_to_model().inverse_transform_point(&cursor) } @@ -129,8 +120,7 @@ impl Camera { /// Compute the point on the model, that the cursor currently points to. pub fn focus_point( &self, - size: Size, - cursor: Option, + cursor: Option, mesh: &Mesh>, ) -> FocusPoint { let cursor = match cursor { @@ -140,7 +130,7 @@ impl Camera { // Transform camera and cursor positions to model space. let origin = self.position(); - let cursor = self.cursor_to_model_space(cursor, size); + let cursor = self.cursor_to_model_space(cursor); let dir = (cursor - origin).normalize(); let mut min_t = None; diff --git a/crates/fj-viewer/src/input/event.rs b/crates/fj-viewer/src/input/event.rs index 0160e578e..357cded83 100644 --- a/crates/fj-viewer/src/input/event.rs +++ b/crates/fj-viewer/src/input/event.rs @@ -1,52 +1,23 @@ -use crate::screen::Position; +use crate::screen::NormalizedPosition; /// An input event pub enum Event { - /// The cursor has moved to another position - CursorMoved(Position), - - /// A key has been pressed or released - Key(Key, KeyState), - - /// The user scrolled - Scroll(MouseScrollDelta), -} - -/// Describes a difference in the vertical mouse scroll wheel state. -/// Positive values indicate movement forward (away from the user). -pub enum MouseScrollDelta { - /// Amount in lines to scroll. - Line(f64), - /// Amount in pixels to scroll. - Pixel(f64), -} - -/// A keyboard or mouse key -pub enum Key { - /// The escape key - Escape, - - /// The numerical key `1` - Key1, - - /// The numerical key `2` - Key2, - - /// The numerical key `3` - Key3, - - /// The left mouse key - MouseLeft, - - /// The right mouse key - MouseRight, -} - -/// Defines the meaning of a key event -pub enum KeyState { - /// A key was pressed - Pressed, - - /// A key was released - Released, + /// Move the model up, down, left or right + Translate { + /// The normalized position of the cursor before input + previous: NormalizedPosition, + /// The normalized position of the cursor after input + current: NormalizedPosition, + }, + + /// Rotate the model around the focus point + Rotation { + /// The angle around the screen x axis to rotate (in radians) + angle_x: f64, + /// The angle around the screen y axis to rotate (in radians) + angle_y: f64, + }, + + /// Move the view forwards and backwards + Zoom(f64), } diff --git a/crates/fj-viewer/src/input/handler.rs b/crates/fj-viewer/src/input/handler.rs index 7bc24446c..71f4212bd 100644 --- a/crates/fj-viewer/src/input/handler.rs +++ b/crates/fj-viewer/src/input/handler.rs @@ -1,20 +1,11 @@ -use fj_interop::mesh::Mesh; -use fj_math::Point; - -use super::{ - event::KeyState, movement::Movement, rotation::Rotation, zoom::Zoom, Event, - Key, -}; -use crate::{ - camera::Camera, - screen::{Position, Size}, -}; +use super::{movement::Movement, rotation::Rotation, zoom::Zoom, Event}; +use crate::camera::{Camera, FocusPoint}; /// Input handling abstraction /// /// Takes user input and applies them to application state. pub struct Handler { - cursor: Option, + focus_point: FocusPoint, movement: Movement, rotation: Rotation, @@ -22,115 +13,39 @@ pub struct Handler { } impl Handler { - /// Returns the state of the cursor position. - pub fn cursor(&self) -> Option { - self.cursor - } - /// Handle an input event - pub fn handle_event( - &mut self, - event: Event, - screen_size: Size, - mesh: &Mesh>, - camera: &mut Camera, - actions: &mut Actions, - ) { + pub fn handle_event(&mut self, event: Event, camera: &mut Camera) { match event { - Event::CursorMoved(position) => { - if let Some(previous) = self.cursor { - let diff_x = position.x - previous.x; - let diff_y = position.y - previous.y; - - self.movement.apply(self.cursor, camera, screen_size); - self.rotation.apply(diff_x, diff_y, camera); - } - - self.cursor = Some(position); - } - Event::Key(Key::Escape, KeyState::Pressed) => actions.exit = true, - - Event::Key(Key::Key1, KeyState::Pressed) => { - actions.toggle_model = true - } - Event::Key(Key::Key2, KeyState::Pressed) => { - actions.toggle_mesh = true - } - Event::Key(Key::Key3, KeyState::Pressed) => { - actions.toggle_debug = true - } - - Event::Key(Key::MouseLeft, KeyState::Pressed) => { - let focus_point = - camera.focus_point(screen_size, self.cursor(), mesh); - - self.rotation.start(focus_point); - } - Event::Key(Key::MouseLeft, KeyState::Released) => { - self.rotation.stop(); + Event::Translate { previous, current } => self.movement.apply( + previous, + current, + &self.focus_point, + camera, + ), + Event::Rotation { angle_x, angle_y } => { + self.rotation + .apply(angle_x, angle_y, &self.focus_point, camera) + } + Event::Zoom(zoom_delta) => { + self.zoom.apply(zoom_delta, &self.focus_point, camera) } - Event::Key(Key::MouseRight, KeyState::Pressed) => { - let focus_point = - camera.focus_point(screen_size, self.cursor(), mesh); - - self.movement.start(focus_point, self.cursor); - } - Event::Key(Key::MouseRight, KeyState::Released) => { - self.movement.stop(); - } - - Event::Scroll(delta) => { - self.zoom.push(delta); - } - - _ => {} } } - /// Update application state from user input. - pub fn update( - &mut self, - delta_t: f64, - camera: &mut Camera, - screen_size: Size, - mesh: &Mesh>, - ) { - let focus_point = camera.focus_point(screen_size, self.cursor(), mesh); - self.zoom.apply_to_camera(delta_t, focus_point, camera); + /// A new focus point was selected (or deselected) + pub fn focus(&mut self, focus_point: FocusPoint) { + self.focus_point = focus_point; } } impl Default for Handler { fn default() -> Self { Self { - cursor: None, + focus_point: FocusPoint::none(), - movement: Movement::new(), - rotation: Rotation::new(), - zoom: Zoom::new(), + movement: Movement, + rotation: Rotation, + zoom: Zoom, } } } - -/// Intermediate input state container -/// -/// Used as a per frame state container for sending application state to `winit`. -#[derive(Default)] -pub struct Actions { - /// Application exit state. - pub exit: bool, - - /// Toggle for the shaded display of the model. - pub toggle_model: bool, - /// Toggle for the model's wireframe. - pub toggle_mesh: bool, - /// Toggle for debug information. - pub toggle_debug: bool, -} - -impl Actions { - /// Returns a new `Actions`. - pub fn new() -> Self { - Self::default() - } -} diff --git a/crates/fj-viewer/src/input/mod.rs b/crates/fj-viewer/src/input/mod.rs index f206600f1..6471f9dfb 100644 --- a/crates/fj-viewer/src/input/mod.rs +++ b/crates/fj-viewer/src/input/mod.rs @@ -6,7 +6,4 @@ mod movement; mod rotation; mod zoom; -pub use self::{ - event::{Event, Key, KeyState, MouseScrollDelta}, - handler::{Actions, Handler}, -}; +pub use self::{event::Event, handler::Handler}; diff --git a/crates/fj-viewer/src/input/movement.rs b/crates/fj-viewer/src/input/movement.rs index b1e4aeba8..058093207 100644 --- a/crates/fj-viewer/src/input/movement.rs +++ b/crates/fj-viewer/src/input/movement.rs @@ -2,57 +2,35 @@ use fj_math::{Point, Scalar, Transform, Vector}; use crate::{ camera::{Camera, FocusPoint}, - screen::{Position, Size}, + screen::NormalizedPosition, }; -pub struct Movement { - focus_point: FocusPoint, - cursor: Option, -} +pub struct Movement; impl Movement { - pub fn new() -> Self { - Self { - focus_point: FocusPoint::none(), - cursor: None, - } - } - - pub fn start(&mut self, focus_point: FocusPoint, cursor: Option) { - self.focus_point = focus_point; - self.cursor = cursor; - } - - pub fn stop(&mut self) { - self.focus_point = FocusPoint::none(); - } - pub fn apply( &mut self, - cursor: Option, + previous: NormalizedPosition, + current: NormalizedPosition, + focus_point: &FocusPoint, camera: &mut Camera, - size: Size, ) { - if let (Some(previous), Some(cursor)) = (self.cursor, cursor) { - let previous = camera.cursor_to_model_space(previous, size); - let cursor = camera.cursor_to_model_space(cursor, size); - - if let Some(focus_point) = self.focus_point.0 { - let d1 = Point::distance(&camera.position(), &cursor); - let d2 = Point::distance(&camera.position(), &focus_point); - - let diff = (cursor - previous) * d2 / d1; - let offset = camera.camera_to_model().transform_vector(&diff); - - camera.translation = camera.translation - * Transform::translation(Vector::from([ - offset.x, - offset.y, - Scalar::ZERO, - ])); - } + let previous = camera.cursor_to_model_space(previous); + let cursor = camera.cursor_to_model_space(current); + + if let Some(focus_point) = focus_point.0 { + let d1 = Point::distance(&camera.position(), &cursor); + let d2 = Point::distance(&camera.position(), &focus_point); + + let diff = (cursor - previous) * d2 / d1; + let offset = camera.camera_to_model().transform_vector(&diff); + + camera.translation = camera.translation + * Transform::translation(Vector::from([ + offset.x, + offset.y, + Scalar::ZERO, + ])); } - - self.cursor = cursor; } } diff --git a/crates/fj-viewer/src/input/rotation.rs b/crates/fj-viewer/src/input/rotation.rs index 84f627a31..ca10260c3 100644 --- a/crates/fj-viewer/src/input/rotation.rs +++ b/crates/fj-viewer/src/input/rotation.rs @@ -2,56 +2,35 @@ use fj_math::{Point, Transform, Vector}; use crate::camera::{Camera, FocusPoint}; -pub struct Rotation { - active: bool, - focus_point: FocusPoint, -} +pub struct Rotation; impl Rotation { - pub fn new() -> Self { - Self { - active: false, - focus_point: FocusPoint::none(), - } - } - - pub fn start(&mut self, focus_point: FocusPoint) { - self.active = true; - self.focus_point = focus_point; - } - - pub fn stop(&mut self) { - self.active = false; - } - - pub fn apply(&self, diff_x: f64, diff_y: f64, camera: &mut Camera) { - if self.active { - let rotate_around: Vector<3> = - self.focus_point.0.unwrap_or_else(Point::origin).coords; - - let f = 0.005; - - let angle_x = diff_y * f; - let angle_y = diff_x * f; - - let rotate_around = Transform::translation(rotate_around); - - // the model rotates not the camera, so invert the transform - let camera_rotation = camera.rotation.inverse(); - let right_vector = right_vector(&camera_rotation); - let up_vector = up_vector(&camera_rotation); - - let rotation = Transform::rotation(right_vector * angle_x) - * Transform::rotation(up_vector * angle_y); - - let transform = camera.camera_to_model() - * rotate_around - * rotation - * rotate_around.inverse(); - - camera.rotation = transform.extract_rotation(); - camera.translation = transform.extract_translation(); - } + pub fn apply( + &self, + angle_x: f64, + angle_y: f64, + focus_point: &FocusPoint, + camera: &mut Camera, + ) { + let rotate_around: Vector<3> = + focus_point.0.unwrap_or_else(Point::origin).coords; + let rotate_around = Transform::translation(rotate_around); + + // the model rotates not the camera, so invert the transform + let camera_rotation = camera.rotation.inverse(); + let right_vector = right_vector(&camera_rotation); + let up_vector = up_vector(&camera_rotation); + + let rotation = Transform::rotation(right_vector * angle_x) + * Transform::rotation(up_vector * angle_y); + + let transform = camera.camera_to_model() + * rotate_around + * rotation + * rotate_around.inverse(); + + camera.rotation = transform.extract_rotation(); + camera.translation = transform.extract_translation(); } } diff --git a/crates/fj-viewer/src/input/zoom.rs b/crates/fj-viewer/src/input/zoom.rs index b95765281..c6ddcb212 100644 --- a/crates/fj-viewer/src/input/zoom.rs +++ b/crates/fj-viewer/src/input/zoom.rs @@ -2,54 +2,21 @@ use fj_math::{Transform, Vector}; use crate::camera::{Camera, FocusPoint}; -use super::event::MouseScrollDelta; - -pub struct Zoom { - accumulated_delta: f64, -} +pub struct Zoom; impl Zoom { - pub fn new() -> Self { - Self { - accumulated_delta: 0.0, - } - } - - pub fn push(&mut self, delta: MouseScrollDelta) { - // Accumulate all zoom inputs - self.accumulated_delta += match delta { - MouseScrollDelta::Line(delta) => ZOOM_FACTOR_LINE * delta, - MouseScrollDelta::Pixel(delta) => ZOOM_FACTOR_PIXEL * delta, - }; - } - - pub fn apply_to_camera( + pub fn apply( &mut self, - delta_t: f64, - focus_point: FocusPoint, + zoom_delta: f64, + focus_point: &FocusPoint, camera: &mut Camera, ) { let distance = match focus_point.0 { Some(fp) => (fp - camera.position()).magnitude(), None => camera.position().coords.magnitude(), }; - let displacement = - self.accumulated_delta * delta_t * distance.into_f64(); + let displacement = zoom_delta * distance.into_f64(); camera.translation = camera.translation * Transform::translation(Vector::from([0.0, 0.0, -displacement])); - - self.accumulated_delta = 0.; } } - -/// Affects the speed of zoom movement given a scroll wheel input in lines. -/// -/// Smaller values will move the camera less with the same input. -/// Larger values will move the camera more with the same input. -const ZOOM_FACTOR_LINE: f64 = 15.0; - -/// Affects the speed of zoom movement given a scroll wheel input in pixels. -/// -/// Smaller values will move the camera less with the same input. -/// Larger values will move the camera more with the same input. -const ZOOM_FACTOR_PIXEL: f64 = 1.0; diff --git a/crates/fj-viewer/src/screen.rs b/crates/fj-viewer/src/screen.rs index 5b3e8678e..09c27dff3 100644 --- a/crates/fj-viewer/src/screen.rs +++ b/crates/fj-viewer/src/screen.rs @@ -14,13 +14,14 @@ pub trait Screen { fn window(&self) -> &Self::Window; } -/// A position on the screen +/// Cursor position in normalized coordinates (-1 to +1) with aspect ratio taken into account. +/// i.e. the center of the screen is at (0, 0) #[derive(Clone, Copy, Debug)] -pub struct Position { - /// The x coordinate of the position +pub struct NormalizedPosition { + /// The x coordinate of the position [-1, 1] pub x: f64, - /// The y coordinate of the position + /// The y coordinate of the position [-1, 1] pub y: f64, } diff --git a/crates/fj-window/src/run.rs b/crates/fj-window/src/run.rs index a732d6026..d953eab4a 100644 --- a/crates/fj-window/src/run.rs +++ b/crates/fj-window/src/run.rs @@ -3,15 +3,15 @@ //! Provides the functionality to create a window and perform basic viewing //! with programmed models. -use std::{error, time::Instant}; +use std::error; use fj_host::Watcher; -use fj_operations::shape_processor::ShapeProcessor; +use fj_operations::shape_processor::{ProcessedShape, ShapeProcessor}; use fj_viewer::{ - camera::Camera, + camera::{Camera, FocusPoint}, graphics::{self, DrawConfig, Renderer}, - input::{self, KeyState}, - screen::{Position, Screen as _, Size}, + input, + screen::{NormalizedPosition, Screen as _, Size}, }; use futures::executor::block_on; use tracing::{trace, warn}; @@ -34,7 +34,8 @@ pub fn run( let event_loop = EventLoop::new(); let window = Window::new(&event_loop)?; - let mut previous_time = Instant::now(); + let mut previous_cursor = None; + let mut held_mouse_button = None; let mut input_handler = input::Handler::default(); let mut renderer = block_on(Renderer::new(&window))?; @@ -47,10 +48,6 @@ pub fn run( event_loop.run(move |event, _, control_flow| { trace!("Handling event: {:?}", event); - let mut actions = input::Actions::new(); - - let now = Instant::now(); - if let Some(new_shape) = watcher.receive() { match shape_processor.process(&new_shape) { Ok(new_shape) => { @@ -113,27 +110,13 @@ pub fn run( .on_event(&renderer.egui.context, window_event); } - // - - let event = match event { + // fj-window events + match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { *control_flow = ControlFlow::Exit; - None - } - Event::WindowEvent { - event: WindowEvent::Resized(size), - .. - } => { - let size = Size { - width: size.width, - height: size.height, - }; - renderer.handle_resize(size); - - None } Event::WindowEvent { event: @@ -148,78 +131,39 @@ pub fn run( }, .. } => match virtual_key_code { - VirtualKeyCode::Escape => Some(input::Event::Key( - input::Key::Escape, - KeyState::Pressed, - )), + VirtualKeyCode::Escape => *control_flow = ControlFlow::Exit, VirtualKeyCode::Key1 => { - Some(input::Event::Key(input::Key::Key1, KeyState::Pressed)) + draw_config.draw_model = !draw_config.draw_model } VirtualKeyCode::Key2 => { - Some(input::Event::Key(input::Key::Key2, KeyState::Pressed)) + draw_config.draw_mesh = !draw_config.draw_mesh } VirtualKeyCode::Key3 => { - Some(input::Event::Key(input::Key::Key3, KeyState::Pressed)) + draw_config.draw_debug = !draw_config.draw_debug } - - _ => None, + _ => {} }, Event::WindowEvent { - event: WindowEvent::CursorMoved { position, .. }, + event: WindowEvent::Resized(size), .. } => { - let position = Position { - x: position.x, - y: position.y, + let size = Size { + width: size.width, + height: size.height, }; - Some(input::Event::CursorMoved(position)) + renderer.handle_resize(size); } Event::WindowEvent { event: WindowEvent::MouseInput { state, button, .. }, .. } => { - let state = match state { - ElementState::Pressed => input::KeyState::Pressed, - ElementState::Released => input::KeyState::Released, + match state { + ElementState::Pressed => held_mouse_button = Some(button), + ElementState::Released => held_mouse_button = None, }; - - match button { - MouseButton::Left => { - Some(input::Event::Key(input::Key::MouseLeft, state)) - } - MouseButton::Right => { - Some(input::Event::Key(input::Key::MouseRight, state)) - } - _ => None, - } } - Event::WindowEvent { - event: WindowEvent::MouseWheel { delta, .. }, - .. - } => Some(input::Event::Scroll(match delta { - MouseScrollDelta::LineDelta(_, y) => { - input::MouseScrollDelta::Line(y as f64) - } - MouseScrollDelta::PixelDelta(PhysicalPosition { - y, .. - }) => input::MouseScrollDelta::Pixel(y), - })), Event::MainEventsCleared => { - let delta_t = now.duration_since(previous_time); - previous_time = now; - - if let (Some(shape), Some(camera)) = (&shape, &mut camera) { - input_handler.update( - delta_t.as_secs_f64(), - camera, - window.size(), - &shape.mesh, - ); - } - window.window().request_redraw(); - - None } Event::RedrawRequested(_) => { if let (Some(shape), Some(camera)) = (&shape, &mut camera) { @@ -231,39 +175,115 @@ pub fn run( warn!("Draw error: {}", err); } } - - None } - _ => None, - }; - - if let (Some(event), Some(shape), Some(camera)) = - (event, &shape, &mut camera) - { - input_handler.handle_event( - event, - window.size(), - &shape.mesh, - camera, - &mut actions, - ); + _ => {} } - if actions.exit { - *control_flow = ControlFlow::Exit; - } - if actions.toggle_model { - draw_config.draw_model = !draw_config.draw_model; - } - if actions.toggle_mesh { - draw_config.draw_mesh = !draw_config.draw_mesh; + // fj-viewer events + if let (Some(shape), Some(camera)) = (&shape, &mut camera) { + if let Some(focus_event) = + focus(&event, previous_cursor, shape, camera) + { + input_handler.focus(focus_event); + } } - if actions.toggle_debug { - draw_config.draw_debug = !draw_config.draw_debug; + + let input_event = input_event( + &event, + &window, + &held_mouse_button, + &mut previous_cursor, + ); + if let (Some(input_event), Some(camera)) = (input_event, &mut camera) { + input_handler.handle_event(input_event, camera); } }); } +fn input_event( + event: &Event<()>, + window: &Window, + held_mouse_button: &Option, + previous_cursor: &mut Option, +) -> Option { + match event { + Event::WindowEvent { + event: WindowEvent::CursorMoved { position, .. }, + .. + } => { + let [width, height] = window.size().as_f64(); + let aspect_ratio = width / height; + + // Cursor position in normalized coordinates (-1 to +1) with + // aspect ratio taken into account. + let current = NormalizedPosition { + x: position.x / width * 2. - 1., + y: -(position.y / height * 2. - 1.) / aspect_ratio, + }; + let event = match (*previous_cursor, held_mouse_button) { + (Some(previous), Some(button)) => match button { + MouseButton::Left => { + let diff_x = current.x - previous.x; + let diff_y = current.y - previous.y; + let angle_x = -diff_y * ROTATION_SENSITIVITY; + let angle_y = diff_x * ROTATION_SENSITIVITY; + + Some(input::Event::Rotation { angle_x, angle_y }) + } + MouseButton::Right => { + Some(input::Event::Translate { previous, current }) + } + _ => None, + }, + _ => None, + }; + *previous_cursor = Some(current); + event + } + Event::WindowEvent { + event: WindowEvent::MouseWheel { delta, .. }, + .. + } => Some(input::Event::Zoom(match delta { + MouseScrollDelta::LineDelta(_, y) => (*y as f64) * ZOOM_FACTOR_LINE, + MouseScrollDelta::PixelDelta(PhysicalPosition { y, .. }) => { + y * ZOOM_FACTOR_PIXEL + } + })), + _ => None, + } +} + +fn focus( + event: &Event<()>, + previous_cursor: Option, + shape: &ProcessedShape, + camera: &Camera, +) -> Option { + let focus_point = match event { + Event::WindowEvent { + event: + WindowEvent::MouseInput { + state, + button: MouseButton::Left | MouseButton::Right, + .. + }, + .. + } => match state { + ElementState::Pressed => { + camera.focus_point(previous_cursor, &shape.mesh) + } + ElementState::Released => FocusPoint::none(), + }, + Event::WindowEvent { + event: WindowEvent::MouseWheel { .. }, + .. + } => camera.focus_point(previous_cursor, &shape.mesh), + + _ => return None, + }; + Some(focus_point) +} + /// Error in main loop #[derive(Debug, thiserror::Error)] pub enum Error { @@ -275,3 +295,21 @@ pub enum Error { #[error("Error initializing graphics")] GraphicsInit(#[from] graphics::InitError), } + +/// Affects the speed of zoom movement given a scroll wheel input in lines. +/// +/// Smaller values will move the camera less with the same input. +/// Larger values will move the camera more with the same input. +const ZOOM_FACTOR_LINE: f64 = 0.075; + +/// Affects the speed of zoom movement given a scroll wheel input in pixels. +/// +/// Smaller values will move the camera less with the same input. +/// Larger values will move the camera more with the same input. +const ZOOM_FACTOR_PIXEL: f64 = 0.005; + +/// Affects the speed of rotation given a change in normalized screen position [-1, 1] +/// +/// Smaller values will move the camera less with the same input. +/// Larger values will move the camera more with the same input. +const ROTATION_SENSITIVITY: f64 = 5.;