use core::fmt::Debug;
use heapless::LinearMap;
use nalgebra::{convert, Point2, RealField, Unit, Vector2};
/// Touch gestures inducing slide, orbit, scale, and focus.
///
/// Implements [`Default`] and can be created with `Touch::default()`.
///
/// All methods except [`Self::fingers()`] must be invoked on matching events fired by your 3D
/// graphics library of choice.
#[derive(Debug, Clone, Default)]
pub struct Touch<F: Debug + Eq, N: Copy + RealField> {
/// Finger positions in insertion order.
pos: LinearMap<F, Point2<N>, 10>,
/// Cached normalization of previous two-finger vector.
vec: Option<(Unit<Vector2<N>>, N)>,
/// Number of fingers and centroid position of potential finger tap gesture.
tap: Option<(usize, Point2<N>)>,
/// Number of total finger moves per potential finger tap gesture.
mvs: usize,
}
impl<F: Debug + Copy + Eq, N: Copy + RealField> Touch<F, N> {
/// Computes centroid position, roll angle, and scale ratio from finger gestures.
///
/// Parameters are:
///
/// * `fid` as generic finger ID like `Some(id)` for touch and `None` for mouse events,
/// * `pos` as current cursor/finger position in screen space,
/// * `mvs` as number of finger moves for debouncing potential finger tap gesture with zero
/// resulting in no delay of non-tap gestures while tap gesture can still be recognized. Use
/// zero unless tap gestures are hardly recognized.
///
/// Returns number of fingers, centroid position, roll angle, and scale ratio in screen space in
/// the order mentioned or `None` when debouncing tap gesture with non-vanishing `mvs`. See
/// [`Self::discard()`] for tap gesture result.
///
/// # Panics
///
/// Panics with more than ten fingers.
pub fn compute(
&mut self,
fid: F,
pos: Point2<N>,
mvs: usize,
) -> Option<(usize, Point2<N>, N, N)> {
// Insert or update finger position.
let _old_pos = self.pos.insert(fid, pos).expect("Too many fingers");
// Current number of fingers.
let num = self.pos.len();
// Maximum number of fingers seen per potential tap.
let max = self.tap.map_or(1, |(tap, _pos)| tap).max(num);
// Centroid position.
#[allow(clippy::cast_precision_loss)]
let pos = self
.pos
.values()
.map(|pos| pos.coords)
.sum::<Vector2<N>>()
.unscale(convert(num as f64))
.into();
// Cancel potential tap if more moves than number of finger starts plus optional number of
// moves per finger for debouncing tap gesture. Debouncing would delay non-tap gestures.
if self.mvs >= max + mvs * max {
// Make sure to not resume cancelled tap when fingers are discarded.
self.mvs = usize::MAX;
// Cancel potential tap.
self.tap = None;
} else {
// Count total moves per potential tap.
self.mvs += 1;
// Insert or update potential tap as long as fingers are not discarded.
if num >= max {
self.tap = Some((num, pos));
}
}
// Inhibit finger gestures for given number of moves per finger. No delay with zero `mvs`.
if self.mvs >= mvs * max {
// Identity roll angle and scale ratio.
let (rot, rat) = (N::zero(), N::one());
// Roll and scale only with two-finger gesture, otherwise orbit or slide via centroid.
if num == 2 {
// Position of first and second finger.
let mut val = self.pos.values();
let one_pos = val.next().unwrap();
let two_pos = val.next().unwrap();
// Ray and its length pointing from first to second finger.
let (new_ray, new_len) = Unit::new_and_get(two_pos - one_pos);
// Get old and replace with new vector.
if let Some((old_ray, old_len)) = self.vec.replace((new_ray, new_len)) {
// Roll angle in opposite direction at centroid.
let rot = old_ray.perp(&new_ray).atan2(old_ray.dot(&new_ray));
// Scale ratio at centroid.
let rat = old_len / new_len;
// Induced two-finger slide, roll, and scale.
Some((num, pos, rot, rat))
} else {
// Start position of slide.
Some((num, pos, rot, rat))
}
} else {
// Induced one-finger or more than two-finger orbit or slide.
Some((num, pos, rot, rat))
}
} else {
// Gesture inhibited.
None
}
}
/// Removes finger position and returns number of fingers and centroid position of tap gesture.
///
/// Returns `None` as long as there are finger positions or no tap gesture has been recognized.
///
/// # Panics
///
/// Panics if generic finger ID `fid` is unknown.
pub fn discard(&mut self, fid: F) -> Option<(usize, Point2<N>)> {
self.pos.remove(&fid).expect("Unknown touch ID");
self.vec = None;
if self.pos.is_empty() {
self.mvs = 0;
self.tap.take()
} else {
None
}
}
/// Number of fingers.
pub fn fingers(&self) -> usize {
self.pos.len()
}
}