diff --git a/Cargo.toml b/Cargo.toml index 502b727..2f79479 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kiss3d-trackball" -version = "0.2.0" +version = "0.3.0" authors = ["Rouven Spreckels "] edition = "2018" description = "Virtual Trackball Camera Mode for Kiss3D" @@ -32,7 +32,7 @@ travis-ci = { repository = "qu1x/kiss3d-trackball" } [dependencies] kiss3d = "0.31" nalgebra = "0.26" -trackball = "0.3" +trackball = "0.5" [features] cc = ["trackball/cc"] diff --git a/README.md b/README.md index d5b530a..f1f3a2d 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,12 @@ This license is designed to provide: * a simple permissive license, * that is compatible with the [`GPL-2.0-or-later`], and - * which also has an express patent grant included. + * which also has an express patent grant included, but + * unlike the [`Apache-2.0`] without patent retaliation. [`BSD-2-Clause-Patent`]: https://spdx.org/licenses/BSD-2-Clause-Patent.html [`GPL-2.0-or-later`]: https://spdx.org/licenses/GPL-2.0-or-later.html +[`Apache-2.0`]: https://spdx.org/licenses/Apache-2.0.html ## Contribution diff --git a/RELEASES.md b/RELEASES.md index decbf90..f4fe7d5 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,9 @@ +# Version 0.3.0 (2021-04-28) + + * Add first person view. + * Use move semantics whenever otherwise cloning borrowed method arguments. + * Reorder arguments of `Trackball::new()` and `Trackball::new_with_frustum()`. + # Version 0.2.0 (2021-04-13) * Use `Fixed` quantity wrt field of view. diff --git a/src/input.rs b/src/input.rs index 613644a..bef8cea 100644 --- a/src/input.rs +++ b/src/input.rs @@ -6,6 +6,7 @@ use std::marker::PhantomData; #[derive(Debug, Clone)] pub struct Input { phantom_data: PhantomData, + first_key: Option, ortho_key: Option, reset_key: Option, orbit_button: Option, @@ -18,6 +19,7 @@ impl Default for Input { fn default() -> Self { Self { phantom_data: PhantomData, + first_key: Some(Key::LShift), ortho_key: Some(Key::O), reset_key: Some(Key::Return), orbit_button: Some(MouseButton::Button1), @@ -29,13 +31,23 @@ impl Default for Input { } impl Input { + /// Key used to enable first person view as long as being pressed. + pub fn first_key(&self) -> Option { + self.first_key + } + /// Sets key used to enable first person view as long as being pressed. + /// + /// Use `None` to disable key. + pub fn rebind_first_key(&mut self, key: Option) { + self.first_key = key; + } /// Key used to switch between orthographic and perspective projection. pub fn ortho_key(&self) -> Option { self.ortho_key } /// Sets key used to switch between orthographic and perspective projection. /// - /// Use `None` to disable switch. + /// Use `None` to disable key. pub fn rebind_ortho_key(&mut self, key: Option) { self.ortho_key = key; } @@ -45,7 +57,7 @@ impl Input { } /// Sets key used to reset camera. /// - /// Use `None` to disable reset. + /// Use `None` to disable key. pub fn rebind_reset_key(&mut self, key: Option) { self.reset_key = key; } @@ -55,7 +67,7 @@ impl Input { } /// Sets button used to orbit camera. /// - /// Use `None` to disable rotation. + /// Use `None` to disable button. pub fn rebind_orbit_button(&mut self, button: Option) { self.orbit_button = button; } @@ -78,7 +90,7 @@ impl Input { } /// Sets button used to slide camera. /// - /// Use `None` to disable slide. + /// Use `None` to disable button. pub fn rebind_slide_button(&mut self, button: Option) { self.slide_button = button; } diff --git a/src/lib.rs b/src/lib.rs index c7d9f16..35d09ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ //! Complements common [`trackball`] operation handlers with [`kiss3d`]-specific [`Input`] resulting //! in a compound [`Trackball`] [`Camera`] mode implementation for the [`kiss3d`] graphics library. +#![allow(clippy::collapsible_else_if)] + use kiss3d::{ camera::Camera, event::{Action, TouchAction, WindowEvent}, @@ -10,7 +12,7 @@ use kiss3d::{ window::Canvas, }; use nalgebra::{Isometry3, Matrix4, Point2, Point3, UnitQuaternion, Vector3}; -use trackball::{Clamp, Frame, Image, Orbit, Scale, Scene, Slide, Touch}; +use trackball::{Clamp, First, Frame, Image, Orbit, Scale, Scene, Slide, Touch}; mod input; pub use input::*; @@ -19,24 +21,25 @@ pub use trackball::Fixed; /// Trackball camera mode. /// -/// A trackball camera is a camera working similarly like a trackball device. The camera eye is -/// orbited around its target, the trackball's center, and always looking at it. This implementation -/// is split into several members defined by the [`trackball`] crate categorizing all the methods to -/// control the different aspects of a camera. +/// A trackball camera is a camera working similarly like a trackball device. The camera eye orbits +/// around its target, the trackball's center, and always looks at it. This implementation is split +/// into several members defined by the [`trackball`] crate categorizing all the methods to control +/// the different aspects of a camera. /// -/// # Camera Controls +/// # Camera Inputs /// -/// Following default controls are defined which are customizable via [`Self::input`]: +/// Following default inputs are defined which are customizable via [`Self::input`]: /// -/// Mouse | Touch | Action -/// --------------------------- | ------------------------- | -------------------------------------- -/// Left Button Press + Drag | One-Finger + Drag | Orbits eye around target. -/// ↳ but at trackball's border | Two-Finger + Roll | Purely rolls eye about view direction. -/// Right Button Press + Drag | Two-Finger + Drag | Slides trackball along focus plane. -/// Scroll In/Out | Two-Finger + Pinch Out/In | Scales distance zooming in/out. -/// Left Button Press + Release | Any-Finger + Release | Slides to cursor/finger position. +/// Mouse | Touch | Action +/// --------------------------- | ------------------------------ | --------------------------------- +/// Left Button Press + Drag | One-Finger + Drag | Orbits around target. +/// ↳ but at trackball's border | Two-Finger + Roll | Rolls about view direction. +/// Drag + Left Shift | Any-Finger + Drag + Left Shift | First person view. +/// Right Button Press + Drag | Two-Finger + Drag | Slides trackball on focus plane. +/// Scroll In/Out | Two-Finger + Pinch Out/In | Scales distance zooming in/out. +/// Left Button Press + Release | Any-Finger + Release | Slides to cursor/finger position. /// -/// Key | Action +/// Keyboard | Action /// --------------------------- | --------------------------------------------------------- /// O | Switches between orthographic and perspective projection. /// Enter | Resets camera eye and target to [`Self::reset`]. @@ -64,24 +67,30 @@ pub struct Trackball { pub scene: Scene, image: Image, + first: First, orbit: Orbit, scale: Scale, slide: Slide, touch: Touch, f32>, + mouse: Option>, } impl Trackball { /// Creates camera with eye position inclusive its roll attitude and target position. /// - /// Default frustum has field of view of π/4 with near and far clip planes at 1E-1 and 1E+6. - pub fn new(eye: &Point3, at: &Point3, up: &Vector3) -> Trackball { - let frame = Frame::look_at(eye, at, up); + /// Default viewing frustum has a fixed vertical field of view of π/4 with near and far clip + /// planes at 1E-1 and 1E+6. + /// + /// **Note:** Argument order differs from cameras in [`kiss3d::camera`]. + pub fn new(target: Point3, eye: &Point3, up: &Vector3) -> Trackball { + let frame = Frame::look_at(target, eye, up); let reset = frame.clone(); let scene = Scene::default(); - let image = Image::new(&frame, &scene, &Point2::new(800.0, 600.0)); + let image = Image::new(&frame, &scene, Point2::new(800.0, 600.0)); Self { input: Default::default(), clamp: Default::default(), + first: Default::default(), frame, reset, scene, @@ -90,18 +99,24 @@ impl Trackball { scale: Default::default(), slide: Default::default(), touch: Default::default(), + mouse: Default::default(), } } - /// Like [`Self::new()`] but with custom frustum. + /// Like [`Self::new()`] but with custom viewing frustum. + /// + /// For a fixed vertical field of view simply pass an [`f32`] angle in radians as `fov`, + /// otherwise see [`Fixed`] and [`Scene::set_fov()`]. + /// + /// **Note:** Argument order differs from cameras in [`kiss3d::camera`]. pub fn new_with_frustum( + target: Point3, + eye: &Point3, + up: &Vector3, fov: impl Into>, znear: f32, zfar: f32, - eye: &Point3, - at: &Point3, - up: &Vector3, ) -> Trackball { - let mut trackball = Self::new(eye, at, up); + let mut trackball = Self::new(target, eye, up); trackball.scene.set_fov(fov); trackball.scene.set_clip_planes(znear, zfar); trackball @@ -113,7 +128,7 @@ impl Camera for Trackball { self.scene.clip_planes(self.frame.distance()) } fn view_transform(&self) -> Isometry3 { - self.image.view_isometry().clone() + *self.image.view_isometry() } fn eye(&self) -> Point3 { self.frame.eye() @@ -124,22 +139,37 @@ impl Camera for Trackball { let pos = Point2::new(x as f32, y as f32); match action { TouchAction::Start | TouchAction::Move => { - if let Some((num, pos, rot, rat)) = self.touch.compute(Some(id), &pos, 0) { - if num == 1 { - if let Some(rot) = self.orbit.compute(&pos, self.image.max()) { - self.frame.local_orbit(&rot); + if action == TouchAction::Start { + self.slide.discard(); + } + if let Some((num, pos, rot, rat)) = self.touch.compute(Some(id), pos, 0) { + if self.first.enabled() { + if let Some(vec) = self.slide.compute(pos) { + if let Some((pitch, yaw, yaw_axis)) = + self.first.compute(&vec, self.image.max()) + { + self.frame.look_around(pitch, yaw, &yaw_axis); + } } } else { - if let Some(vec) = self.slide.compute(&pos) { - self.frame.local_slide(&self.image.project_vec(&vec)); + if num == 1 { + if let Some(rot) = self.orbit.compute(&pos, self.image.max()) { + self.frame.local_orbit(&rot); + } + } else { + if let Some(vec) = self.slide.compute(pos) { + self.frame.local_slide(&self.image.project_vec(&vec)); + } + if num == 2 { + let pos = self.image.project_pos(&pos); + let rot = UnitQuaternion::from_axis_angle( + &self.frame.local_roll_axis(), + rot, + ); + self.frame.local_orbit_around(&rot, &pos); + self.frame.local_scale_around(rat, &pos); + } } - let pos = self.image.project_pos(&pos); - let rot = UnitQuaternion::from_axis_angle( - &self.frame.local_roll_axis(), - rot, - ); - self.frame.local_orbit_at(&rot, &pos); - self.frame.local_scale_at(rat, &pos); } } } @@ -152,10 +182,10 @@ impl Camera for Trackball { } } } - WindowEvent::MouseButton(button, action, _modifiers) => { + WindowEvent::MouseButton(button, action, _modifiers) if !self.first.enabled() => { if Some(button) == self.input.orbit_button() { if action == Action::Press { - self.touch.compute(None, self.image.pos(), 0); + self.touch.compute(None, *self.image.pos(), 0); } else { self.orbit.discard(); if let Some((_num, pos)) = self.touch.discard(None) { @@ -165,53 +195,104 @@ impl Camera for Trackball { } if Some(button) == self.input.slide_button() { if action == Action::Press { - let pos = self.image.pos().clone(); - if let Some(vec) = self.slide.compute(&pos) { - self.frame.local_slide(&self.image.project_vec(&vec)); - } + self.slide.compute(*self.image.pos()); } else { self.slide.discard(); } } } WindowEvent::CursorPos(x, y, modifiers) => { - let pos = Point2::new(x as f32, y as f32); - self.image.set_pos(&pos); - if let Some(orbit_button) = self.input.orbit_button() { - if canvas.get_mouse_button(orbit_button) == Action::Press - && self - .input - .orbit_modifiers() - .map(|m| m == modifiers) - .unwrap_or(true) - { - if let Some((_num, _pos, _rot, _rat)) = self.touch.compute(None, &pos, 0) { - if let Some(rot) = self.orbit.compute(&pos, self.image.max()) { + let pos = Point2::new(x, y); + let is_eq = |old| old == pos || old == Point2::new(pos.x.floor(), pos.y.floor()); + if self.mouse.replace(pos).map_or(true, is_eq) { + return; + } + let (pos, max) = (pos.cast(), *self.image.max()); + if self.first.enabled() { + if self.touch.fingers() == 0 { + if let Some(vec) = self.slide.compute(pos) { + canvas.hide_cursor(true); + canvas.set_cursor_grab(true); + if let Some((pitch, yaw, yaw_axis)) = self.first.compute(&vec, &max) { + self.frame.look_around(pitch, yaw, &yaw_axis); + } + } + if pos.y <= 0.0 { + canvas.set_cursor_position(x, max.y as f64 - 2.0); + self.slide.discard(); + } + if pos.x <= 0.0 { + canvas.set_cursor_position(max.x as f64 - 2.0, y); + self.slide.discard(); + } + if pos.x >= max.x - 1.0 { + canvas.set_cursor_position(1.0, y); + self.slide.discard(); + } + if pos.y >= max.y - 1.0 { + canvas.set_cursor_position(x, 1.0); + self.slide.discard(); + } + } + } else { + self.image.set_pos(pos); + let orbit = self.input.orbit_button().map_or(false, |button| { + canvas.get_mouse_button(button) == Action::Press + && self + .input + .orbit_modifiers() + .map(|m| m == modifiers) + .unwrap_or(true) + }); + let slide = self.input.slide_button().map_or(false, |button| { + canvas.get_mouse_button(button) == Action::Press + && self + .input + .slide_modifiers() + .map(|m| m == modifiers) + .unwrap_or(true) + }); + if orbit && slide { + self.orbit.discard(); + self.slide.discard(); + } + if orbit { + if let Some(pos) = self.touch.compute(None, pos, 0).map(|val| val.1) { + if let Some(rot) = self.orbit.compute(&pos, &max) { self.frame.local_orbit(&rot); } } } - } - if let Some(slide_button) = self.input.slide_button() { - if canvas.get_mouse_button(slide_button) == Action::Press - && self - .input - .slide_modifiers() - .map(|m| m == modifiers) - .unwrap_or(true) - { - if let Some(vec) = self.slide.compute(&pos) { + if slide { + if let Some(vec) = self.slide.compute(pos) { self.frame.local_slide(&self.image.project_vec(&vec)); } } } } WindowEvent::Scroll(_, val, _) => { - self.frame.local_scale_at( + self.frame.local_scale_around( self.scale.compute(val as f32), &self.image.project_pos(self.image.pos()), ); } + WindowEvent::Key(key, action, _modifiers) if Some(key) == self.input.first_key() => { + let mid = self.image.max() * 0.5; + if action == Action::Press { + if !self.first.enabled() { + self.first.capture(self.frame.yaw_axis()); + self.image.set_pos(mid); + } + } else { + self.slide.discard(); + self.first.discard(); + if self.touch.fingers() == 0 { + canvas.set_cursor_position(mid.x as f64, mid.y as f64); + canvas.hide_cursor(false); + canvas.set_cursor_grab(false); + } + } + } WindowEvent::Key(key, Action::Press, _modifiers) if Some(key) == self.input.ortho_key() => { @@ -223,7 +304,7 @@ impl Camera for Trackball { self.frame = self.reset.clone(); } WindowEvent::FramebufferSize(w, h) => { - self.image.set_max(&Point2::new(w as f32, h as f32)); + self.image.set_max(Point2::new(w, h).cast()); } _ => {} } @@ -239,13 +320,13 @@ impl Camera for Trackball { view.upload(self.image.view()); } fn transformation(&self) -> Matrix4 { - self.image.transformation().clone() + *self.image.transformation() } fn inverse_transformation(&self) -> Matrix4 { - self.image.inverse_transformation().clone() + *self.image.inverse_transformation() } fn update(&mut self, _: &Canvas) { - self.frame = self.clamp.compute(&self.frame, &self.scene); - self.image.compute(&self.frame, &self.scene); + self.frame = self.clamp.compute(self.frame.clone(), &self.scene); + self.image.compute(self.frame.clone(), self.scene.clone()); } }