1use std::ops::{Range, RangeInclusive};
11use std::time::Duration;
12
13use rand::Rng;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Step {
18 pub x: f64,
19 pub y: f64,
20 pub delay: Duration,
21}
22
23#[derive(Debug, Clone, PartialEq)]
25pub struct MouseParams {
26 pub tick: Duration,
29 pub min_steps: usize,
31 pub fitts_a_ms: f64,
33 pub fitts_b_ms: f64,
34 pub bend: Range<f64>,
37 pub bend_cap_px: f64,
38 pub overshoot_threshold_px: f64,
40 pub overshoot: Range<f64>,
42 pub overshoot_cap_px: f64,
43 pub overshoot_lateral_px: f64,
46 pub correction_bend_cap_px: f64,
47 pub detour_chance: f64,
50 pub detour_waypoints: RangeInclusive<usize>,
51 pub detour_min_px: f64,
52 pub inset: f64,
55 pub first_start_px: Range<f64>,
57 pub hover_ms: Range<u64>,
59 pub press_ms: Range<u64>,
60 pub wiggle_chance: f64,
62}
63
64impl Default for MouseParams {
65 fn default() -> Self {
66 Self {
67 tick: Duration::from_millis(8),
69 min_steps: 10,
70 fitts_a_ms: 100.0,
71 fitts_b_ms: 150.0,
72 bend: 0.05..0.25,
73 bend_cap_px: 120.0,
74 overshoot_threshold_px: 500.0,
75 overshoot: 0.03..0.08,
76 overshoot_cap_px: 60.0,
77 overshoot_lateral_px: 10.0,
78 correction_bend_cap_px: 4.0,
79 detour_chance: 0.2,
80 detour_waypoints: 1..=2,
81 detour_min_px: 50.0,
82 inset: 0.25,
83 first_start_px: 100.0..300.0,
84 hover_ms: 10..80,
85 press_ms: 80..140,
86 wiggle_chance: 0.2,
87 }
88 }
89}
90
91pub fn bounds(quad: &[f64]) -> Option<(f64, f64, f64, f64)> {
95 if quad.len() != 8 {
96 return None;
97 }
98 let xs = quad.iter().step_by(2);
99 let ys = quad.iter().skip(1).step_by(2);
100 let (x0, x1) = xs.fold((f64::MAX, f64::MIN), |(a, b), &v| (a.min(v), b.max(v)));
101 let (y0, y1) = ys.fold((f64::MAX, f64::MIN), |(a, b), &v| (a.min(v), b.max(v)));
102 (x1 - x0 >= 1.0 && y1 - y0 >= 1.0).then_some((x0, y0, x1, y1))
103}
104
105pub fn pick_point(b: (f64, f64, f64, f64), p: &MouseParams, rng: &mut impl Rng) -> (f64, f64) {
108 let (x0, y0, x1, y1) = b;
109 let at = |lo: f64, hi: f64, r: f64| lo + (hi - lo) * (p.inset + (1.0 - 2.0 * p.inset) * r);
110 let x = at(x0, x1, rng.gen());
111 let y = at(y0, y1, rng.gen());
112 (x.round(), y.round())
113}
114
115pub fn first_start(to: (f64, f64), p: &MouseParams, rng: &mut impl Rng) -> (f64, f64) {
118 let angle = rng.gen_range(0.0..std::f64::consts::TAU);
119 let dist = rng.gen_range(p.first_start_px.clone());
120 (
121 (to.0 + dist * angle.cos()).max(1.0).round(),
122 (to.1 + dist * angle.sin()).max(1.0).round(),
123 )
124}
125
126pub fn path(
132 from: (f64, f64),
133 to: (f64, f64),
134 target_width: f64,
135 p: &MouseParams,
136 rng: &mut impl Rng,
137) -> Vec<Step> {
138 if !rng.gen_bool(p.detour_chance) {
139 return direct_path(from, to, target_width, p, rng);
140 }
141 let raw = detour(from, to, target_width, p, rng);
142 drop_repeats(from, raw)
143}
144
145fn detour(
146 from: (f64, f64),
147 to: (f64, f64),
148 target_width: f64,
149 p: &MouseParams,
150 rng: &mut impl Rng,
151) -> Vec<Step> {
152 let (dx, dy) = (to.0 - from.0, to.1 - from.1);
153 let dist = dx.hypot(dy).max(p.detour_min_px);
154 let (nx, ny) = (-dy / dist, dx / dist);
155 let mut steps = Vec::new();
156 let mut at = from;
157 for _ in 0..rng.gen_range(p.detour_waypoints.clone()) {
158 let t = rng.gen_range(0.2..0.8);
159 let off = rng.gen_range(-0.8..0.8) * dist;
160 let waypoint = (
161 (from.0 + dx * t + nx * off).max(1.0).round(),
162 (from.1 + dy * t + ny * off).max(1.0).round(),
163 );
164 steps.extend(legs(at, waypoint, target_width, p, rng));
165 at = waypoint;
166 }
167 steps.extend(legs(at, to, target_width, p, rng));
168 steps
169}
170
171fn drop_repeats(from: (f64, f64), steps: Vec<Step>) -> Vec<Step> {
175 let mut out: Vec<Step> = Vec::with_capacity(steps.len());
176 let mut prev = from;
177 for st in steps {
178 if (st.x, st.y) != prev {
179 prev = (st.x, st.y);
180 out.push(st);
181 } else if let Some(last) = out.last_mut() {
182 last.delay += st.delay;
183 }
184 }
185 out
186}
187
188pub fn direct_path(
191 from: (f64, f64),
192 to: (f64, f64),
193 target_width: f64,
194 p: &MouseParams,
195 rng: &mut impl Rng,
196) -> Vec<Step> {
197 let raw = legs(from, to, target_width, p, rng);
198 drop_repeats(from, raw)
199}
200
201fn legs(
202 from: (f64, f64),
203 to: (f64, f64),
204 target_width: f64,
205 p: &MouseParams,
206 rng: &mut impl Rng,
207) -> Vec<Step> {
208 let (dx, dy) = (to.0 - from.0, to.1 - from.1);
209 let dist = dx.hypot(dy);
210 if dist <= p.overshoot_threshold_px {
211 return leg(from, to, target_width, p.bend_cap_px, p, rng);
212 }
213 let (ux, uy) = (dx / dist, dy / dist);
214 let ahead = (dist * rng.gen_range(p.overshoot.clone())).min(p.overshoot_cap_px);
215 let lateral = rng.gen_range(-p.overshoot_lateral_px..p.overshoot_lateral_px);
216 let past = (
217 (to.0 + ux * ahead - uy * lateral).max(1.0).round(),
218 (to.1 + uy * ahead + ux * lateral).max(1.0).round(),
219 );
220 let mut steps = leg(from, past, target_width, p.bend_cap_px, p, rng);
221 steps.extend(leg(
222 past,
223 to,
224 target_width,
225 p.correction_bend_cap_px,
226 p,
227 rng,
228 ));
229 steps
230}
231
232fn leg(
233 from: (f64, f64),
234 to: (f64, f64),
235 target_width: f64,
236 bend_cap: f64,
237 p: &MouseParams,
238 rng: &mut impl Rng,
239) -> Vec<Step> {
240 let (dx, dy) = (to.0 - from.0, to.1 - from.1);
241 let dist = dx.hypot(dy);
242 let total_ms = p.fitts_a_ms + p.fitts_b_ms * (dist / target_width.max(1.0) + 1.0).log2();
243 let tick_ms = p.tick.as_secs_f64() * 1000.0;
244 let steps = ((total_ms / tick_ms).round() as usize).max(p.min_steps.max(1));
245
246 let (nx, ny) = if dist > 0.0 {
249 (-dy / dist, dx / dist)
250 } else {
251 (0.0, 0.0)
252 };
253 let side = if rng.gen_bool(0.5) { 1.0 } else { -1.0 };
254 let bend =
255 |r: f64| side * (dist * (p.bend.start + (p.bend.end - p.bend.start) * r)).min(bend_cap);
256 let (b1, b2) = (bend(rng.gen()), bend(rng.gen()));
257 let c1 = (from.0 + dx / 3.0 + nx * b1, from.1 + dy / 3.0 + ny * b1);
258 let c2 = (
259 from.0 + 2.0 * dx / 3.0 + nx * b2,
260 from.1 + 2.0 * dy / 3.0 + ny * b2,
261 );
262
263 (1..=steps)
264 .map(|i| {
265 let t = i as f64 / steps as f64;
266 let t = t * t * (3.0 - 2.0 * t);
268 let (x, y) = if i == steps {
269 to
270 } else {
271 bezier(from, c1, c2, to, t)
272 };
273 Step {
274 x: x.round(),
275 y: y.round(),
276 delay: p.tick,
277 }
278 })
279 .collect()
280}
281
282fn bezier(p0: (f64, f64), p1: (f64, f64), p2: (f64, f64), p3: (f64, f64), t: f64) -> (f64, f64) {
283 let u = 1.0 - t;
284 let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
285 (
286 a * p0.0 + b * p1.0 + c * p2.0 + d * p3.0,
287 a * p0.1 + b * p1.1 + c * p2.1 + d * p3.1,
288 )
289}
290
291pub fn hover_pause(p: &MouseParams, rng: &mut impl Rng) -> Duration {
293 Duration::from_millis(rng.gen_range(p.hover_ms.clone()))
294}
295
296pub fn press_hold(p: &MouseParams, rng: &mut impl Rng) -> Duration {
298 Duration::from_millis(rng.gen_range(p.press_ms.clone()))
299}
300
301pub fn press_wiggle(p: &MouseParams, rng: &mut impl Rng) -> Vec<(f64, f64)> {
304 if !rng.gen_bool(p.wiggle_chance) {
305 return Vec::new();
306 }
307 (0..rng.gen_range(1..=2))
308 .map(|_| loop {
309 let d = (
310 f64::from(rng.gen_range(-1..=1)),
311 f64::from(rng.gen_range(-1..=1)),
312 );
313 if d != (0.0, 0.0) {
314 break d;
315 }
316 })
317 .collect()
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use rand::rngs::StdRng;
324 use rand::SeedableRng;
325
326 fn rng(seed: u64) -> StdRng {
327 StdRng::seed_from_u64(seed)
328 }
329
330 fn params() -> MouseParams {
331 MouseParams::default()
332 }
333
334 #[test]
335 fn path_ends_on_target_and_excludes_start() {
336 for seed in 0..50 {
337 let p = path(
338 (10.0, 10.0),
339 (600.0, 420.0),
340 80.0,
341 ¶ms(),
342 &mut rng(seed),
343 );
344 let last = p.last().unwrap();
345 assert_eq!((last.x, last.y), (600.0, 420.0), "seed {seed}");
346 assert_ne!((p[0].x, p[0].y), (10.0, 10.0), "seed {seed}");
347 }
348 }
349
350 #[test]
351 fn projection_on_line_grows_monotonically() {
352 for seed in 0..50 {
353 let (from, to) = ((50.0, 400.0), (380.0, 190.0));
354 let (dx, dy) = (to.0 - from.0, to.1 - from.1);
355 let proj: Vec<f64> = direct_path(from, to, 60.0, ¶ms(), &mut rng(seed))
356 .iter()
357 .map(|s| (s.x - from.0) * dx + (s.y - from.1) * dy)
358 .collect();
359 let slack = dx.abs() + dy.abs();
361 assert!(
362 proj.windows(2).all(|w| w[1] >= w[0] - slack),
363 "seed {seed}: {proj:?}"
364 );
365 }
366 }
367
368 #[test]
369 fn every_point_is_on_one_side_of_the_line() {
370 for seed in 0..50 {
371 let (from, to) = ((0.0, 0.0), (300.0, 200.0));
372 let side: Vec<f64> = direct_path(from, to, 50.0, ¶ms(), &mut rng(seed))
373 .iter()
374 .map(|s| (to.0 - from.0) * (s.y - from.1) - (to.1 - from.1) * (s.x - from.0))
375 .filter(|c| c.abs() > 1000.0) .collect();
377 assert!(
378 side.iter().all(|c| *c > 0.0) || side.iter().all(|c| *c < 0.0),
379 "seed {seed}: path crosses the line {side:?}"
380 );
381 assert!(!side.is_empty(), "seed {seed}: perfectly straight path");
382 }
383 }
384
385 #[test]
386 fn farther_targets_take_more_points_and_time() {
387 let near = direct_path((0.0, 0.0), (120.0, 0.0), 40.0, ¶ms(), &mut rng(1));
389 let far = direct_path((0.0, 0.0), (480.0, 0.0), 40.0, ¶ms(), &mut rng(1));
390 assert!(far.len() > near.len(), "{} vs {}", far.len(), near.len());
391 let total = |p: &[Step]| p.iter().map(|s| s.delay).sum::<Duration>();
392 assert!(total(&far) > total(&near));
393 assert!(near.len() >= params().min_steps);
394 }
395
396 fn past_target(p: &[Step], from: (f64, f64), to: (f64, f64)) -> f64 {
399 let (dx, dy) = (to.0 - from.0, to.1 - from.1);
400 let d = dx.hypot(dy);
401 p.iter()
402 .map(|s| ((s.x - to.0) * dx + (s.y - to.1) * dy) / d)
403 .fold(f64::MIN, f64::max)
404 }
405
406 #[test]
407 fn long_moves_overshoot_then_correct_short_moves_do_not() {
408 let (from, to) = ((50.0, 50.0), (900.0, 700.0));
409 let cap = params().overshoot_cap_px;
410 for seed in 0..40 {
411 let p = direct_path(from, to, 100.0, ¶ms(), &mut rng(seed));
412 let last = p.last().unwrap();
413 assert_eq!((last.x, last.y), to, "seed {seed}");
414 let past = past_target(&p, from, to);
415 assert!(past > 5.0 && past <= cap + 2.0, "seed {seed}: {past}px");
416 }
417 let near_from = (600.0, 400.0);
419 for seed in 0..40 {
420 let p = direct_path(near_from, to, 100.0, ¶ms(), &mut rng(seed));
421 let past = past_target(&p, near_from, to);
422 assert!(past <= 1.0, "seed {seed}: short move overshot {past}px");
423 }
424 }
425
426 #[test]
427 fn path_sometimes_wanders_and_always_ends_on_target() {
428 let (from, to): ((f64, f64), (f64, f64)) = ((100.0, 100.0), (500.0, 300.0));
429 let straight = (to.0 - from.0).hypot(to.1 - from.1);
430 let mut wandering = 0;
431 for seed in 0..400 {
432 let p = path(from, to, 60.0, ¶ms(), &mut rng(seed));
433 let last = p.last().unwrap();
434 assert_eq!((last.x, last.y), to, "seed {seed}");
435 let mut len = 0.0;
436 let mut prev = from;
437 for s in &p {
438 len += (s.x - prev.0).hypot(s.y - prev.1);
439 prev = (s.x, s.y);
440 }
441 if len / straight > 1.5 {
442 wandering += 1;
443 }
444 }
445 assert!(
447 (30..=110).contains(&wandering),
448 "{wandering}/400 paths wander"
449 );
450 }
451
452 #[test]
453 fn no_step_repeats_the_previous_position() {
454 for seed in 0..200 {
455 let from = (10.0, 10.0);
456 let p = path(from, (300.0, 220.0), 40.0, ¶ms(), &mut rng(seed));
457 let mut prev = from;
458 for s in &p {
459 assert_ne!((s.x, s.y), prev, "seed {seed}");
460 prev = (s.x, s.y);
461 }
462 }
463 }
464
465 #[test]
466 fn points_are_spaced_by_the_polling_tick() {
467 let p = MouseParams {
468 tick: Duration::from_micros(16_667),
469 ..params()
470 };
471 for seed in 0..20 {
472 let steps = path((40.0, 40.0), (900.0, 500.0), 60.0, &p, &mut rng(seed));
473 assert!(
475 steps.iter().all(|s| {
476 let n = s.delay.as_nanos() / p.tick.as_nanos();
477 n >= 1 && s.delay == p.tick * n as u32
478 }),
479 "seed {seed}"
480 );
481 assert!(steps.iter().filter(|s| s.delay == p.tick).count() * 10 > steps.len() * 8);
482 }
483 }
484
485 #[test]
486 fn click_point_stays_inside_inset_and_varies() {
487 let b = bounds(&[300.0, 200.0, 400.0, 200.0, 400.0, 240.0, 300.0, 240.0]).unwrap();
488 let mut r = rng(7);
489 let pts: Vec<(f64, f64)> = (0..100).map(|_| pick_point(b, ¶ms(), &mut r)).collect();
490 assert!(pts
491 .iter()
492 .all(|&(x, y)| (325.0..=375.0).contains(&x) && (210.0..=230.0).contains(&y)));
493 let distinct: std::collections::HashSet<(i64, i64)> =
494 pts.iter().map(|&(x, y)| (x as i64, y as i64)).collect();
495 assert!(
496 distinct.len() > 20,
497 "click points repeat: {}",
498 distinct.len()
499 );
500 }
501
502 #[test]
503 fn empty_or_malformed_box_has_no_click_point() {
504 assert_eq!(bounds(&[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0]), None);
505 assert_eq!(bounds(&[1.0, 2.0]), None);
506 }
507
508 #[test]
509 fn first_start_is_within_configured_distance() {
510 let p = params();
511 let mut r = rng(3);
512 for _ in 0..200 {
513 let s = first_start((500.0, 400.0), &p, &mut r);
514 let d = (s.0 - 500.0).hypot(s.1 - 400.0);
515 assert!(d >= p.first_start_px.start - 1.0 && d <= p.first_start_px.end + 1.0);
516 }
517 }
518
519 #[test]
520 fn hover_press_and_wiggle_follow_params() {
521 let p = params();
522 let mut r = rng(4);
523 for _ in 0..500 {
524 assert!(p
525 .hover_ms
526 .contains(&(hover_pause(&p, &mut r).as_millis() as u64)));
527 assert!(p
528 .press_ms
529 .contains(&(press_hold(&p, &mut r).as_millis() as u64)));
530 }
531 let wiggles: Vec<Vec<(f64, f64)>> = (0..1000).map(|_| press_wiggle(&p, &mut r)).collect();
532 let with = wiggles.iter().filter(|w| !w.is_empty()).count();
533 assert!((130..=270).contains(&with), "{with}/1000 presses drift");
534 assert!(wiggles
535 .iter()
536 .flatten()
537 .all(|&(dx, dy)| dx.abs() <= 1.0 && dy.abs() <= 1.0 && (dx, dy) != (0.0, 0.0)));
538 }
539}