rustmotion-core 0.8.0

Core types, traits, and rendering utilities for rustmotion
Documentation
use rustmotion_core::engine::renderer::{interpolate_path_data, trim_path_between};
use skia_safe::Path;

fn horizontal_line(len: f32) -> Path {
    Path::from_svg(format!("M0 0 L{len} 0")).unwrap()
}

#[test]
fn trim_at_zero_and_progress_at_one_keeps_the_whole_path() {
    let path = horizontal_line(100.0);
    let trimmed = trim_path_between(&path, 0.0, 1.0);
    let mut measure = skia_safe::PathMeasure::new(&trimmed, false, None);
    assert!(
        (measure.length() - 100.0).abs() < 0.5,
        "start=0, end=1 must keep the full path, got length {}",
        measure.length()
    );
}

#[test]
fn trim_start_at_half_removes_the_first_half() {
    let path = horizontal_line(100.0);
    let trimmed = trim_path_between(&path, 0.5, 1.0);
    let mut measure = skia_safe::PathMeasure::new(&trimmed, false, None);
    assert!(
        (measure.length() - 50.0).abs() < 0.5,
        "start=0.5 must leave half the path visible, got length {}",
        measure.length()
    );

    let bounds = trimmed.bounds();
    assert!(
        bounds.left >= 49.0,
        "the first half of the path must have no ink; visible bounds start at {}",
        bounds.left
    );
}

#[test]
fn trim_end_at_half_removes_the_second_half() {
    let path = horizontal_line(100.0);
    let trimmed = trim_path_between(&path, 0.0, 0.5);
    let bounds = trimmed.bounds();
    assert!(
        bounds.right <= 51.0,
        "the second half of the path must have no ink; visible bounds end at {}",
        bounds.right
    );
}

#[test]
fn trim_start_after_end_yields_nothing_instead_of_swapping_silently() {
    let path = horizontal_line(100.0);
    let inverted = trim_path_between(&path, 0.8, 0.8);
    let mut measure = skia_safe::PathMeasure::new(&inverted, false, None);
    assert!(
        measure.length() < 0.5,
        "a zero-width window must paint nothing, got length {}",
        measure.length()
    );
}

#[test]
fn trim_reversed_start_and_end_is_treated_as_the_same_window() {
    let path = horizontal_line(100.0);
    let forward = trim_path_between(&path, 0.2, 0.6);
    let backward = trim_path_between(&path, 0.6, 0.2);
    let mut mf = skia_safe::PathMeasure::new(&forward, false, None);
    let mut mb = skia_safe::PathMeasure::new(&backward, false, None);
    assert!(
        (mf.length() - mb.length()).abs() < 0.5,
        "trim_path_between(0.2, 0.6) and trim_path_between(0.6, 0.2) must describe the same \
         window, got {} and {}",
        mf.length(),
        mb.length()
    );
}

#[test]
fn interpolate_matching_structure_paths_blends_the_points() {
    let from = "M0 0 L10 0 L10 10 Z";
    let to = "M0 0 L20 0 L20 20 Z";
    let mid = interpolate_path_data(from, to, 0.5).expect("same command structure must succeed");
    let bounds = mid.bounds();
    assert!(
        (bounds.right - 15.0).abs() < 0.01,
        "halfway between a 10-wide and a 20-wide triangle must be 15 wide, got {}",
        bounds.right
    );
    assert!(
        (bounds.bottom - 15.0).abs() < 0.01,
        "halfway between a 10-tall and a 20-tall triangle must be 15 tall, got {}",
        bounds.bottom
    );
}

#[test]
fn interpolate_at_t_zero_matches_the_first_keyframe_exactly() {
    let from = "M0 0 L10 0 L10 10 Z";
    let to = "M0 0 L40 0 L40 40 Z";
    let start = interpolate_path_data(from, to, 0.0).unwrap();
    let reference = Path::from_svg(from).unwrap();
    assert_eq!(*start.bounds(), *reference.bounds());
}

#[test]
fn interpolate_at_t_one_matches_the_second_keyframe_exactly() {
    let from = "M0 0 L10 0 L10 10 Z";
    let to = "M0 0 L40 0 L40 40 Z";
    let end = interpolate_path_data(from, to, 1.0).unwrap();
    let reference = Path::from_svg(to).unwrap();
    assert_eq!(*end.bounds(), *reference.bounds());
}

#[test]
fn interpolate_mismatched_command_counts_is_reported_and_returns_none() {
    let triangle = "M0 0 L10 0 L10 10 Z";
    let pentagon = "M0 0 L10 0 L10 10 L5 15 L0 10 Z";
    assert!(
        interpolate_path_data(triangle, pentagon, 0.5).is_none(),
        "a triangle and a pentagon do not share a command structure; interpolation must refuse \
         rather than snap or panic"
    );
}

#[test]
fn interpolate_mismatched_command_kind_at_the_same_position_is_reported_and_returns_none() {
    let with_a_line = "M0 0 L10 0 L10 10 Z";
    let with_a_curve = "M0 0 Q5 5 10 0 L10 10 Z";
    assert!(
        interpolate_path_data(with_a_line, with_a_curve, 0.5).is_none(),
        "a line and a quadratic curve at the same position in the command list must refuse to \
         interpolate rather than silently reinterpreting the control point"
    );
}

#[test]
fn interpolate_unparseable_svg_returns_none_instead_of_panicking() {
    assert!(interpolate_path_data("not a path", "M0 0 L10 0", 0.5).is_none());
    assert!(interpolate_path_data("M0 0 L10 0", "not a path", 0.5).is_none());
}