cranpose-liquid 0.9.18

Glass controls, themes and motion for Cranpose apps
Documentation
use super::*;

#[test]
fn control_reversals_track_native_stretch_compression_and_stopped_rebound() {
    for (name, kind) in [
        ("slider", LensShapeKind::Thumb),
        ("toggle", LensShapeKind::Thumb),
        ("segmented", LensShapeKind::Segment),
    ] {
        let runtime =
            cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
        let shape = LensShapeMotion::new(runtime.handle(), kind);
        let mut squared_error = [0.0; 2];
        let mut counts = 0;
        for row in include_str!("../../../tests/fixtures/native_control_shape.csv")
            .lines()
            .skip(1)
        {
            let fields = row.split(',').collect::<Vec<_>>();
            if fields[0] != name {
                continue;
            }
            let values = fields[1..]
                .iter()
                .map(|value| value.parse::<f64>().expect("native geometry"))
                .collect::<Vec<_>>();
            runtime
                .handle()
                .drain_frame_callbacks(1_000_000_000 + (values[0].max(0.0) * 1e9) as u64);
            let actual = shape.projection(values[1] as f32);
            for (index, value) in [actual.0, actual.1].into_iter().enumerate() {
                squared_error[index] += (f64::from(value) - values[index + 2]).powi(2);
            }
            counts += 1;
        }
        assert!(counts > 200);
        for error in squared_error {
            let rms = (error / f64::from(counts)).sqrt();
            assert!(rms < 0.03, "{name} native strain RMS: {rms}");
        }
    }
}

#[test]
fn reversing_motion_preserves_the_stretch_and_rebound() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let forward = LensShapeMotion::new(runtime.handle(), LensShapeKind::Tab);
    let reverse = LensShapeMotion::new(runtime.handle(), LensShapeKind::Tab);
    let mut greatest = 0.0f32;
    for frame in 0..240 {
        runtime
            .handle()
            .drain_frame_callbacks(1_000_000_000 + frame * 8_333_333);
        let position = ((frame as f32 - 96.0) * 8.0).clamp(0.0, 285.5);
        let a = forward.sample(position);
        let b = reverse.sample(285.5 - position);
        greatest = greatest.max(a.width.abs());
        assert!(
            (a.width - b.width).abs() < 0.00001 && (a.height - b.height).abs() < 0.00001,
            "direction changed strain at frame {frame}: {a:?}, {b:?}"
        );
    }
    assert!(greatest > 0.05);
}

#[test]
fn contact_light_tracks_native_onset_and_keeps_the_dimmed_release() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let motion = TabContactMotion::new(runtime.handle());
    let mut route = usize::MAX;
    let mut error = 0.0;
    let mut samples = 0;
    for row in include_str!("../../../tests/fixtures/native_tab_glow.csv")
        .lines()
        .skip(1)
    {
        let fields = row
            .split(',')
            .map(|v| v.parse::<f64>().unwrap())
            .collect::<Vec<_>>();
        let next = fields[0] as usize;
        let origin = (next as u64 + 1) * 10_000_000_000;
        if next != route {
            motion.glow.borrow_mut().snapTo(0.0);
            motion.pressed(true, Some(origin));
            route = next;
        }
        runtime
            .handle()
            .drain_frame_callbacks(origin + (fields[1] * 1e9) as u64);
        error += (f64::from(motion.glow_state().get()) - fields[2]).powi(2);
        samples += 1;
    }
    assert!(samples >= 50);
    assert!((error / f64::from(samples)).sqrt() < 0.03);
    motion.crossed_tab();
    for frame in 1..=120 {
        runtime
            .handle()
            .drain_frame_callbacks(40_250_000_000 + frame * 16_666_667);
    }
    assert_eq!(motion.local_glow_factor_state().get(), 0.5);
    motion.pressed(false, Some(42_250_000_000));
    runtime.handle().drain_frame_callbacks(42_350_000_000);
    assert!((motion.glow_state().get() - 0.64).abs() < 0.03);
    assert_eq!(motion.local_glow_factor_state().get(), 0.5);
}

#[test]
fn contact_clock_integrates_from_input_and_keeps_release_velocity() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let motion = TabContactMotion::new(runtime.handle());
    runtime.handle().drain_frame_callbacks(1_000_000_000);
    motion.pressed(true, Some(1_000_000_000));
    runtime.handle().drain_frame_callbacks(1_050_000_000);
    let before = motion.lens_state().value();
    assert!(before > 0.34 && before < 0.35, "contact sample: {before}");
    assert!(motion.bar_state().value() > 0.1);
    let velocity = motion.lens.borrow().velocity();
    motion.pressed(false, Some(1_050_000_000));
    assert_eq!(motion.lens.borrow().velocity(), velocity);
    for frame in 1..=120 {
        runtime
            .handle()
            .drain_frame_callbacks(1_050_000_000 + frame * 16_666_667);
    }
    assert_eq!(motion.lens_state().value(), 0.0);
    assert_eq!(motion.bar_state().value(), 0.0);
}

#[test]
fn contact_without_input_time_uses_the_shared_frame_clock() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let motion = TabContactMotion::new(runtime.handle());
    runtime.handle().drain_frame_callbacks(1_000_000_000);
    motion.pressed(true, None);
    runtime.handle().drain_frame_callbacks(1_050_000_000);
    assert!(motion.lens_state().value() > 0.2);
    assert!(motion.bar_state().value() > 0.1);
}

#[test]
fn stopped_shape_returns_to_exact_zero() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let shape = LensShapeMotion::new(runtime.handle(), LensShapeKind::Tab);
    shape
        .width
        .strain
        .borrow_mut()
        .animateTo(0.0005, spring(0.4, 35.0));
    for frame in 0..600 {
        runtime
            .handle()
            .drain_frame_callbacks(1_000_000_000 + frame * 16_666_667);
        shape.sample(0.0);
    }
    assert_eq!(shape.width.strain.borrow().target(), 0.0);
    assert_eq!(shape.width.strain.borrow().state().value(), 0.0);
    assert!(!shape.width.strain.borrow().is_running());
}

#[test]
fn shape_response_tracks_native_reversal_microframes() {
    let runtime = cranpose_core::Runtime::new(std::sync::Arc::new(cranpose_core::DefaultScheduler));
    let mut shape = LensShapeMotion::new(runtime.handle(), LensShapeKind::Tab);
    let mut errors = [[0.0f32; 2]; 8];
    let mut counts = [0; 8];
    let mut extrema = [0.0f32; 2];
    let mut route = usize::MAX;
    let mut output = String::from("route,time,x,native_width,native_height,width,height\n");
    for line in include_str!("../../../tests/fixtures/native_tab_shape.csv")
        .lines()
        .skip(1)
    {
        let fields: Vec<f64> = line.split(',').map(|v| v.parse().unwrap()).collect();
        let next = fields[0] as usize;
        if route != next {
            route = next;
            shape = LensShapeMotion::new(runtime.handle(), LensShapeKind::Tab);
        }
        runtime
            .handle()
            .drain_frame_callbacks((route as u64 + 1) * 10_000_000_000 + (fields[1] * 1e9) as u64);
        let actual = shape.sample(fields[2] as f32);
        output.push_str(&format!(
            "{route},{},{},{},{},{},{}\n",
            fields[1], fields[2], fields[3], fields[4], actual.width, actual.height
        ));
        for (axis, value) in [actual.width, actual.height].into_iter().enumerate() {
            errors[route][axis] += (value - fields[axis + 3] as f32).powi(2);
        }
        extrema[0] = extrema[0].max(actual.width);
        extrema[1] = extrema[1].min(actual.width);
        counts[route] += 1;
    }
    if let Ok(path) = std::env::var("CRANPOSE_OPTICAL_DEBUG") {
        std::fs::write(std::path::Path::new(&path).join("strain.csv"), output).unwrap();
    }
    assert!(counts.iter().all(|count| *count > 350));
    assert!(
        extrema[0] > 0.12 && extrema[1] < -0.12,
        "missing stretch/rebound: {extrema:?}"
    );
    for (route, axes) in errors.into_iter().enumerate() {
        for (axis, error) in axes.into_iter().enumerate() {
            let rms = (error / counts[route] as f32).sqrt();
            assert!(
                rms < 0.025,
                "native route {route} axis {axis} strain RMS: {rms}"
            );
        }
    }
}