use super::*;
fn dynamics() -> LiquidDynamics {
let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
LiquidDynamics::new(runtime.handle())
}
fn settle(d: &LiquidDynamics, pos: (f32, f32), frames: usize) -> LiquidPose {
let mut pose = d.pose();
for _ in 0..frames {
pose = d.advance(pos, 1.0 / 60.0);
}
pose
}
fn cruise(d: &LiquidDynamics, speed_dp_s: f32, frames: usize) -> LiquidPose {
let dt = 1.0 / 60.0;
let mut x = 0.0;
let mut pose = d.pose();
for _ in 0..frames {
x += speed_dp_s * dt;
pose = d.advance((x, 0.0), dt);
}
pose
}
#[test]
fn constant_speed_elongates_along_axis_and_conserves_area() {
let d = dynamics();
let pose = cruise(&d, 1200.0, 40);
assert!((1.36..1.40).contains(&pose.stretch));
assert!((pose.stretch * pose.ortho - 1.0).abs() < 1e-4);
assert!((pose.axis.0 - 1.0).abs() < 1e-4);
}
#[test]
fn launch_compresses_before_cruise_stretch_wins() {
let d = dynamics();
d.advance((0.0, 0.0), 1.0 / 60.0);
let dt = 1.0 / 60.0;
let pose = d.advance((1500.0 * dt, 0.0), dt);
assert!(pose.stretch < 1.0, "launch stretch {}", pose.stretch);
assert!(pose.ortho > 1.0, "launch ortho {}", pose.ortho);
}
#[test]
fn launch_acceleration_leaves_a_persistent_trailing_material_wake() {
let d = dynamics();
let dt = 1.0 / 60.0;
d.advance((0.0, 0.0), dt);
let launch = d.advance((1200.0 * dt, 0.0), dt);
assert!(
launch.bulge_amplitude > 0.5,
"launch must displace liquid toward the trailing edge: {launch:?}"
);
assert!(
(launch.bulge_direction.abs() - std::f32::consts::PI).abs() < 0.1,
"rightward acceleration must trail to the left: {launch:?}"
);
let cruise = d.advance((2400.0 * dt, 0.0), dt);
assert!(
cruise.bulge_amplitude > 0.25,
"the material wake must survive beyond one pointer sample: {launch:?} -> {cruise:?}"
);
assert!(
(cruise.bulge_direction.abs() - std::f32::consts::PI).abs() < 0.2,
"the remembered wake cannot flip on the next sample: {cruise:?}"
);
}
#[test]
fn braking_decompresses_past_cruise_and_swells_leading_edge() {
let d = dynamics();
let cruise_pose = cruise(&d, 1200.0, 40);
let dt = 1.0 / 60.0;
let x = d.last_pos.get().unwrap().0;
let brake = d.advance((x + 300.0 * dt, 0.0), dt);
assert!(
brake.stretch > cruise_pose.stretch + 0.04,
"brake {} vs cruise {}",
brake.stretch,
cruise_pose.stretch
);
assert!(
brake.bulge_amplitude > 0.5,
"bulge {}",
brake.bulge_amplitude
);
assert!(
brake.ortho < cruise_pose.ortho,
"brake ortho {}",
brake.ortho
);
assert!(brake.bulge_direction.abs() < 1e-3);
}
#[test]
fn rest_decays_to_identity() {
let d = dynamics();
cruise(&d, 1200.0, 40);
let pose = settle(&d, d.last_pos.get().unwrap(), 60);
assert!(
(pose.stretch - 1.0).abs() < 0.02,
"stretch {}",
pose.stretch
);
assert!(pose.bulge_amplitude < 0.2);
assert!(pose.speed < 15.0);
}
#[test]
fn rest_reaches_an_exact_fixed_point_and_stops_producing_poses() {
let d = dynamics();
cruise(&d, 1200.0, 40);
let resting = d.last_pos.get().unwrap();
let mut frames_to_rest = None;
for frame in 1..=600 {
let pose = d.advance(resting, 1.0 / 60.0);
if pose == d.advance(resting, 1.0 / 60.0) {
frames_to_rest = Some(frame);
break;
}
}
let frames_to_rest = frames_to_rest.expect("a stationary lens must stop producing new poses");
assert!(
frames_to_rest <= 120,
"rest took {frames_to_rest} frames of stationary travel"
);
let pose = d.pose();
assert_eq!(pose.stretch, 1.0, "resting stretch {pose:?}");
assert_eq!(pose.ortho, 1.0, "resting ortho {pose:?}");
assert_eq!(pose.bulge_amplitude, 0.0, "resting bulge {pose:?}");
assert_eq!(pose.speed, 0.0, "resting speed {pose:?}");
for _ in 0..600 {
assert_eq!(
d.advance(resting, 1.0 / 60.0),
pose,
"a lens at rest must keep returning the identical pose"
);
}
}
#[test]
fn rest_snap_is_invisible_next_to_the_pose_it_replaces() {
let d = dynamics();
cruise(&d, 1200.0, 40);
let resting = d.last_pos.get().unwrap();
let dt = 1.0f32 / 60.0;
let undecay = 1.0 / (-dt / RELEASE_TAU).exp();
let mut previous = d.pose();
for _ in 0..600 {
let pose = d.advance(resting, dt);
if pose.stretch == 1.0 && pose.bulge_amplitude == 0.0 && pose.speed == 0.0 {
assert!(
(previous.stretch - 1.0).abs() <= REST_STRETCH * undecay,
"snap jumped stretch from {previous:?}"
);
assert!(
previous.bulge_amplitude <= REST_BULGE * undecay,
"snap jumped bulge from {previous:?}"
);
assert!(
previous.speed <= REST_SPEED * undecay,
"snap jumped speed from {previous:?}"
);
return;
}
previous = pose;
}
panic!("a stationary lens never reached rest: {previous:?}");
}
#[test]
fn cruising_never_snaps_to_rest() {
let d = dynamics();
let pose = cruise(&d, 1200.0, 40);
assert!(pose.speed > REST_SPEED, "cruise speed {pose:?}");
assert!(
(pose.stretch - 1.0).abs() > REST_STRETCH,
"cruise stretch {pose:?}"
);
let slow = dynamics();
let pose = cruise(&slow, 40.0, 40);
assert!(
pose.speed > REST_SPEED,
"a slow but real drag must keep its motion: {pose:?}"
);
}
#[test]
fn axis_follows_motion_direction_and_holds_at_rest() {
let d = dynamics();
let dt = 1.0 / 60.0;
d.advance((0.0, 0.0), dt);
let mut x = 0.0;
let mut pose = d.pose();
for _ in 0..10 {
x -= 900.0 * dt;
pose = d.advance((x, 0.0), dt);
}
assert!((pose.axis.0 + 1.0).abs() < 1e-4, "axis {:?}", pose.axis);
assert!(
pose.bulge_direction.abs() < 0.1,
"leftward launch inertia must trail toward +x: {pose:?}"
);
let held = settle(&d, (x, 0.0), 30);
assert!((held.axis.0 + 1.0).abs() < 1e-4);
}
#[test]
fn frame_rate_independent_cruise() {
let d60 = dynamics();
let d120 = dynamics();
let cruise60 = cruise(&d60, 1000.0, 30);
let dt = 1.0 / 120.0;
let mut x = 0.0;
let mut cruise120 = d120.pose();
for _ in 0..60 {
x += 1000.0 * dt;
cruise120 = d120.advance((x, 0.0), dt);
}
assert!(
(cruise60.stretch - cruise120.stretch).abs() < 0.02,
"60Hz {} vs 120Hz {}",
cruise60.stretch,
cruise120.stretch
);
}
#[test]
fn teleport_re_anchors_without_energy() {
let d = dynamics();
d.advance((0.0, 0.0), 1.0 / 60.0);
let pose = d.advance((4000.0, 0.0), 1.0 / 60.0);
assert_eq!(pose, d.pose());
assert!(
(pose.stretch - 1.0).abs() < 1e-4,
"teleport {}",
pose.stretch
);
let resumed = cruise(&d, 800.0, 30);
assert!(resumed.stretch > 1.01);
}
#[test]
fn vertical_travel_stretches_height_not_width() {
let d = dynamics();
let dt = 1.0 / 60.0;
let mut y = 0.0;
d.advance((0.0, 0.0), dt);
let mut pose = d.pose();
for _ in 0..30 {
y += 1000.0 * dt;
pose = d.advance((0.0, y), dt);
}
assert!(
(1.30..1.34).contains(&pose.stretch),
"stretch {}",
pose.stretch
);
assert!((pose.stretch * pose.ortho - 1.0).abs() < 1e-4);
}
#[test]
fn reset_forgets_motion() {
let d = dynamics();
cruise(&d, 1200.0, 40);
d.reset();
assert_eq!(d.pose().stretch, 1.0);
let pose = d.advance((500.0, 0.0), 1.0 / 60.0);
assert!((pose.stretch - 1.0).abs() < 1e-4);
}
#[test]
fn a_pose_away_from_rest_asks_for_the_next_frame_until_it_rests() {
const FRAME_NANOS: u64 = 16_666_667;
type UpdatePose = fn(&LiquidDynamics, (f32, f32)) -> LiquidPose;
let updates: [UpdatePose; 1] = [LiquidDynamics::update];
for update in updates {
let app_context = cranpose_ui::AppContext::new();
let _scope = app_context.enter_scope();
let runtime =
cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
let clock = runtime.handle();
let d = LiquidDynamics::new(runtime.handle());
let mut now = 0;
let mut x = 0.0;
for _ in 0..20 {
now += FRAME_NANOS;
x += 12.0;
clock.drain_frame_callbacks(now);
update(&d, (x, 0.0));
}
assert!(
cranpose_ui::take_render_invalidation(),
"a travelling lens must ask for the next frame"
);
let mut rested_at = None;
for frame in 1..=240 {
now += FRAME_NANOS;
clock.drain_frame_callbacks(now);
let pose = update(&d, (x, 0.0));
let asked = cranpose_ui::take_render_invalidation();
if pose.stretch == 1.0 && pose.bulge_amplitude == 0.0 && pose.speed == 0.0 {
assert!(
!asked,
"the frame that reaches rest must not ask for another"
);
rested_at = Some(frame);
break;
}
assert!(
asked,
"frame {frame}: a lens still relaxing must ask for the next frame: {pose:?}"
);
}
let rested_at = rested_at.expect("a stopped lens must come to rest");
assert!(
rested_at > 5,
"the droplet should relax over several frames, not {rested_at}"
);
for _ in 0..3 {
now += FRAME_NANOS;
clock.drain_frame_callbacks(now);
update(&d, (x, 0.0));
assert!(
!cranpose_ui::take_render_invalidation(),
"a lens at rest must not ask for frames"
);
}
}
}