trackball/orbit.rs
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
use nalgebra::{Point2, RealField, Unit, UnitQuaternion, Vector3};
use simba::scalar::SubsetOf;
#[cfg(not(feature = "cc"))]
use crate::Image;
/// Orbit induced by displacement on screen.
///
/// 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: Copy + RealField> {
/// Caches normalization of previous cursor/finger position.
vec: Option<(Unit<Vector3<N>>, N)>,
}
#[cfg(not(feature = "cc"))]
use nalgebra::Matrix3;
#[cfg(not(feature = "cc"))]
impl<N: Copy + 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 space with origin in top left corner:
///
/// * x-axis from left to right,
/// * y-axis from top to bottom.
///
/// Camera space with origin at its target, the 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>> {
// Clamped cursor/finger position from left to right and top to bottom.
let pos = Image::clamp_pos_wrt_max(pos, max);
// Centered cursor/finger position and its maximum from left to right and bottom to top.
let (pos, max) = Image::transform_pos_and_max_wrt_max(&pos, max);
// Positive z-axis pointing from far to near.
let (pos, pza) = (pos.coords.push(N::zero()), 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;
}
/// Casts components to another type, e.g., between [`f32`] and [`f64`].
#[must_use]
pub fn cast<M: Copy + RealField>(self) -> Orbit<M>
where
N: SubsetOf<M>,
{
Orbit {
vec: self.vec.map(|(ray, len)| (ray.cast(), len.to_superset())),
}
}
}
#[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 space with origin in top left corner:
///
/// * x-axis from left to right,
/// * y-axis from top to bottom.
///
/// Camera space with origin at its target, the 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();
#[allow(unsafe_code)]
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));
#[allow(clippy::float_cmp)]
(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;
}
/// Casts components to another type, e.g., to [`f64`].
#[must_use]
pub fn cast<M: Copy + RealField>(self) -> Orbit<M>
where
f32: SubsetOf<M>,
{
Orbit {
vec: self.vec.map(|(ray, len)| (ray.cast(), len.to_superset())),
}
}
}
#[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 space with origin in top left corner:
///
/// * x-axis from left to right,
/// * y-axis from top to bottom.
///
/// Camera space with origin at its target, the 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();
#[allow(unsafe_code)]
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));
#[allow(clippy::float_cmp)]
(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;
}
/// Casts components to another type, e.g., to [`f32`].
#[must_use]
pub fn cast<M: Copy + RealField>(self) -> Orbit<M>
where
f64: SubsetOf<M>,
{
Orbit {
vec: self.vec.map(|(ray, len)| (ray.cast(), len.to_superset())),
}
}
}
#[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);
}