1use crate::input::{InputEvent, TouchContact, TouchPhase};
54use std::collections::HashMap;
55
56const ZOOM_THRESHOLD: f64 = 0.1;
63
64const ROTATION_THRESHOLD_PX: f64 = 25.0;
68
69const PITCH_DEGREES_PER_PX: f64 = -0.5;
73
74pub struct GestureRecognizer {
84 fingers: HashMap<u64, FingerState>,
86
87 two_finger_ids: Option<(u64, u64)>,
90 start_distance: f64,
92 last_distance: f64,
94 start_vector: Option<[f64; 2]>,
96 last_vector: Option<[f64; 2]>,
98 min_diameter: f64,
101 zoom_active: bool,
103 rotate_active: bool,
105 pitch_valid: Option<bool>,
107 pitch_last_points: Option<([f64; 2], [f64; 2])>,
109}
110
111#[derive(Debug, Clone, Copy)]
112struct FingerState {
113 x: f64,
114 y: f64,
115}
116
117impl Default for GestureRecognizer {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl GestureRecognizer {
124 pub fn new() -> Self {
126 Self {
127 fingers: HashMap::new(),
128 two_finger_ids: None,
129 start_distance: 0.0,
130 last_distance: 0.0,
131 start_vector: None,
132 last_vector: None,
133 min_diameter: 0.0,
134 zoom_active: false,
135 rotate_active: false,
136 pitch_valid: None,
137 pitch_last_points: None,
138 }
139 }
140
141 pub fn reset(&mut self) {
143 self.fingers.clear();
144 self.reset_two_finger();
145 }
146
147 #[inline]
149 pub fn finger_count(&self) -> usize {
150 self.fingers.len()
151 }
152
153 pub fn process(&mut self, contact: TouchContact) -> Vec<InputEvent> {
159 match contact.phase {
160 TouchPhase::Started => self.on_start(contact),
161 TouchPhase::Moved => self.on_move(contact),
162 TouchPhase::Ended | TouchPhase::Cancelled => self.on_end(contact),
163 }
164 }
165
166 fn on_start(&mut self, c: TouchContact) -> Vec<InputEvent> {
169 self.fingers.insert(c.id, FingerState { x: c.x, y: c.y });
170
171 if self.two_finger_ids.is_none() && self.fingers.len() >= 2 {
173 let mut ids: Vec<u64> = self.fingers.keys().copied().collect();
174 ids.sort_unstable();
175 let (id_a, id_b) = (ids[0], ids[1]);
176 let a = self.fingers[&id_a];
177 let b = self.fingers[&id_b];
178 let dist = distance(a.x, a.y, b.x, b.y);
179 let vec = [b.x - a.x, b.y - a.y];
180
181 self.two_finger_ids = Some((id_a, id_b));
182 self.start_distance = dist;
183 self.last_distance = dist;
184 self.start_vector = Some(vec);
185 self.last_vector = Some(vec);
186 self.min_diameter = dist;
187 self.zoom_active = false;
188 self.rotate_active = false;
189 self.pitch_valid = None;
190 self.pitch_last_points = Some(([a.x, a.y], [b.x, b.y]));
191 }
192
193 Vec::new()
194 }
195
196 fn on_move(&mut self, c: TouchContact) -> Vec<InputEvent> {
197 let prev = match self.fingers.get(&c.id) {
199 Some(f) => *f,
200 None => return Vec::new(), };
202 let dx = c.x - prev.x;
203 let dy = c.y - prev.y;
204
205 self.fingers.insert(c.id, FingerState { x: c.x, y: c.y });
207
208 let mut events = Vec::new();
209
210 if let Some((id_a, id_b)) = self.two_finger_ids {
212 if let (Some(a), Some(b)) = (self.fingers.get(&id_a), self.fingers.get(&id_b)) {
213 let a = *a;
214 let b = *b;
215
216 events.extend(self.check_zoom(a, b));
218
219 events.extend(self.check_rotation(a, b));
221
222 events.extend(self.check_pitch(a, b, c.id));
224 }
225 }
226
227 if dx.abs() > f64::EPSILON || dy.abs() > f64::EPSILON {
232 let (anchor_x, anchor_y) = self.pan_anchor();
233 events.push(InputEvent::Pan {
234 dx,
235 dy,
236 x: Some(anchor_x),
237 y: Some(anchor_y),
238 });
239 }
240
241 events
242 }
243
244 fn on_end(&mut self, c: TouchContact) -> Vec<InputEvent> {
245 self.fingers.remove(&c.id);
246
247 if let Some((id_a, id_b)) = self.two_finger_ids {
250 if c.id == id_a || c.id == id_b {
251 self.reset_two_finger();
252 }
253 }
254
255 Vec::new()
256 }
257
258 fn check_zoom(&mut self, a: FingerState, b: FingerState) -> Option<InputEvent> {
261 let dist = distance(a.x, a.y, b.x, b.y);
262 if dist < 1.0 {
263 return None; }
265
266 if !self.zoom_active {
267 let delta = (dist / self.start_distance).ln() / std::f64::consts::LN_2;
268 if delta.abs() < ZOOM_THRESHOLD {
269 return None;
270 }
271 self.zoom_active = true;
272 }
273
274 let zoom_delta = (dist / self.last_distance).ln() / std::f64::consts::LN_2;
275 self.last_distance = dist;
276
277 let factor = 2.0_f64.powf(zoom_delta);
279
280 let mx = (a.x + b.x) * 0.5;
282 let my = (a.y + b.y) * 0.5;
283
284 Some(InputEvent::Zoom {
285 factor,
286 x: Some(mx),
287 y: Some(my),
288 })
289 }
290
291 fn check_rotation(&mut self, a: FingerState, b: FingerState) -> Option<InputEvent> {
292 let vec = [b.x - a.x, b.y - a.y];
293 let mag = (vec[0] * vec[0] + vec[1] * vec[1]).sqrt();
294 if mag < 1.0 {
295 return None;
296 }
297
298 self.min_diameter = self.min_diameter.min(mag);
299
300 if !self.rotate_active {
301 if let Some(start) = self.start_vector {
302 let bearing = angle_between(vec, start);
303 let circumference = std::f64::consts::PI * self.min_diameter;
304 let threshold_deg = if circumference > 0.0 {
305 ROTATION_THRESHOLD_PX / circumference * 360.0
306 } else {
307 360.0
308 };
309 if bearing.abs() < threshold_deg {
310 self.last_vector = Some(vec);
311 return None;
312 }
313 }
314 self.rotate_active = true;
315 }
316
317 let bearing_delta = if let Some(last) = self.last_vector {
318 angle_between(vec, last)
319 } else {
320 0.0
321 };
322 self.last_vector = Some(vec);
323
324 if bearing_delta.abs() < f64::EPSILON {
325 return None;
326 }
327
328 let delta_yaw = bearing_delta.to_radians();
330 Some(InputEvent::Rotate {
331 delta_yaw,
332 delta_pitch: 0.0,
333 })
334 }
335
336 fn check_pitch(&mut self, a: FingerState, b: FingerState, moved_id: u64) -> Option<InputEvent> {
337 let (id_a, id_b) = self.two_finger_ids?;
338
339 if moved_id != id_a && moved_id != id_b {
341 return None;
342 }
343
344 let last_points = self.pitch_last_points?;
345 let vec_a = [a.x - last_points.0[0], a.y - last_points.0[1]];
346 let vec_b = [b.x - last_points.1[0], b.y - last_points.1[1]];
347
348 if self.pitch_valid.is_none() {
352 let a_mag = (vec_a[0] * vec_a[0] + vec_a[1] * vec_a[1]).sqrt();
353 let b_mag = (vec_b[0] * vec_b[0] + vec_b[1] * vec_b[1]).sqrt();
354 if a_mag > 2.0 && b_mag > 2.0 {
355 let a_vert = vec_a[1].abs() > vec_a[0].abs();
356 let b_vert = vec_b[1].abs() > vec_b[0].abs();
357 let same_dir = vec_a[1] * vec_b[1] > 0.0;
359 self.pitch_valid = Some(a_vert && b_vert && same_dir);
360 }
361 }
362
363 if self.pitch_valid != Some(true) {
364 return None;
367 }
368
369 self.pitch_last_points = Some(([a.x, a.y], [b.x, b.y]));
371
372 let y_avg = (vec_a[1] + vec_b[1]) * 0.5;
374 if y_avg.abs() < f64::EPSILON {
375 return None;
376 }
377
378 let pitch_delta_rad = (y_avg * PITCH_DEGREES_PER_PX).to_radians();
379 Some(InputEvent::Rotate {
380 delta_yaw: 0.0,
381 delta_pitch: pitch_delta_rad,
382 })
383 }
384
385 fn pan_anchor(&self) -> (f64, f64) {
388 if self.fingers.is_empty() {
389 return (0.0, 0.0);
390 }
391 let (mut sx, mut sy) = (0.0, 0.0);
392 for f in self.fingers.values() {
393 sx += f.x;
394 sy += f.y;
395 }
396 let n = self.fingers.len() as f64;
397 (sx / n, sy / n)
398 }
399
400 fn reset_two_finger(&mut self) {
401 self.two_finger_ids = None;
402 self.start_distance = 0.0;
403 self.last_distance = 0.0;
404 self.start_vector = None;
405 self.last_vector = None;
406 self.min_diameter = 0.0;
407 self.zoom_active = false;
408 self.rotate_active = false;
409 self.pitch_valid = None;
410 self.pitch_last_points = None;
411 }
412}
413
414#[inline]
420fn distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
421 let dx = x2 - x1;
422 let dy = y2 - y1;
423 (dx * dx + dy * dy).sqrt()
424}
425
426fn angle_between(a: [f64; 2], b: [f64; 2]) -> f64 {
430 let cross = b[0] * a[1] - b[1] * a[0];
431 let dot = a[0] * b[0] + a[1] * b[1];
432 cross.atan2(dot).to_degrees()
433}
434
435#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::input::TouchPhase;
443
444 fn tc(id: u64, phase: TouchPhase, x: f64, y: f64) -> TouchContact {
445 TouchContact { id, phase, x, y }
446 }
447
448 #[test]
451 fn single_finger_pan() {
452 let mut g = GestureRecognizer::new();
453 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
454 let events = g.process(tc(0, TouchPhase::Moved, 110.0, 205.0));
455 assert_eq!(events.len(), 1);
456 match events[0] {
457 InputEvent::Pan { dx, dy, .. } => {
458 assert!((dx - 10.0).abs() < 1e-9);
459 assert!((dy - 5.0).abs() < 1e-9);
460 }
461 _ => panic!("expected Pan"),
462 }
463 }
464
465 #[test]
466 fn finger_end_clears_state() {
467 let mut g = GestureRecognizer::new();
468 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
469 let _ = g.process(tc(0, TouchPhase::Ended, 100.0, 200.0));
470 assert_eq!(g.finger_count(), 0);
471 }
472
473 #[test]
476 fn pinch_zoom_produces_zoom_event() {
477 let mut g = GestureRecognizer::new();
478 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
480 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
481
482 let _ = g.process(tc(0, TouchPhase::Moved, 50.0, 200.0));
484 let events = g.process(tc(1, TouchPhase::Moved, 250.0, 200.0));
485
486 let has_zoom = events.iter().any(|e| e.is_zoom());
487 assert!(has_zoom, "expected zoom event from pinch: {events:?}");
488 }
489
490 #[test]
491 fn pinch_zoom_below_threshold_does_not_activate() {
492 let mut g = GestureRecognizer::new();
493 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
494 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
495
496 let events = g.process(tc(1, TouchPhase::Moved, 201.0, 200.0));
498 let has_zoom = events.iter().any(|e| e.is_zoom());
499 assert!(!has_zoom, "should not zoom below threshold");
500 }
501
502 #[test]
505 fn rotation_produces_rotate_event() {
506 let mut g = GestureRecognizer::new();
507 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
509 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
510
511 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 250.0));
513 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 150.0));
514
515 let has_rotate = events.iter().any(|e| {
516 matches!(e,
517 InputEvent::Rotate { delta_yaw, .. } if delta_yaw.abs() > 1e-6
518 )
519 });
520 assert!(has_rotate, "expected rotation event: {events:?}");
521 }
522
523 #[test]
526 fn vertical_drag_produces_pitch() {
527 let mut g = GestureRecognizer::new();
528 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
529 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
530
531 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 230.0));
533 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 230.0));
534
535 let has_pitch = events.iter().any(|e| {
536 matches!(e,
537 InputEvent::Rotate { delta_pitch, .. } if delta_pitch.abs() > 1e-6
538 )
539 });
540 assert!(has_pitch, "expected pitch event: {events:?}");
541 }
542
543 #[test]
546 fn lifting_one_finger_resets_two_finger_state() {
547 let mut g = GestureRecognizer::new();
548 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
549 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
550 assert!(g.two_finger_ids.is_some());
551
552 let _ = g.process(tc(1, TouchPhase::Ended, 200.0, 200.0));
553 assert!(g.two_finger_ids.is_none());
554 }
555
556 #[test]
557 fn cancel_resets_everything() {
558 let mut g = GestureRecognizer::new();
559 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
560 g.reset();
561 assert_eq!(g.finger_count(), 0);
562 assert!(g.two_finger_ids.is_none());
563 }
564
565 #[test]
566 fn third_finger_ignored_for_two_finger_gesture() {
567 let mut g = GestureRecognizer::new();
568 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
569 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
570 let ids_before = g.two_finger_ids;
571 let _ = g.process(tc(2, TouchPhase::Started, 300.0, 200.0));
572 assert_eq!(g.two_finger_ids, ids_before);
574 }
575
576 #[test]
579 fn pan_anchor_is_centroid() {
580 let mut g = GestureRecognizer::new();
581 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
582 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
583 let (ax, ay) = g.pan_anchor();
584 assert!((ax - 150.0).abs() < 1e-9);
585 assert!((ay - 200.0).abs() < 1e-9);
586 }
587
588 #[test]
591 fn angle_between_90_degrees() {
592 let a = [1.0, 0.0];
593 let b = [0.0, 1.0];
594 let angle = angle_between(a, b);
595 assert!((angle.abs() - 90.0).abs() < 0.1, "got {angle}");
596 }
597
598 #[test]
599 fn angle_between_opposite_is_180() {
600 let a = [1.0, 0.0];
601 let b = [-1.0, 0.0];
602 let angle = angle_between(a, b);
603 assert!((angle.abs() - 180.0).abs() < 0.1, "got {angle}");
604 }
605
606 #[test]
609 fn pinch_out_zooms_in() {
610 let mut g = GestureRecognizer::new();
611 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
612 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
613
614 let _ = g.process(tc(0, TouchPhase::Moved, 0.0, 200.0));
616 let events = g.process(tc(1, TouchPhase::Moved, 300.0, 200.0));
617
618 let zoom_event = events.iter().find(|e| e.is_zoom());
619 if let Some(InputEvent::Zoom { factor, .. }) = zoom_event {
620 assert!(
621 *factor > 1.0,
622 "spreading fingers should zoom in, got factor={factor}"
623 );
624 } else {
625 panic!("expected zoom event: {events:?}");
626 }
627 }
628
629 #[test]
630 fn pinch_in_zooms_out() {
631 let mut g = GestureRecognizer::new();
632 let _ = g.process(tc(0, TouchPhase::Started, 0.0, 200.0));
633 let _ = g.process(tc(1, TouchPhase::Started, 300.0, 200.0));
634
635 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 200.0));
637 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 200.0));
638
639 let zoom_event = events.iter().find(|e| e.is_zoom());
640 if let Some(InputEvent::Zoom { factor, .. }) = zoom_event {
641 assert!(
642 *factor < 1.0,
643 "squeezing fingers should zoom out, got factor={factor}"
644 );
645 } else {
646 panic!("expected zoom event: {events:?}");
647 }
648 }
649}