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}