use crate::layout::Point;
use rand::Rng;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct SmartMouseConfig {
pub steps: usize,
pub overshoot: f64,
pub jitter: f64,
pub step_delay_ms: u64,
pub easing: bool,
}
impl Default for SmartMouseConfig {
fn default() -> Self {
Self {
steps: 25,
overshoot: 0.15,
jitter: 1.5,
step_delay_ms: 8,
easing: true,
}
}
}
#[derive(Debug, Clone)]
pub struct MovementStep {
pub point: Point,
pub delay: Duration,
}
fn cubic_bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
let inv = 1.0 - t;
let inv2 = inv * inv;
let inv3 = inv2 * inv;
let t2 = t * t;
let t3 = t2 * t;
Point {
x: inv3 * p0.x + 3.0 * inv2 * t * p1.x + 3.0 * inv * t2 * p2.x + t3 * p3.x,
y: inv3 * p0.y + 3.0 * inv2 * t * p1.y + 3.0 * inv * t2 * p2.y + t3 * p3.y,
}
}
fn ease_in_out(t: f64) -> f64 {
if t < 0.5 {
4.0 * t * t * t
} else {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}
}
pub fn generate_path(from: Point, to: Point, config: &SmartMouseConfig) -> Vec<MovementStep> {
let mut rng = rand::rng();
let steps = config.steps.max(2);
let dx = to.x - from.x;
let dy = to.y - from.y;
let distance = (dx * dx + dy * dy).sqrt();
if distance < 2.0 {
return vec![MovementStep {
point: to,
delay: Duration::from_millis(config.step_delay_ms),
}];
}
let (perp_x, perp_y) = if distance > 0.001 {
(-dy / distance, dx / distance)
} else {
(0.0, 1.0)
};
let spread = distance * 0.3;
let offset1: f64 = rng.random_range(-spread..spread);
let offset2: f64 = rng.random_range(-spread..spread);
let cp1 = Point {
x: from.x + dx * 0.25 + perp_x * offset1,
y: from.y + dy * 0.25 + perp_y * offset1,
};
let cp2 = Point {
x: from.x + dx * 0.75 + perp_x * offset2,
y: from.y + dy * 0.75 + perp_y * offset2,
};
let should_overshoot = config.overshoot > 0.0 && distance > 10.0;
let overshoot_target = if should_overshoot {
let overshoot_amount = distance * config.overshoot * rng.random_range(0.5..1.5);
Point {
x: to.x + (dx / distance) * overshoot_amount,
y: to.y + (dy / distance) * overshoot_amount,
}
} else {
to
};
let main_steps = if should_overshoot {
(steps as f64 * 0.85) as usize
} else {
steps
};
let mut path = Vec::with_capacity(steps + 2);
let end = if should_overshoot {
overshoot_target
} else {
to
};
for i in 1..=main_steps {
let raw_t = i as f64 / main_steps as f64;
let t = if config.easing {
ease_in_out(raw_t)
} else {
raw_t
};
let mut p = cubic_bezier(from, cp1, cp2, end, t);
if config.jitter > 0.0 && i < main_steps.saturating_sub(2) {
p.x += rng.random_range(-config.jitter..config.jitter);
p.y += rng.random_range(-config.jitter..config.jitter);
}
let delay_variation: f64 = rng.random_range(0.7..1.3);
let delay = Duration::from_millis((config.step_delay_ms as f64 * delay_variation) as u64);
path.push(MovementStep { point: p, delay });
}
if should_overshoot {
let correction_steps = steps.saturating_sub(main_steps).max(3);
let last = path.last().map(|s| s.point).unwrap_or(overshoot_target);
for i in 1..=correction_steps {
let t = i as f64 / correction_steps as f64;
let t = if config.easing { ease_in_out(t) } else { t };
let p = Point {
x: last.x + (to.x - last.x) * t,
y: last.y + (to.y - last.y) * t,
};
let delay = Duration::from_millis((config.step_delay_ms as f64 * 0.6) as u64);
path.push(MovementStep { point: p, delay });
}
}
if let Some(last) = path.last_mut() {
last.point = to;
}
path
}
pub struct SmartMouse {
pos_x: AtomicU64,
pos_y: AtomicU64,
config: SmartMouseConfig,
}
impl std::fmt::Debug for SmartMouse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pos = self.position();
f.debug_struct("SmartMouse")
.field("position", &pos)
.field("config", &self.config)
.finish()
}
}
impl SmartMouse {
pub fn new() -> Self {
Self {
pos_x: AtomicU64::new(0.0_f64.to_bits()),
pos_y: AtomicU64::new(0.0_f64.to_bits()),
config: SmartMouseConfig::default(),
}
}
pub fn with_config(config: SmartMouseConfig) -> Self {
Self {
pos_x: AtomicU64::new(0.0_f64.to_bits()),
pos_y: AtomicU64::new(0.0_f64.to_bits()),
config,
}
}
pub fn position(&self) -> Point {
Point {
x: f64::from_bits(self.pos_x.load(Ordering::Relaxed)),
y: f64::from_bits(self.pos_y.load(Ordering::Relaxed)),
}
}
pub fn set_position(&self, point: Point) {
self.pos_x.store(point.x.to_bits(), Ordering::Relaxed);
self.pos_y.store(point.y.to_bits(), Ordering::Relaxed);
}
pub fn config(&self) -> &SmartMouseConfig {
&self.config
}
pub fn path_to(&self, target: Point) -> Vec<MovementStep> {
let from = self.position();
self.set_position(target);
generate_path(from, target, &self.config)
}
}
impl Default for SmartMouse {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cubic_bezier_endpoints() {
let p0 = Point::new(0.0, 0.0);
let p1 = Point::new(25.0, 50.0);
let p2 = Point::new(75.0, 50.0);
let p3 = Point::new(100.0, 100.0);
let start = cubic_bezier(p0, p1, p2, p3, 0.0);
assert!((start.x - p0.x).abs() < 1e-10);
assert!((start.y - p0.y).abs() < 1e-10);
let end = cubic_bezier(p0, p1, p2, p3, 1.0);
assert!((end.x - p3.x).abs() < 1e-10);
assert!((end.y - p3.y).abs() < 1e-10);
}
#[test]
fn test_cubic_bezier_midpoint() {
let p0 = Point::new(0.0, 0.0);
let p1 = Point::new(33.3, 33.3);
let p2 = Point::new(66.6, 66.6);
let p3 = Point::new(100.0, 100.0);
let mid = cubic_bezier(p0, p1, p2, p3, 0.5);
assert!((mid.x - 50.0).abs() < 1.0);
assert!((mid.y - 50.0).abs() < 1.0);
}
#[test]
fn test_ease_in_out_boundaries() {
assert!((ease_in_out(0.0)).abs() < 1e-10);
assert!((ease_in_out(1.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_ease_in_out_midpoint() {
let mid = ease_in_out(0.5);
assert!((mid - 0.5).abs() < 1e-10);
}
#[test]
fn test_ease_in_out_monotonic() {
let mut prev = 0.0;
for i in 1..=100 {
let t = i as f64 / 100.0;
let val = ease_in_out(t);
assert!(
val >= prev,
"ease_in_out should be monotonically increasing"
);
prev = val;
}
}
#[test]
fn test_generate_path_ends_at_target() {
let from = Point::new(10.0, 20.0);
let to = Point::new(500.0, 300.0);
let config = SmartMouseConfig::default();
let path = generate_path(from, to, &config);
assert!(!path.is_empty());
let last = &path.last().unwrap().point;
assert!(
(last.x - to.x).abs() < 1e-10 && (last.y - to.y).abs() < 1e-10,
"path must end exactly at target, got ({}, {})",
last.x,
last.y
);
}
#[test]
fn test_generate_path_short_distance() {
let from = Point::new(100.0, 100.0);
let to = Point::new(100.5, 100.5);
let config = SmartMouseConfig::default();
let path = generate_path(from, to, &config);
assert_eq!(
path.len(),
1,
"very short moves should produce a single step"
);
assert!((path[0].point.x - to.x).abs() < 1e-10);
assert!((path[0].point.y - to.y).abs() < 1e-10);
}
#[test]
fn test_generate_path_no_overshoot() {
let from = Point::new(0.0, 0.0);
let to = Point::new(200.0, 200.0);
let config = SmartMouseConfig {
overshoot: 0.0,
..Default::default()
};
let path = generate_path(from, to, &config);
assert_eq!(path.len(), config.steps);
}
#[test]
fn test_generate_path_no_jitter() {
let from = Point::new(0.0, 0.0);
let to = Point::new(200.0, 200.0);
let config = SmartMouseConfig {
jitter: 0.0,
overshoot: 0.0,
easing: false,
..Default::default()
};
let path = generate_path(from, to, &config);
assert!(!path.is_empty());
let last = &path.last().unwrap().point;
assert!((last.x - to.x).abs() < 1e-10);
assert!((last.y - to.y).abs() < 1e-10);
}
#[test]
fn test_generate_path_step_count_with_overshoot() {
let from = Point::new(0.0, 0.0);
let to = Point::new(500.0, 500.0);
let config = SmartMouseConfig {
steps: 30,
overshoot: 0.2,
..Default::default()
};
let path = generate_path(from, to, &config);
assert!(path.len() >= config.steps);
}
#[test]
fn test_generate_path_no_huge_jumps() {
let from = Point::new(0.0, 0.0);
let to = Point::new(300.0, 300.0);
let config = SmartMouseConfig {
steps: 50,
overshoot: 0.0,
jitter: 0.0,
..Default::default()
};
let path = generate_path(from, to, &config);
let mut prev = from;
let max_distance = (300.0_f64 * 300.0 + 300.0 * 300.0).sqrt();
for step in &path {
let dx = step.point.x - prev.x;
let dy = step.point.y - prev.y;
let step_dist = (dx * dx + dy * dy).sqrt();
assert!(
step_dist < max_distance * 0.6,
"step jumped {} pixels (max total: {})",
step_dist,
max_distance
);
prev = step.point;
}
}
#[test]
fn test_smart_mouse_position_tracking() {
let mouse = SmartMouse::new();
assert_eq!(mouse.position(), Point::new(0.0, 0.0));
mouse.set_position(Point::new(100.0, 200.0));
assert_eq!(mouse.position(), Point::new(100.0, 200.0));
}
#[test]
fn test_smart_mouse_path_to_updates_position() {
let mouse = SmartMouse::new();
let target = Point::new(500.0, 300.0);
let path = mouse.path_to(target);
assert!(!path.is_empty());
assert_eq!(mouse.position(), target);
}
#[test]
fn test_smart_mouse_consecutive_paths() {
let mouse = SmartMouse::with_config(SmartMouseConfig {
overshoot: 0.0,
jitter: 0.0,
..Default::default()
});
let target1 = Point::new(100.0, 100.0);
let path1 = mouse.path_to(target1);
assert!(!path1.is_empty());
assert_eq!(mouse.position(), target1);
let target2 = Point::new(400.0, 300.0);
let _path2 = mouse.path_to(target2);
assert_eq!(mouse.position(), target2);
}
#[test]
fn test_smart_mouse_same_position_no_move() {
let mouse = SmartMouse::new();
mouse.set_position(Point::new(100.0, 100.0));
let path = mouse.path_to(Point::new(100.0, 100.0));
assert_eq!(path.len(), 1);
}
#[test]
fn test_smart_mouse_custom_config() {
let config = SmartMouseConfig {
steps: 10,
overshoot: 0.0,
jitter: 0.0,
step_delay_ms: 16,
easing: false,
};
let mouse = SmartMouse::with_config(config.clone());
let path = mouse.path_to(Point::new(200.0, 200.0));
assert_eq!(path.len(), config.steps);
}
#[test]
fn test_movement_delays_are_reasonable() {
let config = SmartMouseConfig {
step_delay_ms: 10,
..Default::default()
};
let path = generate_path(Point::new(0.0, 0.0), Point::new(500.0, 500.0), &config);
for step in &path {
assert!(
step.delay.as_millis() <= 30,
"delay too large: {:?}",
step.delay
);
}
}
#[test]
fn test_default_config() {
let config = SmartMouseConfig::default();
assert_eq!(config.steps, 25);
assert!((config.overshoot - 0.15).abs() < 1e-10);
assert!((config.jitter - 1.5).abs() < 1e-10);
assert_eq!(config.step_delay_ms, 8);
assert!(config.easing);
}
}