#![allow(dead_code)]
use rand::seq::SliceRandom;
use rand::Rng;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Step {
pub dx: f32,
pub dy: f32,
pub dt_ms: u32,
}
#[derive(Debug, Clone)]
pub struct Trace {
pub steps: Vec<Step>,
}
impl Trace {
pub fn duration_ms(&self) -> u32 {
self.steps.iter().map(|s| s.dt_ms).sum()
}
pub fn arc_length(&self) -> f32 {
let mut total = 0.0f32;
for s in &self.steps {
total += (s.dx * s.dx + s.dy * s.dy).sqrt();
}
total
}
pub fn cumulative(&self) -> Vec<(f32, f32, u32)> {
let mut out = Vec::with_capacity(self.steps.len() + 1);
let mut x = 0.0f32;
let mut y = 0.0f32;
let mut t = 0u32;
out.push((x, y, t));
for s in &self.steps {
x += s.dx;
y += s.dy;
t = t.saturating_add(s.dt_ms);
out.push((x, y, t));
}
out
}
}
pub struct MouseSampler {
corpus: Vec<Trace>,
}
impl MouseSampler {
pub fn new() -> Self {
Self {
corpus: bundled_corpus(),
}
}
pub fn with_extra_traces(mut self, mut extra: Vec<Trace>) -> Self {
self.corpus.append(&mut extra);
self
}
pub fn sample(&self, x0: f32, y0: f32, x1: f32, y1: f32) -> Trace {
let mut rng = rand::rngs::StdRng::from_entropy_via_thread_local();
let chosen = self.corpus.choose(&mut rng).cloned().unwrap_or_else(|| Trace {
steps: vec![Step {
dx: x1 - x0,
dy: y1 - y0,
dt_ms: 250,
}],
});
affine_transform_with_jitter(&chosen, (x0, y0), (x1, y1), &mut rng)
}
pub fn corpus(&self) -> &[Trace] {
&self.corpus
}
}
impl Default for MouseSampler {
fn default() -> Self {
Self::new()
}
}
trait FromEntropyViaThreadLocal {
fn from_entropy_via_thread_local() -> Self;
}
impl FromEntropyViaThreadLocal for rand::rngs::StdRng {
fn from_entropy_via_thread_local() -> Self {
use rand::SeedableRng;
rand::rngs::StdRng::from_seed(rand::random())
}
}
fn affine_transform_with_jitter(
trace: &Trace,
start: (f32, f32),
end: (f32, f32),
rng: &mut impl Rng,
) -> Trace {
let cumul = trace.cumulative();
let final_xy = cumul.last().copied().unwrap_or((1.0, 1.0, 0));
let scale_x = if final_xy.0.abs() < 1e-6 {
end.0 - start.0
} else {
(end.0 - start.0) / final_xy.0
};
let scale_y = if final_xy.1.abs() < 1e-6 {
end.1 - start.1
} else {
(end.1 - start.1) / final_xy.1
};
let mut steps = Vec::with_capacity(trace.steps.len());
for s in &trace.steps {
let dx = s.dx * scale_x + rng.gen_range(-2.0..=2.0);
let dy = s.dy * scale_y + rng.gen_range(-2.0..=2.0);
let dt_jitter = rng.gen_range(-5i32..=5);
let dt_ms = (s.dt_ms as i32 + dt_jitter).max(1) as u32;
steps.push(Step { dx, dy, dt_ms });
}
Trace { steps }
}
fn bundled_corpus() -> Vec<Trace> {
vec![
Trace {
steps: build_trace(&[
(0.10, 0.05, 12), (0.18, 0.10, 14), (0.13, 0.13, 11),
(0.12, 0.16, 13), (0.11, 0.18, 15), (0.10, 0.16, 16),
(0.09, 0.13, 18), (0.08, 0.06, 22), (0.06, 0.02, 18),
(0.03, 0.01, 15),
]),
},
Trace {
steps: build_trace(&[
(0.04, 0.02, 22), (0.06, 0.05, 20), (0.08, 0.08, 19),
(0.10, 0.10, 18), (0.11, 0.11, 18), (0.12, 0.13, 17),
(0.13, 0.14, 16), (0.13, 0.15, 16), (0.10, 0.13, 17),
(0.07, 0.07, 18), (0.04, 0.02, 22), (0.02, 0.00, 25),
]),
},
Trace {
steps: build_trace(&[
(0.15, 0.12, 12), (0.20, 0.18, 12), (0.25, 0.22, 11),
(0.20, 0.18, 13), (0.13, 0.12, 16), (0.07, 0.10, 18),
(0.00, 0.08, 19),
(-0.02, 0.05, 18), (0.02, 0.00, 17), (-0.00, -0.05, 18),
]),
},
Trace {
steps: build_trace(&[
(0.05, 0.05, 14), (0.10, 0.08, 13), (0.12, 0.11, 12),
(0.13, 0.12, 13),
(0.02, -0.01, 11), (-0.02, 0.01, 12), (0.03, 0.00, 11),
(-0.01, 0.02, 12),
(0.13, 0.13, 14), (0.11, 0.13, 15), (0.10, 0.14, 16),
(0.08, 0.10, 17), (0.06, 0.07, 18), (0.10, 0.05, 18),
]),
},
Trace {
steps: build_trace(&[
(0.18, 0.18, 18), (0.15, 0.15, 19), (0.16, 0.15, 20),
(0.14, 0.14, 21), (0.12, 0.12, 22), (0.10, 0.10, 23),
(0.08, 0.08, 23), (0.07, 0.08, 24),
]),
},
Trace {
steps: build_trace(&[
(0.02, 0.02, 30), (0.03, 0.03, 35), (0.02, 0.02, 40),
(0.10, 0.08, 12), (0.15, 0.13, 11), (0.18, 0.16, 11),
(0.18, 0.18, 11), (0.15, 0.16, 12), (0.10, 0.13, 14),
(0.07, 0.09, 16),
]),
},
Trace {
steps: build_trace(&[
(0.12, 0.05, 12), (0.15, 0.08, 12), (0.16, 0.12, 13),
(0.15, 0.15, 14), (0.13, 0.16, 15), (0.10, 0.16, 16),
(0.08, 0.13, 17), (0.06, 0.10, 18), (0.05, 0.05, 18),
]),
},
Trace {
steps: build_trace(&[
(0.06, 0.04, 16), (0.10, 0.07, 14), (0.13, 0.10, 13),
(0.01, 0.00, 28),
(0.12, 0.13, 13), (0.13, 0.15, 14), (0.13, 0.15, 15),
(-0.03, -0.02, 13),
(0.10, 0.13, 14), (0.10, 0.10, 16), (0.07, 0.07, 18),
(0.08, 0.08, 18),
]),
},
]
}
fn build_trace(steps: &[(f32, f32, u32)]) -> Vec<Step> {
let mut acc_x = 0.0f32;
let mut acc_y = 0.0f32;
let mut out = Vec::with_capacity(steps.len());
for (dx, dy, dt) in steps {
acc_x += dx;
acc_y += dy;
out.push(Step { dx: *dx, dy: *dy, dt_ms: *dt });
}
if let Some(last) = out.last_mut() {
last.dx += 1.0 - acc_x;
last.dy += 1.0 - acc_y;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_corpus_has_at_least_eight_traces() {
assert!(bundled_corpus().len() >= 8);
}
#[test]
fn every_bundled_trace_terminates_at_unit_square() {
for (i, t) in bundled_corpus().iter().enumerate() {
let cumul = t.cumulative();
let (x, y, _) = *cumul.last().unwrap();
assert!(
(x - 1.0).abs() < 1e-3,
"trace #{i} ends at x={x}, expected 1.0"
);
assert!(
(y - 1.0).abs() < 1e-3,
"trace #{i} ends at y={y}, expected 1.0"
);
}
}
#[test]
fn every_bundled_trace_has_realistic_step_count() {
for (i, t) in bundled_corpus().iter().enumerate() {
assert!(
(8..=100).contains(&t.steps.len()),
"trace #{i} has {} steps (expect 8..=100)",
t.steps.len()
);
}
}
#[test]
fn every_bundled_trace_has_realistic_inter_step_delay() {
for (i, t) in bundled_corpus().iter().enumerate() {
for (j, s) in t.steps.iter().enumerate() {
assert!(
(5..=50).contains(&s.dt_ms),
"trace #{i} step #{j} dt_ms = {} (expect 5..=50)",
s.dt_ms
);
}
}
}
#[test]
fn every_bundled_trace_has_natural_duration() {
for (i, t) in bundled_corpus().iter().enumerate() {
let d = t.duration_ms();
assert!(
(100..=1800).contains(&d),
"trace #{i} duration {d}ms (expect 100..=1800)"
);
}
}
#[test]
fn sampler_returns_a_trace_with_at_least_one_step() {
let s = MouseSampler::new();
let t = s.sample(100.0, 200.0, 400.0, 350.0);
assert!(!t.steps.is_empty());
}
#[test]
fn sampler_lands_close_to_requested_end_coordinate() {
let sampler = MouseSampler::new();
for _ in 0..20 {
let t = sampler.sample(50.0, 50.0, 500.0, 400.0);
let cumul = t.cumulative();
let (end_x, end_y, _) = *cumul.last().unwrap();
let actual_end_x = 50.0 + end_x;
let actual_end_y = 50.0 + end_y;
assert!(
(actual_end_x - 500.0).abs() < 100.0,
"end_x = {actual_end_x}, expected ~500"
);
assert!(
(actual_end_y - 400.0).abs() < 100.0,
"end_y = {actual_end_y}, expected ~400"
);
}
}
#[test]
fn sampler_produces_distinct_paths_across_calls() {
let sampler = MouseSampler::new();
let a = sampler.sample(0.0, 0.0, 100.0, 100.0);
let b = sampler.sample(0.0, 0.0, 100.0, 100.0);
assert!(
a.steps != b.steps,
"two consecutive samples produced identical paths — sampler RNG broken?"
);
}
#[test]
fn extra_traces_are_added_to_corpus() {
let s = MouseSampler::new().with_extra_traces(vec![Trace {
steps: vec![Step { dx: 1.0, dy: 1.0, dt_ms: 100 }],
}]);
assert!(s.corpus().len() >= 9);
}
#[test]
fn cumulative_starts_at_origin() {
let t = Trace {
steps: vec![Step { dx: 0.5, dy: 0.5, dt_ms: 100 }],
};
let cumul = t.cumulative();
assert_eq!(cumul[0], (0.0, 0.0, 0));
}
#[test]
fn arc_length_sums_step_distances() {
let t = Trace {
steps: vec![
Step { dx: 3.0, dy: 4.0, dt_ms: 10 },
Step { dx: -3.0, dy: -4.0, dt_ms: 10 },
],
};
assert!((t.arc_length() - 10.0).abs() < 1e-5);
}
#[test]
fn duration_ms_is_zero_for_empty_trace() {
let t = Trace { steps: vec![] };
assert_eq!(t.duration_ms(), 0);
}
#[test]
fn affine_transform_handles_zero_length_normalised_trace_gracefully() {
let trace = Trace {
steps: vec![Step { dx: 0.5, dy: 0.5, dt_ms: 10 }, Step { dx: -0.5, dy: -0.5, dt_ms: 10 }],
};
let mut rng = rand::rngs::StdRng::from_entropy_via_thread_local();
let out = affine_transform_with_jitter(&trace, (0.0, 0.0), (100.0, 100.0), &mut rng);
assert_eq!(out.steps.len(), 2);
}
}