animsmith-core 0.4.1

Engine-agnostic data model, sampling, measurements, and checks for the animsmith animation-clip linter
Documentation
use animsmith_core::config::{CheckSettings, Config, ConfigValidationError};

const NONNEGATIVE_CHECK_SETTINGS: [(&str, &str); 10] = [
    ("loop-seam", "max_ratio"),
    ("loop-seam", "min_stride_step_m"),
    ("loop-closure", "max_position_delta_m"),
    ("loop-closure", "max_rotation_delta_deg"),
    ("loop-seam-vel", "max_velocity_delta_mps"),
    ("loop-seam-rot", "max_angular_velocity_delta_degps"),
    ("frozen-bone", "min_rotation_deg"),
    ("bind-pose", "max_mean_rest_delta_deg"),
    ("foot-slide", "contact_height_m"),
    ("foot-slide", "max_slide_mps"),
];

fn settings_with(field: &str, value: f64) -> CheckSettings {
    let mut settings = CheckSettings::default();
    match field {
        "max_ratio" => settings.max_ratio = Some(value),
        "min_stride_step_m" => settings.min_stride_step_m = Some(value),
        "max_position_delta_m" => settings.max_position_delta_m = Some(value),
        "max_rotation_delta_deg" => settings.max_rotation_delta_deg = Some(value),
        "max_velocity_delta_mps" => settings.max_velocity_delta_mps = Some(value),
        "max_angular_velocity_delta_degps" => {
            settings.max_angular_velocity_delta_degps = Some(value);
        }
        "min_rotation_deg" => settings.min_rotation_deg = Some(value),
        "max_mean_rest_delta_deg" => settings.max_mean_rest_delta_deg = Some(value),
        "contact_height_m" => settings.contact_height_m = Some(value),
        "max_slide_mps" => settings.max_slide_mps = Some(value),
        _ => panic!("unknown check setting {field}"),
    }
    settings
}

fn settings_value(settings: &CheckSettings, field: &str) -> Option<f64> {
    match field {
        "max_ratio" => settings.max_ratio,
        "min_stride_step_m" => settings.min_stride_step_m,
        "max_position_delta_m" => settings.max_position_delta_m,
        "max_rotation_delta_deg" => settings.max_rotation_delta_deg,
        "max_velocity_delta_mps" => settings.max_velocity_delta_mps,
        "max_angular_velocity_delta_degps" => settings.max_angular_velocity_delta_degps,
        "min_rotation_deg" => settings.min_rotation_deg,
        "max_mean_rest_delta_deg" => settings.max_mean_rest_delta_deg,
        "contact_height_m" => settings.contact_height_m,
        "max_slide_mps" => settings.max_slide_mps,
        _ => panic!("unknown check setting {field}"),
    }
}

#[test]
fn deserialized_check_settings_reject_negative_and_non_finite_values() {
    for (check_id, field) in NONNEGATIVE_CHECK_SETTINGS {
        for value in [
            "-0.01", "-1.0", "-1e308", "-5e-324", "nan", "-nan", "inf", "-inf",
        ] {
            let source = format!("[checks.{check_id}]\n{field} = {value}\n");
            assert!(
                toml::from_str::<Config>(&source).is_err(),
                "accepted invalid {check_id}.{field}={value}"
            );
        }

        let source = format!("[checks.{check_id}]\n{field} = 0.0\n");
        let config: Config = toml::from_str(&source)
            .unwrap_or_else(|error| panic!("rejected {check_id}.{field}=0.0: {error}"));
        assert_eq!(
            settings_value(&config.check_settings(check_id), field),
            Some(0.0),
            "did not preserve {check_id}.{field}=0.0"
        );
    }
}

#[test]
fn programmatic_check_settings_return_typed_validation_errors() {
    for (check_id, field) in NONNEGATIVE_CHECK_SETTINGS {
        for value in [
            -0.01,
            -1.0,
            -f64::MAX,
            -f64::from_bits(1),
            f64::NAN,
            f64::from_bits(0x7ff8_0000_0000_0001),
            f64::from_bits(0xfff8_0000_0000_0001),
            f64::INFINITY,
            f64::NEG_INFINITY,
        ] {
            let mut config = Config::default();
            config
                .checks
                .insert(check_id.into(), settings_with(field, value));

            assert_eq!(
                config.validate(),
                Err(ConfigValidationError::InvalidCheckSetting {
                    check_id: check_id.into(),
                    field,
                }),
                "{check_id}.{field}={value}"
            );
        }

        let mut config = Config::default();
        config
            .checks
            .insert(check_id.into(), settings_with(field, 0.0));
        config
            .validate()
            .unwrap_or_else(|error| panic!("rejected {check_id}.{field}=0.0: {error}"));
    }
}