1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! Virtual Trackball Orbiting via the Exponential Map
//!
//! This is an alternative trackball technique using exponential map and parallel transport to
//! preserve distances and angles for inducing coherent and intuitive trackball rotations. For
//! instance, displacements on straight radial lines through the screen's center are carried to arcs
//! of the same length on great circles of the trackball. This is in contrast to state-of-the-art
//! techniques using orthogonal projection which distorts radial distances further away from the
//! screen's center. This implementation strictly follows the recipe given in the paper of
//! Stantchev, G.. “Virtual Trackball Modeling and the Exponential Map.” . [S2CID] [44199608].
//!
//! [S2CID]: https://en.wikipedia.org/wiki/S2CID_(identifier)
//! [44199608]: https://api.semanticscholar.org/CorpusID:44199608
//!
//! # Status
//!
//! Currently only [`nalgebra`] is supported as underlying linear algebra library but others will be
//! supported behind feature gates so that only your library of choice becomes a dependency. The
//! [`Orbit`] operation handler will be complemented with other handlers for common trackball camera
//! mode operations like slide, scale, and focus. Projection view matrices will be computed as well
//! with scale preserving transitions between orthographic and perspective projection mode.
//!
//! # Example
//!
//! A trackball camera mode implementation can be as easily as this by delegating events of your 3D
//! graphics library of choice to the [`Orbit`] operation handler along with other handlers for
//! common trackball camera mode operations like slide, scale, and focus.
//!
//! ```
//! use nalgebra::{Point2, UnitQuaternion, Vector3};
//! use std::f32::consts::PI;
//! use trackball::Orbit;
//!
//! /// Trackball camera mode.
//! pub struct Trackball {
//! 	// Camera eye alignment.
//! 	align: UnitQuaternion<f32>,
//! 	// Orbit operation handler along with slide, scale, and focus operation handlers.
//! 	orbit: Orbit<f32>,
//! 	// Maximum cursor/finger position as screen's width and height.
//! 	frame: Point2<f32>,
//! }
//!
//! impl Trackball {
//! 	// Usually, a cursor position event with left mouse button being pressed.
//! 	fn handle_left_button_displacement(&mut self, pos: &Point2<f32>) {
//! 		// Optionally, do a coordinate system transformation like flipping x-axis/z-axis.
//! 		let camera_space = UnitQuaternion::from_axis_angle(&Vector3::y_axis(), PI);
//! 		// Or directly apply this induced rotation.
//! 		let rotation = self.orbit.compute(&pos, &self.frame).unwrap_or_default();
//! 		// Post-multiply rotation to total camera alignment.
//! 		self.align *= camera_space * rotation * camera_space.inverse();
//! 	}
//! 	// Event when left mouse button is released again.
//! 	fn handle_left_button_release(&mut self) {
//! 		// Can also or instead be invoked on `Self::handle_left_button_press()`.
//! 		self.orbit.discard();
//! 	}
//! }
//! ```

use nalgebra::{Point2, RealField, Unit, UnitQuaternion, Vector3};

/// Orbit operation handler.
///
/// Implements [`Default`] and can be created with `Orbit::default()`.
///
/// Both its methods must be invoked on matching events fired by your 3D graphics library of choice.
#[derive(Debug, Clone, Default)]
pub struct Orbit<N: RealField> {
	/// Caches normalization of previous cursor/finger position.
	pub vec: Option<(Unit<Vector3<N>>, N)>,
}

#[cfg(not(feature = "cc"))]
use nalgebra::Matrix3;

#[cfg(not(feature = "cc"))]
impl<N: RealField> Orbit<N> {
	/// Computes rotation between previous and current cursor/finger position.
	///
	/// Normalization of previous position is cached and has to be discarded on button/finger
	/// release via [`Self::discard()`]. Current position `pos` is clamped between origin and
	/// maximum position `max` as screen's width and height.
	///
	/// Screen coordinate system with origin in top left corner:
	///
	///   * x-axis from left to right,
	///   * y-axis from top to bottom.
	///
	/// Trackball coordinate system with origin in trackball's center:
	///
	///   * x-axis from left to right,
	///   * y-axis from bottom to top,
	///   * z-axis from far to near.
	///
	/// Returns `None`:
	///
	///   * on first invocation and after [`Self::discard()`] as there is no previous position yet,
	///   * in the unlikely case that a position event fires twice resulting in zero displacements.
	pub fn compute(&mut self, pos: &Point2<N>, max: &Point2<N>) -> Option<UnitQuaternion<N>> {
		// Clamp position between screen's left top and right bottom corner.
		let pos = Point2::new(pos.x.clamp(N::zero(), max.x), pos.y.clamp(N::zero(), max.y));
		// Maximum centered cursor/finger position as half the screen's width and height.
		let max = max / (N::one() + N::one());
		// Current centered cursor/finger position from left to right and bottom to top.
		let pos = Vector3::new(pos.x - max.x, max.y - pos.y, N::zero());
		// Positive z-axis pointing from far to near.
		let pza = Vector3::z_axis();
		// New position as ray and length on xy-plane or z-axis of zero length for origin position.
		let (ray, len) = Unit::try_new_and_get(pos, N::zero()).unwrap_or((pza, N::zero()));
		// Get old ray and length as start position and offset and replace with new ray and length.
		let (pos, off) = self.vec.replace((ray, len))?;
		// Displacement vector from old to new ray and length.
		let vec = ray.into_inner() * len - pos.into_inner() * off;
		// Shadow new ray and length as normalized displacement vector.
		let (ray, len) = Unit::try_new_and_get(vec, N::zero())?;
		// Treat maximum of half the screen's width or height as trackball's radius.
		let max = max.x.max(max.y);
		// Map trackball's diameter onto half its circumference for start positions so that only
		// screen corners are mapped to lower hemisphere which induces less intuitive rotations.
		let (sin, cos) = (off / max * N::frac_pi_2()).sin_cos();
		// Exponential map of start position.
		let exp = Vector3::new(sin * pos.x, sin * pos.y, cos);
		// Tangent ray of geodesic at exponential map.
		let tan = Vector3::new(cos * pos.x, cos * pos.y, -sin);
		// Cross product of z-axis and start position to construct orthonormal frames.
		let zxp = Vector3::new(-pos.y, pos.x, N::zero());
		// Orthonormal frame as argument of differential of exponential map.
		let arg = Matrix3::from_columns(&[pza.into_inner(), pos.into_inner(), zxp]);
		// Orthonormal frame as image of differential of exponential map.
		let img = Matrix3::from_columns(&[exp, tan, zxp]);
		// Compute differential of exponential map by its argument and image and apply it to
		// displacement vector which in turn spans rotation plane together with exponential map.
		let vec = (img * arg.tr_mul(&ray.into_inner())).cross(&exp);
		// Angle of rotation is displacement length divided by radius.
		Unit::try_new(vec, N::zero()).map(|ray| UnitQuaternion::from_axis_angle(&ray, len / max))
	}

	/// Discards cached normalization of previous cursor/finger position on button/finger release.
	pub fn discard(&mut self) {
		self.vec = None;
	}
}

#[cfg(feature = "cc")]
use nalgebra::Quaternion;

#[cfg(feature = "cc")]
impl Orbit<f32> {
	/// Computes rotation between previous and current cursor/finger position.
	///
	/// Normalization of previous position is cached and has to be discarded on button/finger
	/// release via [`Self::discard()`]. Current position `pos` is clamped between origin and
	/// maximum position `max` as screen's width and height.
	///
	/// Screen coordinate system with origin in top left corner:
	///
	///   * x-axis from left to right,
	///   * y-axis from top to bottom.
	///
	/// Trackball coordinate system with origin in trackball's center:
	///
	///   * x-axis from left to right,
	///   * y-axis from bottom to top,
	///   * z-axis from far to near.
	///
	/// Returns `None`:
	///
	///   * on first invocation and after [`Self::discard()`] as there is no previous position yet,
	///   * in the unlikely case that a position event fires twice resulting in zero displacements.
	pub fn compute(&mut self, pos: &Point2<f32>, max: &Point2<f32>) -> Option<UnitQuaternion<f32>> {
		let mut rot = Quaternion::identity();
		let mut old = self
			.vec
			.map(|(ray, len)| ray.into_inner().push(len))
			.unwrap_or_default();
		unsafe {
			trackball_orbit_f(
				rot.as_vector_mut().as_mut_ptr(),
				old.as_mut_ptr(),
				pos.coords.as_ptr(),
				max.coords.as_ptr(),
			);
		}
		self.vec = Some((Unit::new_unchecked(old.xyz()), old.w));
		(rot.w != 1.0).then(|| UnitQuaternion::new_unchecked(rot))
	}

	/// Discards cached normalization of previous cursor/finger position on button/finger release.
	pub fn discard(&mut self) {
		self.vec = None;
	}
}

#[cfg(feature = "cc")]
impl Orbit<f64> {
	/// Computes rotation between previous and current cursor/finger position.
	///
	/// Normalization of previous position is cached and has to be discarded on button/finger
	/// release via [`Self::discard()`]. Current position `pos` is clamped between origin and
	/// maximum position `max` as screen's width and height.
	///
	/// Screen coordinate system with origin in top left corner:
	///
	///   * x-axis from left to right,
	///   * y-axis from top to bottom.
	///
	/// Trackball coordinate system with origin in trackball's center:
	///
	///   * x-axis from left to right,
	///   * y-axis from bottom to top,
	///   * z-axis from far to near.
	///
	/// Returns `None`:
	///
	///   * on first invocation and after [`Self::discard()`] as there is no previous position yet,
	///   * in the unlikely case that a position event fires twice resulting in zero displacements.
	pub fn compute(&mut self, pos: &Point2<f64>, max: &Point2<f64>) -> Option<UnitQuaternion<f64>> {
		let mut rot = Quaternion::identity();
		let mut old = self
			.vec
			.map(|(ray, len)| ray.into_inner().push(len))
			.unwrap_or_default();
		unsafe {
			trackball_orbit_d(
				rot.as_vector_mut().as_mut_ptr(),
				old.as_mut_ptr(),
				pos.coords.as_ptr(),
				max.coords.as_ptr(),
			);
		}
		self.vec = Some((Unit::new_unchecked(old.xyz()), old.w));
		(rot.w != 1.0).then(|| UnitQuaternion::new_unchecked(rot))
	}

	/// Discards cached normalization of previous cursor/finger position on button/finger release.
	pub fn discard(&mut self) {
		self.vec = None;
	}
}

#[cfg(feature = "cc")]
extern "C" {
	fn trackball_orbit_f(xyzw: *mut f32, xyzm: *mut f32, xy: *const f32, wh: *const f32);
	fn trackball_orbit_d(xyzw: *mut f64, xyzm: *mut f64, xy: *const f64, wh: *const f64);
}