use std::time::{Duration, Instant};
use kui_core::Vec2;
pub(crate) const GAP: Duration = Duration::from_millis(200);
pub(crate) const DECIDE: f32 = 4.0;
pub(crate) const DECAY: f32 = 0.75;
pub(crate) const SWITCH_RATIO: f32 = 3.0;
pub(crate) const SWITCH_MIN: f32 = 10.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Axis {
X,
Y,
}
#[derive(Debug, Default)]
pub(crate) struct AxisLock {
axis: Option<Axis>,
last: Option<Instant>,
travel: (f32, f32),
recent: (f32, f32),
}
impl AxisLock {
pub(crate) fn end(&mut self) {
*self = AxisLock::default();
}
pub(crate) fn pixel(&mut self, d: Vec2, now: Instant) -> Vec2 {
if self
.last
.is_some_and(|t| now.saturating_duration_since(t) > GAP)
{
*self = AxisLock::default();
}
self.last = Some(now);
let (ax, ay) = (d.x.abs(), d.y.abs());
self.recent = (self.recent.0 * DECAY + ax, self.recent.1 * DECAY + ay);
let axis = match self.axis {
None => {
self.travel = (self.travel.0 + ax, self.travel.1 + ay);
let (tx, ty) = self.travel;
if tx + ty >= DECIDE {
let a = if tx > ty { Axis::X } else { Axis::Y };
self.axis = Some(a);
a
} else if ax > ay {
Axis::X
} else {
Axis::Y
}
}
Some(a) => {
let (along, across, other) = match a {
Axis::X => (self.recent.0, self.recent.1, Axis::Y),
Axis::Y => (self.recent.1, self.recent.0, Axis::X),
};
if across >= SWITCH_MIN && across > along * SWITCH_RATIO {
self.axis = Some(other);
other
} else {
a
}
}
};
match axis {
Axis::X => Vec2::new(d.x, 0.0),
Axis::Y => Vec2::new(0.0, d.y),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const FRAME: Duration = Duration::from_millis(8);
fn swipe(l: &mut AxisLock, t: &mut Instant, d: Vec2, n: usize) -> Vec2 {
let mut out = Vec2::ZERO;
for _ in 0..n {
*t += FRAME;
let p = l.pixel(d, *t);
out = Vec2::new(out.x + p.x, out.y + p.y);
}
out
}
#[test]
fn a_vertical_swipe_drops_its_drift() {
let mut t = Instant::now();
let mut l = AxisLock::default();
let hand = swipe(&mut l, &mut t, Vec2::new(1.5, -12.0), 20);
assert_eq!(hand, Vec2::new(0.0, -240.0));
let glide = swipe(&mut l, &mut t, Vec2::new(0.6, -3.0), 30);
assert_eq!(glide.x, 0.0, "momentum keeps the hand's axis");
let drift = swipe(&mut l, &mut t, Vec2::new(-4.0, -5.0), 10);
assert_eq!(drift.x, 0.0, "sideways under the ratio stays dropped");
}
#[test]
fn a_sideways_swipe_drops_its_vertical_drift() {
let mut t = Instant::now();
let mut l = AxisLock::default();
let out = swipe(&mut l, &mut t, Vec2::new(9.0, 2.0), 10);
assert_eq!(out, Vec2::new(90.0, 0.0));
}
#[test]
fn an_undecided_swipe_keeps_each_events_larger_axis() {
let t = Instant::now();
let mut l = AxisLock::default();
assert_eq!(l.pixel(Vec2::new(0.5, 1.0), t), Vec2::new(0.0, 1.0));
assert_eq!(l.pixel(Vec2::new(1.0, 0.5), t + FRAME), Vec2::new(1.0, 0.0));
assert_eq!(
l.pixel(Vec2::new(0.2, 1.0), t + FRAME * 2),
Vec2::new(0.0, 1.0)
);
assert_eq!(l.axis, Some(Axis::Y));
}
#[test]
fn a_turn_the_other_axis_clearly_carries_turns_the_lock() {
let mut t = Instant::now();
let mut l = AxisLock::default();
swipe(&mut l, &mut t, Vec2::new(0.0, 10.0), 10);
let turned = swipe(&mut l, &mut t, Vec2::new(10.0, 1.0), 10);
assert!(turned.x > 0.0, "the turn goes through: {turned:?}");
assert_eq!(l.axis, Some(Axis::X));
let last = l.pixel(Vec2::new(10.0, 1.0), t + FRAME);
assert_eq!(last, Vec2::new(10.0, 0.0), "and is locked in turn");
}
#[test]
fn a_slow_turn_turns_the_lock() {
let mut t = Instant::now();
let mut l = AxisLock::default();
swipe(&mut l, &mut t, Vec2::new(0.0, 10.0), 10);
let turned = swipe(&mut l, &mut t, Vec2::new(3.0, 0.5), 20);
assert!(turned.x > 0.0, "{turned:?}");
assert_eq!(l.axis, Some(Axis::X));
}
#[test]
fn a_pause_ends_the_swipe() {
let mut t = Instant::now();
let mut l = AxisLock::default();
swipe(&mut l, &mut t, Vec2::new(0.0, 10.0), 10);
t += GAP + FRAME;
let out = swipe(&mut l, &mut t, Vec2::new(6.0, 1.0), 3);
assert_eq!(out, Vec2::new(18.0, 0.0));
}
#[test]
fn a_notch_ends_the_swipe() {
let mut t = Instant::now();
let mut l = AxisLock::default();
swipe(&mut l, &mut t, Vec2::new(0.0, 10.0), 10);
l.end();
let out = swipe(&mut l, &mut t, Vec2::new(6.0, 1.0), 3);
assert_eq!(out, Vec2::new(18.0, 0.0));
}
}