trackball/
touch.rs

1use core::fmt::Debug;
2use heapless::LinearMap;
3use nalgebra::{Point2, RealField, Unit, Vector2, convert};
4use simba::scalar::SubsetOf;
5
6/// Touch gestures inducing slide, orbit, scale, and focus.
7///
8/// Implements [`Default`] and can be created with `Touch::default()`.
9///
10/// All methods except [`Self::fingers()`] must be invoked on matching events fired by your 3D
11/// graphics library of choice.
12#[derive(Debug, Clone, Default)]
13pub struct Touch<F: Debug + Eq, N: Copy + RealField> {
14	/// Finger positions in insertion order.
15	pos: LinearMap<F, Point2<N>, 10>,
16	/// Cached normalization of previous two-finger vector.
17	vec: Option<(Unit<Vector2<N>>, N)>,
18	/// Number of fingers and centroid position of potential finger tap gesture.
19	tap: Option<(usize, Point2<N>)>,
20	/// Number of total finger moves per potential finger tap gesture.
21	mvs: usize,
22}
23
24impl<F: Debug + Copy + Eq, N: Copy + RealField> Touch<F, N> {
25	/// Computes centroid position, roll angle, and scale ratio from finger gestures.
26	///
27	/// Parameters are:
28	///
29	///   * `fid` as generic finger ID like `Some(id)` for touch and `None` for mouse events,
30	///   * `pos` as current cursor/finger position in screen space,
31	///   * `mvs` as number of finger moves for debouncing potential finger tap gesture with zero
32	///     resulting in no delay of non-tap gestures while tap gesture can still be recognized. Use
33	///     zero unless tap gestures are hardly recognized.
34	///
35	/// Returns number of fingers, centroid position, roll angle, and scale ratio in screen space in
36	/// the order mentioned or `None` when debouncing tap gesture with non-vanishing `mvs`. See
37	/// [`Self::discard()`] for tap gesture result.
38	///
39	/// # Panics
40	///
41	/// Panics with more than ten fingers.
42	pub fn compute(
43		&mut self,
44		fid: F,
45		pos: Point2<N>,
46		mvs: usize,
47	) -> Option<(usize, Point2<N>, N, N)> {
48		// Insert or update finger position.
49		let old_pos = self.pos.insert(fid, pos).expect("Too many fingers");
50		// Ignore events of unchanged finger position.
51		if old_pos == Some(pos) {
52			return None;
53		}
54		// Current number of fingers.
55		let num = self.pos.len();
56		// Maximum number of fingers seen per potential tap.
57		let max = self.tap.map_or(1, |(tap, _pos)| tap).max(num);
58		// Centroid position.
59		#[allow(clippy::cast_precision_loss)]
60		let pos = self
61			.pos
62			.values()
63			.map(|pos| pos.coords)
64			.sum::<Vector2<N>>()
65			.unscale(convert(num as f64))
66			.into();
67		// Cancel potential tap if more moves than number of finger starts plus optional number of
68		// moves per finger for debouncing tap gesture. Debouncing would delay non-tap gestures.
69		if self.mvs >= max + mvs * max {
70			// Make sure to not resume cancelled tap when fingers are discarded.
71			self.mvs = usize::MAX;
72			// Cancel potential tap.
73			self.tap = None;
74		} else {
75			// Count total moves per potential tap.
76			self.mvs += 1;
77			// Insert or update potential tap as long as fingers are not discarded.
78			if num >= max {
79				self.tap = Some((num, pos));
80			}
81		}
82		// Inhibit finger gestures for given number of moves per finger. No delay with zero `mvs`.
83		if self.mvs >= mvs * max {
84			// Identity roll angle and scale ratio.
85			let (rot, rat) = (N::zero(), N::one());
86			// Roll and scale only with two-finger gesture, otherwise orbit or slide via centroid.
87			if num == 2 {
88				// Position of first and second finger.
89				let mut val = self.pos.values();
90				let one_pos = val.next().unwrap();
91				let two_pos = val.next().unwrap();
92				// Ray and its length pointing from first to second finger.
93				let (new_ray, new_len) = Unit::new_and_get(two_pos - one_pos);
94				// Get old and replace with new vector.
95				if let Some((old_ray, old_len)) = self.vec.replace((new_ray, new_len)) {
96					// Roll angle in opposite direction at centroid.
97					let rot = old_ray.perp(&new_ray).atan2(old_ray.dot(&new_ray));
98					// Scale ratio at centroid.
99					let rat = old_len / new_len;
100					// Induced two-finger slide, roll, and scale.
101					Some((num, pos, rot, rat))
102				} else {
103					// Start position of slide.
104					Some((num, pos, rot, rat))
105				}
106			} else {
107				// Induced one-finger or more than two-finger orbit or slide.
108				Some((num, pos, rot, rat))
109			}
110		} else {
111			// Gesture inhibited.
112			None
113		}
114	}
115	/// Removes finger position and returns number of fingers and centroid position of tap gesture.
116	///
117	/// Returns `None` as long as there are finger positions or no tap gesture has been recognized.
118	///
119	/// Discards finger positions and tap gesture if generic finger ID `fid` is unknown.
120	pub fn discard(&mut self, fid: F) -> Option<(usize, Point2<N>)> {
121		let unknown = self.pos.remove(&fid).is_none();
122		self.vec = None;
123		if self.pos.is_empty() || unknown {
124			self.pos.clear();
125			self.mvs = 0;
126			self.tap.take().filter(|_tap| !unknown)
127		} else {
128			None
129		}
130	}
131	/// Number of fingers.
132	#[must_use]
133	pub fn fingers(&self) -> usize {
134		self.pos.len()
135	}
136	/// Casts components to another type, e.g., between [`f32`] and [`f64`].
137	#[must_use]
138	pub fn cast<M: Copy + RealField>(self) -> Touch<F, M>
139	where
140		N: SubsetOf<M>,
141	{
142		Touch {
143			pos: self
144				.pos
145				.into_iter()
146				.map(|(&fid, pos)| (fid, pos.cast()))
147				.collect::<LinearMap<F, Point2<M>, 10>>(),
148			vec: self.vec.map(|(ray, len)| (ray.cast(), len.to_superset())),
149			tap: self.tap.map(|(mvs, pos)| (mvs, pos.cast())),
150			mvs: self.mvs,
151		}
152	}
153}