1use crate::layout::Point;
9use rand::RngExt;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::Duration;
12
13#[derive(Debug, Clone)]
15pub struct SmartMouseConfig {
16 pub steps: usize,
18 pub overshoot: f64,
20 pub jitter: f64,
22 pub step_delay_ms: u64,
24 pub easing: bool,
26}
27
28impl Default for SmartMouseConfig {
29 fn default() -> Self {
30 Self {
31 steps: 25,
32 overshoot: 0.15,
33 jitter: 1.5,
34 step_delay_ms: 8,
35 easing: true,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct MovementStep {
43 pub point: Point,
45 pub delay: Duration,
47}
48
49fn cubic_bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: f64) -> Point {
54 let inv = 1.0 - t;
55 let inv2 = inv * inv;
56 let inv3 = inv2 * inv;
57 let t2 = t * t;
58 let t3 = t2 * t;
59
60 Point {
61 x: inv3 * p0.x + 3.0 * inv2 * t * p1.x + 3.0 * inv * t2 * p2.x + t3 * p3.x,
62 y: inv3 * p0.y + 3.0 * inv2 * t * p1.y + 3.0 * inv * t2 * p2.y + t3 * p3.y,
63 }
64}
65
66fn ease_in_out(t: f64) -> f64 {
68 if t < 0.5 {
69 4.0 * t * t * t
70 } else {
71 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
72 }
73}
74
75pub fn generate_path(from: Point, to: Point, config: &SmartMouseConfig) -> Vec<MovementStep> {
80 let mut rng = rand::rng();
81 let steps = config.steps.max(2);
82
83 let dx = to.x - from.x;
84 let dy = to.y - from.y;
85 let distance = (dx * dx + dy * dy).sqrt();
86
87 if distance < 2.0 {
89 return vec![MovementStep {
90 point: to,
91 delay: Duration::from_millis(config.step_delay_ms),
92 }];
93 }
94
95 let (perp_x, perp_y) = if distance > 0.001 {
97 (-dy / distance, dx / distance)
98 } else {
99 (0.0, 1.0)
100 };
101
102 let spread = distance * 0.3;
104 let offset1: f64 = rng.random_range(-spread..spread);
105 let offset2: f64 = rng.random_range(-spread..spread);
106
107 let cp1 = Point {
108 x: from.x + dx * 0.25 + perp_x * offset1,
109 y: from.y + dy * 0.25 + perp_y * offset1,
110 };
111 let cp2 = Point {
112 x: from.x + dx * 0.75 + perp_x * offset2,
113 y: from.y + dy * 0.75 + perp_y * offset2,
114 };
115
116 let should_overshoot = config.overshoot > 0.0 && distance > 10.0;
118
119 let overshoot_target = if should_overshoot {
120 let overshoot_amount = distance * config.overshoot * rng.random_range(0.5..1.5);
121 Point {
122 x: to.x + (dx / distance) * overshoot_amount,
123 y: to.y + (dy / distance) * overshoot_amount,
124 }
125 } else {
126 to
127 };
128
129 let main_steps = if should_overshoot {
130 (steps as f64 * 0.85) as usize
131 } else {
132 steps
133 };
134
135 let mut path = Vec::with_capacity(steps + 2);
136
137 let end = if should_overshoot {
139 overshoot_target
140 } else {
141 to
142 };
143
144 for i in 1..=main_steps {
145 let raw_t = i as f64 / main_steps as f64;
146 let t = if config.easing {
147 ease_in_out(raw_t)
148 } else {
149 raw_t
150 };
151
152 let mut p = cubic_bezier(from, cp1, cp2, end, t);
153
154 if config.jitter > 0.0 && i < main_steps.saturating_sub(2) {
156 p.x += rng.random_range(-config.jitter..config.jitter);
157 p.y += rng.random_range(-config.jitter..config.jitter);
158 }
159
160 let delay_variation: f64 = rng.random_range(0.7..1.3);
162 let delay = Duration::from_millis((config.step_delay_ms as f64 * delay_variation) as u64);
163
164 path.push(MovementStep { point: p, delay });
165 }
166
167 if should_overshoot {
169 let correction_steps = steps.saturating_sub(main_steps).max(3);
170 let last = path.last().map(|s| s.point).unwrap_or(overshoot_target);
171
172 for i in 1..=correction_steps {
173 let t = i as f64 / correction_steps as f64;
174 let t = if config.easing { ease_in_out(t) } else { t };
175
176 let p = Point {
177 x: last.x + (to.x - last.x) * t,
178 y: last.y + (to.y - last.y) * t,
179 };
180
181 let delay = Duration::from_millis((config.step_delay_ms as f64 * 0.6) as u64);
182 path.push(MovementStep { point: p, delay });
183 }
184 }
185
186 if let Some(last) = path.last_mut() {
188 last.point = to;
189 }
190
191 path
192}
193
194pub struct SmartMouse {
201 pos_x: AtomicU64,
202 pos_y: AtomicU64,
203 config: SmartMouseConfig,
204}
205
206impl std::fmt::Debug for SmartMouse {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 let pos = self.position();
209 f.debug_struct("SmartMouse")
210 .field("position", &pos)
211 .field("config", &self.config)
212 .finish()
213 }
214}
215
216impl SmartMouse {
217 pub fn new() -> Self {
219 Self {
220 pos_x: AtomicU64::new(0.0_f64.to_bits()),
221 pos_y: AtomicU64::new(0.0_f64.to_bits()),
222 config: SmartMouseConfig::default(),
223 }
224 }
225
226 pub fn with_config(config: SmartMouseConfig) -> Self {
228 Self {
229 pos_x: AtomicU64::new(0.0_f64.to_bits()),
230 pos_y: AtomicU64::new(0.0_f64.to_bits()),
231 config,
232 }
233 }
234
235 pub fn position(&self) -> Point {
237 Point {
238 x: f64::from_bits(self.pos_x.load(Ordering::Relaxed)),
239 y: f64::from_bits(self.pos_y.load(Ordering::Relaxed)),
240 }
241 }
242
243 pub fn set_position(&self, point: Point) {
245 self.pos_x.store(point.x.to_bits(), Ordering::Relaxed);
246 self.pos_y.store(point.y.to_bits(), Ordering::Relaxed);
247 }
248
249 pub fn config(&self) -> &SmartMouseConfig {
251 &self.config
252 }
253
254 pub fn path_to(&self, target: Point) -> Vec<MovementStep> {
259 let from = self.position();
260 self.set_position(target);
261 generate_path(from, target, &self.config)
262 }
263}
264
265impl Default for SmartMouse {
266 fn default() -> Self {
267 Self::new()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_cubic_bezier_endpoints() {
277 let p0 = Point::new(0.0, 0.0);
278 let p1 = Point::new(25.0, 50.0);
279 let p2 = Point::new(75.0, 50.0);
280 let p3 = Point::new(100.0, 100.0);
281
282 let start = cubic_bezier(p0, p1, p2, p3, 0.0);
283 assert!((start.x - p0.x).abs() < 1e-10);
284 assert!((start.y - p0.y).abs() < 1e-10);
285
286 let end = cubic_bezier(p0, p1, p2, p3, 1.0);
287 assert!((end.x - p3.x).abs() < 1e-10);
288 assert!((end.y - p3.y).abs() < 1e-10);
289 }
290
291 #[test]
292 fn test_cubic_bezier_midpoint() {
293 let p0 = Point::new(0.0, 0.0);
295 let p1 = Point::new(33.3, 33.3);
296 let p2 = Point::new(66.6, 66.6);
297 let p3 = Point::new(100.0, 100.0);
298
299 let mid = cubic_bezier(p0, p1, p2, p3, 0.5);
300 assert!((mid.x - 50.0).abs() < 1.0);
302 assert!((mid.y - 50.0).abs() < 1.0);
303 }
304
305 #[test]
306 fn test_ease_in_out_boundaries() {
307 assert!((ease_in_out(0.0)).abs() < 1e-10);
308 assert!((ease_in_out(1.0) - 1.0).abs() < 1e-10);
309 }
310
311 #[test]
312 fn test_ease_in_out_midpoint() {
313 let mid = ease_in_out(0.5);
314 assert!((mid - 0.5).abs() < 1e-10);
315 }
316
317 #[test]
318 fn test_ease_in_out_monotonic() {
319 let mut prev = 0.0;
320 for i in 1..=100 {
321 let t = i as f64 / 100.0;
322 let val = ease_in_out(t);
323 assert!(
324 val >= prev,
325 "ease_in_out should be monotonically increasing"
326 );
327 prev = val;
328 }
329 }
330
331 #[test]
332 fn test_generate_path_ends_at_target() {
333 let from = Point::new(10.0, 20.0);
334 let to = Point::new(500.0, 300.0);
335 let config = SmartMouseConfig::default();
336
337 let path = generate_path(from, to, &config);
338
339 assert!(!path.is_empty());
340 let last = &path.last().unwrap().point;
341 assert!(
342 (last.x - to.x).abs() < 1e-10 && (last.y - to.y).abs() < 1e-10,
343 "path must end exactly at target, got ({}, {})",
344 last.x,
345 last.y
346 );
347 }
348
349 #[test]
350 fn test_generate_path_short_distance() {
351 let from = Point::new(100.0, 100.0);
352 let to = Point::new(100.5, 100.5);
353 let config = SmartMouseConfig::default();
354
355 let path = generate_path(from, to, &config);
356
357 assert_eq!(
358 path.len(),
359 1,
360 "very short moves should produce a single step"
361 );
362 assert!((path[0].point.x - to.x).abs() < 1e-10);
363 assert!((path[0].point.y - to.y).abs() < 1e-10);
364 }
365
366 #[test]
367 fn test_generate_path_no_overshoot() {
368 let from = Point::new(0.0, 0.0);
369 let to = Point::new(200.0, 200.0);
370 let config = SmartMouseConfig {
371 overshoot: 0.0,
372 ..Default::default()
373 };
374
375 let path = generate_path(from, to, &config);
376 assert_eq!(path.len(), config.steps);
377 }
378
379 #[test]
380 fn test_generate_path_no_jitter() {
381 let from = Point::new(0.0, 0.0);
382 let to = Point::new(200.0, 200.0);
383 let config = SmartMouseConfig {
384 jitter: 0.0,
385 overshoot: 0.0,
386 easing: false,
387 ..Default::default()
388 };
389
390 let path = generate_path(from, to, &config);
394 assert!(!path.is_empty());
395 let last = &path.last().unwrap().point;
396 assert!((last.x - to.x).abs() < 1e-10);
397 assert!((last.y - to.y).abs() < 1e-10);
398 }
399
400 #[test]
401 fn test_generate_path_step_count_with_overshoot() {
402 let from = Point::new(0.0, 0.0);
403 let to = Point::new(500.0, 500.0);
404 let config = SmartMouseConfig {
405 steps: 30,
406 overshoot: 0.2,
407 ..Default::default()
408 };
409
410 let path = generate_path(from, to, &config);
411 assert!(path.len() >= config.steps);
413 }
414
415 #[test]
416 fn test_generate_path_no_huge_jumps() {
417 let from = Point::new(0.0, 0.0);
418 let to = Point::new(300.0, 300.0);
419 let config = SmartMouseConfig {
420 steps: 50,
421 overshoot: 0.0,
422 jitter: 0.0,
423 ..Default::default()
424 };
425
426 let path = generate_path(from, to, &config);
427
428 let mut prev = from;
429 let max_distance = (300.0_f64 * 300.0 + 300.0 * 300.0).sqrt(); for step in &path {
432 let dx = step.point.x - prev.x;
433 let dy = step.point.y - prev.y;
434 let step_dist = (dx * dx + dy * dy).sqrt();
435 assert!(
437 step_dist < max_distance * 0.6,
438 "step jumped {} pixels (max total: {})",
439 step_dist,
440 max_distance
441 );
442 prev = step.point;
443 }
444 }
445
446 #[test]
447 fn test_smart_mouse_position_tracking() {
448 let mouse = SmartMouse::new();
449
450 assert_eq!(mouse.position(), Point::new(0.0, 0.0));
451
452 mouse.set_position(Point::new(100.0, 200.0));
453 assert_eq!(mouse.position(), Point::new(100.0, 200.0));
454 }
455
456 #[test]
457 fn test_smart_mouse_path_to_updates_position() {
458 let mouse = SmartMouse::new();
459 let target = Point::new(500.0, 300.0);
460
461 let path = mouse.path_to(target);
462 assert!(!path.is_empty());
463
464 assert_eq!(mouse.position(), target);
466 }
467
468 #[test]
469 fn test_smart_mouse_consecutive_paths() {
470 let mouse = SmartMouse::with_config(SmartMouseConfig {
471 overshoot: 0.0,
472 jitter: 0.0,
473 ..Default::default()
474 });
475
476 let target1 = Point::new(100.0, 100.0);
477 let path1 = mouse.path_to(target1);
478 assert!(!path1.is_empty());
479 assert_eq!(mouse.position(), target1);
480
481 let target2 = Point::new(400.0, 300.0);
482 let _path2 = mouse.path_to(target2);
483 assert_eq!(mouse.position(), target2);
484 }
485
486 #[test]
487 fn test_smart_mouse_same_position_no_move() {
488 let mouse = SmartMouse::new();
489 mouse.set_position(Point::new(100.0, 100.0));
490
491 let path = mouse.path_to(Point::new(100.0, 100.0));
492 assert_eq!(path.len(), 1);
494 }
495
496 #[test]
497 fn test_smart_mouse_custom_config() {
498 let config = SmartMouseConfig {
499 steps: 10,
500 overshoot: 0.0,
501 jitter: 0.0,
502 step_delay_ms: 16,
503 easing: false,
504 };
505
506 let mouse = SmartMouse::with_config(config.clone());
507 let path = mouse.path_to(Point::new(200.0, 200.0));
508
509 assert_eq!(path.len(), config.steps);
510 }
511
512 #[test]
513 fn test_movement_delays_are_reasonable() {
514 let config = SmartMouseConfig {
515 step_delay_ms: 10,
516 ..Default::default()
517 };
518
519 let path = generate_path(Point::new(0.0, 0.0), Point::new(500.0, 500.0), &config);
520
521 for step in &path {
522 assert!(
524 step.delay.as_millis() <= 30,
525 "delay too large: {:?}",
526 step.delay
527 );
528 }
529 }
530
531 #[test]
532 fn test_default_config() {
533 let config = SmartMouseConfig::default();
534 assert_eq!(config.steps, 25);
535 assert!((config.overshoot - 0.15).abs() < 1e-10);
536 assert!((config.jitter - 1.5).abs() < 1e-10);
537 assert_eq!(config.step_delay_ms, 8);
538 assert!(config.easing);
539 }
540}