trackball/
touch.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
use core::fmt::Debug;
use heapless::LinearMap;
use nalgebra::{convert, Point2, RealField, Unit, Vector2};
use simba::scalar::SubsetOf;

/// 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");
		// Ignore events of unchanged finger position.
		if old_pos == Some(pos) {
			return None;
		}
		// 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.
	///
	/// Discards finger positions and tap gesture if generic finger ID `fid` is unknown.
	pub fn discard(&mut self, fid: F) -> Option<(usize, Point2<N>)> {
		let unknown = self.pos.remove(&fid).is_none();
		self.vec = None;
		if self.pos.is_empty() || unknown {
			self.pos.clear();
			self.mvs = 0;
			self.tap.take().filter(|_tap| !unknown)
		} else {
			None
		}
	}
	/// Number of fingers.
	#[must_use]
	pub fn fingers(&self) -> usize {
		self.pos.len()
	}
	/// Casts components to another type, e.g., between [`f32`] and [`f64`].
	#[must_use]
	pub fn cast<M: Copy + RealField>(self) -> Touch<F, M>
	where
		N: SubsetOf<M>,
	{
		Touch {
			pos: self
				.pos
				.into_iter()
				.map(|(&fid, pos)| (fid, pos.cast()))
				.collect::<LinearMap<F, Point2<M>, 10>>(),
			vec: self.vec.map(|(ray, len)| (ray.cast(), len.to_superset())),
			tap: self.tap.map(|(mvs, pos)| (mvs, pos.cast())),
			mvs: self.mvs,
		}
	}
}