Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions crates/rendering/src/cursor_interpolation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const SHAKE_DETECTION_WINDOW_MS: f64 = 100.0;
const DECIMATE_FPS: f64 = 60.0;
const DECIMATE_MIN_DIST_UV: f64 = 1.0 / 1920.0;
const SIMULATION_STEP_MS: f64 = 1000.0 / 60.0;
const SPRING_SETTLE_EXTRA_MS: f64 = 300.0;
const SPRING_SETTLE_TIMEOUT_MS: f64 = 5000.0;
/// Per-step one-pole coefficient for adapting the phase lead when the active
/// spring profile changes; ~130ms time constant at the 60Hz simulation step,
/// so profile switches ease in without popping the target.
Expand Down Expand Up @@ -485,9 +485,9 @@ fn build_smoothed_timeline(
sim.set_target_position(start_pos);

let end_time_ms = moves.last().unwrap().time_ms;
let settle_end = end_time_ms + SPRING_SETTLE_EXTRA_MS;
let settle_deadline = end_time_ms + SPRING_SETTLE_TIMEOUT_MS;

let capacity = ((settle_end / SIMULATION_STEP_MS).ceil() as usize) + 2;
let capacity = ((settle_deadline / SIMULATION_STEP_MS).ceil() as usize) + 2;
let mut events = Vec::with_capacity(capacity);
let mut target_hint: usize = 0;
let mut cid_hint: usize = 0;
Expand All @@ -502,7 +502,7 @@ fn build_smoothed_timeline(
let mut t_ms = SIMULATION_STEP_MS;
let mut lead_ms = spring_lag_ms(&smoothing_config);

while t_ms <= settle_end {
while t_ms <= settle_deadline {
let clamped_t = t_ms.min(end_time_ms);

context.advance_to(t_ms);
Expand Down Expand Up @@ -539,6 +539,10 @@ fn build_smoothed_timeline(
cursor_id: cid,
});

if t_ms >= end_time_ms && sim.is_at_rest() {
break;
}

t_ms += SIMULATION_STEP_MS;
}

Expand Down Expand Up @@ -938,6 +942,26 @@ mod tests {
prev = pos;
}
}

#[test]
fn smoothed_cursor_reaches_final_recorded_position() {
let cursor = CursorEvents {
moves: vec![cursor_move(0.0, 0.1, 0.2), cursor_move(1000.0, 0.9, 0.8)],
clicks: vec![],
};
let smoothing = SpringMassDamperSimulationConfig {
tension: 80.0,
mass: 2.5,
friction: 28.0,
};

let settled = interpolate_cursor(&cursor, 10.0, Some(smoothing)).unwrap();

assert!((settled.position.coord.x - 0.9).abs() < 1e-6);
assert!((settled.position.coord.y - 0.8).abs() < 1e-6);
assert_eq!(settled.velocity, XY::new(0.0, 0.0));
}

fn linear_position_at_time(moves: &[CursorMoveEvent], time_ms: f64) -> (f64, f64) {
if moves.is_empty() {
return (0.0, 0.0);
Expand Down Expand Up @@ -997,9 +1021,9 @@ mod tests {
sim.set_target_position(start_pos);

let end_time_ms = moves.last().unwrap().time_ms;
let settle_end = end_time_ms + SPRING_SETTLE_EXTRA_MS;
let settle_deadline = end_time_ms + SPRING_SETTLE_TIMEOUT_MS;

let capacity = ((settle_end / SIMULATION_STEP_MS).ceil() as usize) + 2;
let capacity = ((settle_deadline / SIMULATION_STEP_MS).ceil() as usize) + 2;
let mut events = Vec::with_capacity(capacity);
let mut target_hint: usize = 0;
let mut cid_hint: usize = 0;
Expand All @@ -1014,7 +1038,7 @@ mod tests {
let mut t_ms = SIMULATION_STEP_MS;
let mut lead_ms = spring_lag_ms(&smoothing_config);

while t_ms <= settle_end {
while t_ms <= settle_deadline {
let clamped_t = t_ms.min(end_time_ms);

context.advance_to(t_ms);
Expand Down Expand Up @@ -1047,6 +1071,10 @@ mod tests {
cursor_id: cid,
});

if t_ms >= end_time_ms && sim.is_at_rest() {
break;
}

t_ms += SIMULATION_STEP_MS;
}

Expand Down
4 changes: 4 additions & 0 deletions crates/rendering/src/spring_mass_damper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ impl SpringMassDamperSimulation {
self.target_position = target_position;
}

pub(crate) fn is_at_rest(&self) -> bool {
self.position == self.target_position && self.velocity == XY::new(0.0, 0.0)
}

fn step(&mut self, dt_ms: f32) -> SpringStep {
let key = [dt_ms, self.tension, self.mass, self.friction].map(f32::to_bits);
if let Some(cached) = self.cached_step
Expand Down
Loading