kiss3d_trackball/
lib.rs

1//! Coherent Virtual Trackball Camera Mode for Kiss 3D
2//!
3//! Complements common [`trackball`] operation handlers with [`kiss3d`]-specific [`Input`] resulting
4//! in a compound [`Trackball`] [`Camera`] mode implementation for the [`kiss3d`] graphics library.
5//!
6//! # Coherence
7//!
8//! This is an alternative trackball technique using exponential map and parallel transport to
9//! preserve distances and angles for inducing coherent and intuitive trackball rotations. For
10//! instance, displacements on straight radial lines through the screen's center are carried to arcs
11//! of the same length on great circles of the trackball (e.g., dragging the mouse along an eights
12//! of the trackball's circumference rolls the camera by 360/8=45 degrees, dragging the mouse from
13//! the screen's center to its further edge *linearly* rotates the camera by 1 [radian], where the
14//! trackball's diameter is the maximum of the screen's width and height). This is in contrast to
15//! state-of-the-art techniques using orthogonal projection which distorts radial distances further
16//! away from the screen's center (e.g., the rotation accelerates towards the edge).[^1]
17//!
18//! [^1]: G. Stantchev, “Virtual Trackball Modeling and the Exponential Map”, [S2CID 44199608 (2004)
19//! ](https://api.semanticscholar.org/CorpusID:44199608), [Archived PDF
20//! ](https://web.archive.org/web/2/http://www.math.umd.edu:80/~gogo/Papers/trackballExp.pdf)
21//!
22//! [radian]: https://en.wikipedia.org/wiki/Radian
23
24#![allow(clippy::collapsible_else_if)]
25#![no_std]
26
27use kiss3d::{
28	camera::Camera,
29	event::{Action, Key, Modifiers, MouseButton, TouchAction, WindowEvent},
30	nalgebra::{Isometry3, Matrix4, Point2, Point3, UnitQuaternion, Vector3},
31	resource::ShaderUniform,
32	window::Canvas,
33};
34use trackball::{First, Fixed, Frame, Image, Orbit, Scale, Scope, Slide, Touch};
35
36pub use kiss3d;
37pub use trackball;
38
39mod input;
40pub use input::*;
41
42/// Trackball camera mode.
43///
44/// A trackball camera is a camera working similarly like a trackball device. The camera eye orbits
45/// around its target, the trackball's center, and always looks at it. This implementation is split
46/// into several members defined by the [`trackball`] crate categorizing all the methods to control
47/// the different aspects of a camera.
48///
49/// # Camera Input
50///
51/// Following default inputs are defined which are customizable via [`Self::input`]:
52///
53/// Mouse                       | Touch                          | Action
54/// --------------------------- | ------------------------------ | ---------------------------------
55/// Left Button Press + Drag    | One-Finger + Drag              | Orbits around target.
56/// ↳ but at trackball's border | Two-Finger + Roll              | Rolls about view direction.
57/// Drag + Left Shift           | Any-Finger + Drag + Left Shift | First person view.
58/// Right Button Press + Drag   | Two-Finger + Drag              | Slides trackball on focus plane.
59/// Scroll In/Out               | Two-Finger + Pinch Out/In      | Scales distance zooming in/out.
60/// Left Button Press + Release | Any-Finger + Release           | Slides to cursor/finger position.
61///
62/// Keyboard                    | Action
63/// --------------------------- | ---------------------------------------------------------
64/// O                           | Switches between orthographic and perspective projection.
65/// Enter                       | Resets camera eye and target to [`Self::reset`].
66///
67/// # Camera Alignment
68///
69/// Realign camera via [`Self::frame`]. Optionally, update the alignment to reset to when pressing
70/// [`Input::reset_key()`] via [`Self::reset`].
71///
72/// # Camera Projection
73///
74/// Adjust camera projection via [`Self::scope`] like setting field of view or clip plane distances.
75#[derive(Clone)]
76pub struct Trackball {
77	/// Input keys/buttons and their modifiers.
78	pub input: Input<f32>,
79	/// Frame wrt camera eye and target.
80	pub frame: Frame<f32>,
81	/// Reset frame wrt camera eye and target.
82	pub reset: Frame<f32>,
83	/// Scope wrt enclosing viewing frustum.
84	pub scope: Scope<f32>,
85
86	image: Image<f32>,
87	first: First<f32>,
88	orbit: Orbit<f32>,
89	scale: Scale<f32>,
90	slide: Slide<f32>,
91	touch: Touch<Option<u64>, f32>,
92	mouse: Option<Point2<f64>>,
93}
94
95impl Trackball {
96	/// Creates camera with eye position inclusive its roll attitude and target position.
97	///
98	/// Default viewing frustum has a fixed vertical field of view of π/4 with near and far clip
99	/// planes at 1E-1 and 1E+3.
100	///
101	/// **Note:** Argument order differs from cameras in [`kiss3d::camera`].
102	#[must_use]
103	pub fn new(target: Point3<f32>, eye: &Point3<f32>, up: &Vector3<f32>) -> Trackball {
104		let frame = Frame::look_at(target, eye, up);
105		let reset = frame;
106		let scope = Scope::default();
107		let mut image = Image::new(&Frame::default(), &scope, Point2::new(800.0, 600.0));
108		image.set_passive(true);
109		image.compute(frame, scope);
110		Self {
111			input: Input::default(),
112			first: First::default(),
113			frame,
114			reset,
115			scope,
116			image,
117			orbit: Orbit::default(),
118			scale: Scale::default(),
119			slide: Slide::default(),
120			touch: Touch::default(),
121			mouse: Option::default(),
122		}
123	}
124	/// Like [`Self::new()`] but with custom viewing frustum.
125	///
126	/// For a fixed vertical field of view simply pass an [`f32`] angle in radians as `fov`,
127	/// otherwise see [`Fixed`] and [`Scope::set_fov()`].
128	///
129	/// **Note:** Argument order differs from cameras in [`kiss3d::camera`].
130	pub fn new_with_frustum(
131		target: Point3<f32>,
132		eye: &Point3<f32>,
133		up: &Vector3<f32>,
134		fov: impl Into<Fixed<f32>>,
135		znear: f32,
136		zfar: f32,
137	) -> Trackball {
138		let mut trackball = Self::new(target, eye, up);
139		trackball.scope.set_fov(fov);
140		trackball.scope.set_clip_planes(znear, zfar);
141		trackball
142	}
143	fn handle_touch(
144		&mut self,
145		_canvas: &Canvas,
146		id: u64,
147		x: f64,
148		y: f64,
149		action: TouchAction,
150		_modifiers: Modifiers,
151	) {
152		#[allow(clippy::cast_possible_truncation)]
153		let pos = Point2::new(x as f32, y as f32);
154		match action {
155			TouchAction::Start | TouchAction::Move => {
156				if action == TouchAction::Start {
157					self.slide.discard();
158				}
159				if let Some((num, pos, rot, rat)) = self.touch.compute(Some(id), pos, 0) {
160					if self.first.enabled() {
161						if let Some(vec) = self.slide.compute(pos) {
162							if let Some((pitch, yaw, yaw_axis)) =
163								self.first.compute(&vec, self.image.max())
164							{
165								self.frame.look_around(pitch, yaw, yaw_axis);
166							}
167						}
168					} else {
169						if num == 1 {
170							if let Some(rot) = self.orbit.compute(&pos, self.image.max()) {
171								self.frame.local_orbit(&rot);
172							}
173						} else {
174							if let Some(vec) = self.slide.compute(pos) {
175								self.frame.local_slide(&self.image.project_vec(&vec));
176							}
177							if num == 2 {
178								let pos = self.image.project_pos(&pos);
179								let rot = UnitQuaternion::from_axis_angle(
180									&self.frame.local_roll_axis(),
181									rot,
182								);
183								self.frame.local_orbit_around(&rot, &pos);
184								self.frame.local_scale_around(rat, &pos);
185							}
186						}
187					}
188				}
189			}
190			TouchAction::End | TouchAction::Cancel => {
191				if let Some((_num, pos)) = self.touch.discard(Some(id)) {
192					self.frame.local_slide(&self.image.project_pos(&pos).coords);
193				}
194				self.orbit.discard();
195				self.slide.discard();
196			}
197		}
198	}
199	fn handle_mouse_button(
200		&mut self,
201		_canvas: &Canvas,
202		button: MouseButton,
203		action: Action,
204		_modifiers: Modifiers,
205	) {
206		if !self.first.enabled() {
207			if Some(button) == self.input.orbit_button() {
208				if action == Action::Press {
209					self.touch.compute(None, *self.image.pos(), 0);
210				} else {
211					self.orbit.discard();
212					if let Some((_num, pos)) = self.touch.discard(None) {
213						self.frame.local_slide(&self.image.project_pos(&pos).coords);
214					}
215				}
216			}
217			if Some(button) == self.input.slide_button() {
218				if action == Action::Press {
219					self.slide.compute(*self.image.pos());
220				} else {
221					self.slide.discard();
222				}
223			}
224		}
225	}
226	fn handle_cursor_pos(&mut self, canvas: &Canvas, x: f64, y: f64, modifiers: Modifiers) {
227		let pos = Point2::new(x, y);
228		let is_eq = |old| old == pos || old == Point2::new(pos.x.floor(), pos.y.floor());
229		if self.mouse.replace(pos).is_none_or(is_eq) {
230			return;
231		}
232		let (pos, max) = (pos.cast(), *self.image.max());
233		if self.first.enabled() {
234			if self.touch.fingers() == 0 {
235				if let Some(vec) = self.slide.compute(pos) {
236					canvas.hide_cursor(true);
237					canvas.set_cursor_grab(true);
238					if let Some((pitch, yaw, yaw_axis)) = self.first.compute(&vec, &max) {
239						self.frame.look_around(pitch, yaw, yaw_axis);
240					}
241				}
242				if pos.y <= 0.0 {
243					canvas.set_cursor_position(x, f64::from(max.y) - 2.0);
244					self.slide.discard();
245				}
246				if pos.x <= 0.0 {
247					canvas.set_cursor_position(f64::from(max.x) - 2.0, y);
248					self.slide.discard();
249				}
250				if pos.x >= max.x - 1.0 {
251					canvas.set_cursor_position(1.0, y);
252					self.slide.discard();
253				}
254				if pos.y >= max.y - 1.0 {
255					canvas.set_cursor_position(x, 1.0);
256					self.slide.discard();
257				}
258			}
259		} else {
260			self.image.set_pos(pos);
261			let orbit = self.input.orbit_button().is_some_and(|button| {
262				canvas.get_mouse_button(button) == Action::Press
263					&& self.input.orbit_modifiers().is_none_or(|m| m == modifiers)
264			});
265			let slide = self.input.slide_button().is_some_and(|button| {
266				canvas.get_mouse_button(button) == Action::Press
267					&& self.input.slide_modifiers().is_none_or(|m| m == modifiers)
268			});
269			if orbit && slide {
270				self.orbit.discard();
271				self.slide.discard();
272			}
273			if orbit {
274				if let Some(pos) = self.touch.compute(None, pos, 0).map(|val| val.1) {
275					if let Some(rot) = self.orbit.compute(&pos, &max) {
276						self.frame.local_orbit(&rot);
277					}
278				}
279			}
280			if slide {
281				if let Some(vec) = self.slide.compute(pos) {
282					self.frame.local_slide(&self.image.project_vec(&vec));
283				}
284			}
285		}
286	}
287	fn handle_scroll(&mut self, _canvas: &Canvas, _dx: f64, dy: f64, _modifiers: Modifiers) {
288		self.frame.local_scale_around(
289			#[allow(clippy::cast_possible_truncation)]
290			self.scale.compute(dy as f32),
291			&self.image.project_pos(self.image.pos()),
292		);
293	}
294	fn handle_key(&mut self, canvas: &Canvas, key: Key, action: Action, _modifiers: Modifiers) {
295		if Some(key) == self.input.first_key() {
296			let mid = self.image.max() * 0.5;
297			if action == Action::Press {
298				if !self.first.enabled() {
299					self.first.capture(self.frame.yaw_axis());
300					self.image.set_pos(mid);
301				}
302			} else {
303				self.slide.discard();
304				self.first.discard();
305				if self.touch.fingers() == 0 {
306					canvas.set_cursor_position(mid.x.into(), mid.y.into());
307					canvas.hide_cursor(false);
308					canvas.set_cursor_grab(false);
309				}
310			}
311		} else if action == Action::Press {
312			if Some(key) == self.input.ortho_key() {
313				self.scope.set_ortho(!self.scope.ortho());
314			} else if Some(key) == self.input.reset_key() {
315				self.frame = self.reset;
316			}
317		}
318	}
319	fn handle_framebuffer_size(&mut self, _canvas: &Canvas, w: u32, h: u32) {
320		self.image.set_max(Point2::new(w, h).cast());
321	}
322}
323
324impl Camera for Trackball {
325	fn clip_planes(&self) -> (f32, f32) {
326		self.scope.clip_planes(self.frame.distance())
327	}
328	fn view_transform(&self) -> Isometry3<f32> {
329		*self.image.view_isometry()
330	}
331	fn eye(&self) -> Point3<f32> {
332		self.frame.eye()
333	}
334	fn handle_event(&mut self, canvas: &Canvas, event: &WindowEvent) {
335		match *event {
336			WindowEvent::Touch(id, x, y, action, modifiers) => {
337				self.handle_touch(canvas, id, x, y, action, modifiers);
338			}
339			WindowEvent::MouseButton(button, action, modifiers) => {
340				self.handle_mouse_button(canvas, button, action, modifiers);
341			}
342			WindowEvent::CursorPos(x, y, modifiers) => {
343				self.handle_cursor_pos(canvas, x, y, modifiers);
344			}
345			WindowEvent::Scroll(dx, dy, modifiers) => {
346				self.handle_scroll(canvas, dx, dy, modifiers);
347			}
348			WindowEvent::Key(key, action, modifiers) => {
349				self.handle_key(canvas, key, action, modifiers);
350			}
351			WindowEvent::FramebufferSize(w, h) => {
352				self.handle_framebuffer_size(canvas, w, h);
353			}
354			_ => {}
355		}
356	}
357	#[inline]
358	fn upload(
359		&self,
360		_: usize,
361		proj: &mut ShaderUniform<Matrix4<f32>>,
362		view: &mut ShaderUniform<Matrix4<f32>>,
363	) {
364		proj.upload(self.image.projection());
365		view.upload(self.image.view());
366	}
367	fn transformation(&self) -> Matrix4<f32> {
368		*self.image.transformation()
369	}
370	fn inverse_transformation(&self) -> Matrix4<f32> {
371		*self.image.inverse_transformation()
372	}
373	fn update(&mut self, _: &Canvas) {
374		self.image.compute(self.frame, self.scope);
375	}
376}