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(
337 &mut self,
338 a: FingerState,
339 b: FingerState,
340 moved_id: u64,
341 ) -> Option<InputEvent> {
342 let (id_a, id_b) = self.two_finger_ids?;
343
344 if moved_id != id_a && moved_id != id_b {
346 return None;
347 }
348
349 let last_points = self.pitch_last_points?;
350 let vec_a = [a.x - last_points.0[0], a.y - last_points.0[1]];
351 let vec_b = [b.x - last_points.1[0], b.y - last_points.1[1]];
352
353 if self.pitch_valid.is_none() {
357 let a_mag = (vec_a[0] * vec_a[0] + vec_a[1] * vec_a[1]).sqrt();
358 let b_mag = (vec_b[0] * vec_b[0] + vec_b[1] * vec_b[1]).sqrt();
359 if a_mag > 2.0 && b_mag > 2.0 {
360 let a_vert = vec_a[1].abs() > vec_a[0].abs();
361 let b_vert = vec_b[1].abs() > vec_b[0].abs();
362 let same_dir = vec_a[1] * vec_b[1] > 0.0;
364 self.pitch_valid = Some(a_vert && b_vert && same_dir);
365 }
366 }
367
368 if self.pitch_valid != Some(true) {
369 return None;
372 }
373
374 self.pitch_last_points = Some(([a.x, a.y], [b.x, b.y]));
376
377 let y_avg = (vec_a[1] + vec_b[1]) * 0.5;
379 if y_avg.abs() < f64::EPSILON {
380 return None;
381 }
382
383 let pitch_delta_rad = (y_avg * PITCH_DEGREES_PER_PX).to_radians();
384 Some(InputEvent::Rotate {
385 delta_yaw: 0.0,
386 delta_pitch: pitch_delta_rad,
387 })
388 }
389
390 fn pan_anchor(&self) -> (f64, f64) {
393 if self.fingers.is_empty() {
394 return (0.0, 0.0);
395 }
396 let (mut sx, mut sy) = (0.0, 0.0);
397 for f in self.fingers.values() {
398 sx += f.x;
399 sy += f.y;
400 }
401 let n = self.fingers.len() as f64;
402 (sx / n, sy / n)
403 }
404
405 fn reset_two_finger(&mut self) {
406 self.two_finger_ids = None;
407 self.start_distance = 0.0;
408 self.last_distance = 0.0;
409 self.start_vector = None;
410 self.last_vector = None;
411 self.min_diameter = 0.0;
412 self.zoom_active = false;
413 self.rotate_active = false;
414 self.pitch_valid = None;
415 self.pitch_last_points = None;
416 }
417}
418
419#[inline]
425fn distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
426 let dx = x2 - x1;
427 let dy = y2 - y1;
428 (dx * dx + dy * dy).sqrt()
429}
430
431fn angle_between(a: [f64; 2], b: [f64; 2]) -> f64 {
435 let cross = b[0] * a[1] - b[1] * a[0];
436 let dot = a[0] * b[0] + a[1] * b[1];
437 cross.atan2(dot).to_degrees()
438}
439
440#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::input::TouchPhase;
448
449 fn tc(id: u64, phase: TouchPhase, x: f64, y: f64) -> TouchContact {
450 TouchContact { id, phase, x, y }
451 }
452
453 #[test]
456 fn single_finger_pan() {
457 let mut g = GestureRecognizer::new();
458 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
459 let events = g.process(tc(0, TouchPhase::Moved, 110.0, 205.0));
460 assert_eq!(events.len(), 1);
461 match events[0] {
462 InputEvent::Pan { dx, dy, .. } => {
463 assert!((dx - 10.0).abs() < 1e-9);
464 assert!((dy - 5.0).abs() < 1e-9);
465 }
466 _ => panic!("expected Pan"),
467 }
468 }
469
470 #[test]
471 fn finger_end_clears_state() {
472 let mut g = GestureRecognizer::new();
473 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
474 let _ = g.process(tc(0, TouchPhase::Ended, 100.0, 200.0));
475 assert_eq!(g.finger_count(), 0);
476 }
477
478 #[test]
481 fn pinch_zoom_produces_zoom_event() {
482 let mut g = GestureRecognizer::new();
483 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
485 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
486
487 let _ = g.process(tc(0, TouchPhase::Moved, 50.0, 200.0));
489 let events = g.process(tc(1, TouchPhase::Moved, 250.0, 200.0));
490
491 let has_zoom = events.iter().any(|e| e.is_zoom());
492 assert!(has_zoom, "expected zoom event from pinch: {events:?}");
493 }
494
495 #[test]
496 fn pinch_zoom_below_threshold_does_not_activate() {
497 let mut g = GestureRecognizer::new();
498 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
499 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
500
501 let events = g.process(tc(1, TouchPhase::Moved, 201.0, 200.0));
503 let has_zoom = events.iter().any(|e| e.is_zoom());
504 assert!(!has_zoom, "should not zoom below threshold");
505 }
506
507 #[test]
510 fn rotation_produces_rotate_event() {
511 let mut g = GestureRecognizer::new();
512 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
514 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
515
516 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 250.0));
518 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 150.0));
519
520 let has_rotate = events.iter().any(|e| matches!(e,
521 InputEvent::Rotate { delta_yaw, .. } if delta_yaw.abs() > 1e-6
522 ));
523 assert!(has_rotate, "expected rotation event: {events:?}");
524 }
525
526 #[test]
529 fn vertical_drag_produces_pitch() {
530 let mut g = GestureRecognizer::new();
531 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
532 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
533
534 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 230.0));
536 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 230.0));
537
538 let has_pitch = events.iter().any(|e| matches!(e,
539 InputEvent::Rotate { delta_pitch, .. } if delta_pitch.abs() > 1e-6
540 ));
541 assert!(has_pitch, "expected pitch event: {events:?}");
542 }
543
544 #[test]
547 fn lifting_one_finger_resets_two_finger_state() {
548 let mut g = GestureRecognizer::new();
549 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
550 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
551 assert!(g.two_finger_ids.is_some());
552
553 let _ = g.process(tc(1, TouchPhase::Ended, 200.0, 200.0));
554 assert!(g.two_finger_ids.is_none());
555 }
556
557 #[test]
558 fn cancel_resets_everything() {
559 let mut g = GestureRecognizer::new();
560 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
561 g.reset();
562 assert_eq!(g.finger_count(), 0);
563 assert!(g.two_finger_ids.is_none());
564 }
565
566 #[test]
567 fn third_finger_ignored_for_two_finger_gesture() {
568 let mut g = GestureRecognizer::new();
569 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
570 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
571 let ids_before = g.two_finger_ids;
572 let _ = g.process(tc(2, TouchPhase::Started, 300.0, 200.0));
573 assert_eq!(g.two_finger_ids, ids_before);
575 }
576
577 #[test]
580 fn pan_anchor_is_centroid() {
581 let mut g = GestureRecognizer::new();
582 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
583 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
584 let (ax, ay) = g.pan_anchor();
585 assert!((ax - 150.0).abs() < 1e-9);
586 assert!((ay - 200.0).abs() < 1e-9);
587 }
588
589 #[test]
592 fn angle_between_90_degrees() {
593 let a = [1.0, 0.0];
594 let b = [0.0, 1.0];
595 let angle = angle_between(a, b);
596 assert!((angle.abs() - 90.0).abs() < 0.1, "got {angle}");
597 }
598
599 #[test]
600 fn angle_between_opposite_is_180() {
601 let a = [1.0, 0.0];
602 let b = [-1.0, 0.0];
603 let angle = angle_between(a, b);
604 assert!((angle.abs() - 180.0).abs() < 0.1, "got {angle}");
605 }
606
607 #[test]
610 fn pinch_out_zooms_in() {
611 let mut g = GestureRecognizer::new();
612 let _ = g.process(tc(0, TouchPhase::Started, 100.0, 200.0));
613 let _ = g.process(tc(1, TouchPhase::Started, 200.0, 200.0));
614
615 let _ = g.process(tc(0, TouchPhase::Moved, 0.0, 200.0));
617 let events = g.process(tc(1, TouchPhase::Moved, 300.0, 200.0));
618
619 let zoom_event = events.iter().find(|e| e.is_zoom());
620 if let Some(InputEvent::Zoom { factor, .. }) = zoom_event {
621 assert!(*factor > 1.0, "spreading fingers should zoom in, got factor={factor}");
622 } else {
623 panic!("expected zoom event: {events:?}");
624 }
625 }
626
627 #[test]
628 fn pinch_in_zooms_out() {
629 let mut g = GestureRecognizer::new();
630 let _ = g.process(tc(0, TouchPhase::Started, 0.0, 200.0));
631 let _ = g.process(tc(1, TouchPhase::Started, 300.0, 200.0));
632
633 let _ = g.process(tc(0, TouchPhase::Moved, 100.0, 200.0));
635 let events = g.process(tc(1, TouchPhase::Moved, 200.0, 200.0));
636
637 let zoom_event = events.iter().find(|e| e.is_zoom());
638 if let Some(InputEvent::Zoom { factor, .. }) = zoom_event {
639 assert!(*factor < 1.0, "squeezing fingers should zoom out, got factor={factor}");
640 } else {
641 panic!("expected zoom event: {events:?}");
642 }
643 }
644}