use animsmith_core::diff::{
AMPLITUDE_THRESHOLD_M, DURATION_THRESHOLD_S, LOOP_ANGULAR_VELOCITY_THRESHOLD_DEGPS,
LOOP_POSITION_THRESHOLD_M, LOOP_ROTATION_THRESHOLD_DEG, LOOP_VELOCITY_THRESHOLD_MPS,
MetricDelta, PHASE_THRESHOLD, ROOT_TRAJECTORY_TRANSLATION_THRESHOLD_M,
ROOT_TRAJECTORY_YAW_THRESHOLD_DEG, ROTATION_RANGE_THRESHOLD_DEG, SEAM_THRESHOLD,
SPEED_THRESHOLD_MPS, diff_measurements,
};
use animsmith_core::measure::ClipMeasurements;
use serde_json::json;
use std::collections::BTreeMap;
type MeasurementMap = BTreeMap<String, ClipMeasurements>;
type PublicDiffFn = fn(&MeasurementMap, &MeasurementMap) -> Vec<MetricDelta>;
const PUBLIC_DIFF_MEASUREMENTS: PublicDiffFn = diff_measurements;
fn measurements(duration_s: f64) -> MeasurementMap {
serde_json::from_value(json!({
"walk": {
"duration_s": duration_s,
"frame_count": 31,
"animated_bones": ["hips"],
"bone_channels": [{
"bone_index": 0,
"bone_name": "hips",
"properties": ["translation"]
}],
"bone_rotation_range_deg": { "hips": 10.0 },
"loop_continuity": { "bones": [{
"bone_index": 0,
"bone_name": "hips",
"position_delta_m": 0.02,
"rotation_delta_deg": 2.0,
"seam_velocity_delta_mps": 0.2,
"seam_angular_velocity_delta_degps": 10.0
}] },
"loop_continuity_availability": "measured",
"loop_endpoint_mode_availability": "not_applicable",
"frame_grid_availability": "not_applicable",
"loop_seam_ratio": 0.2,
"loop_seam_ratio_availability": "measured",
"gait": {
"phase": 0.25,
"phase_availability": "measured",
"lr_amplitude_m": 0.1
},
"gait_availability": "measured",
"root_trajectory": {
"bone_index": 0,
"bone_name": "hips",
"source_role": "hips_fallback",
"translation": {
"horizontal_displacement_x_m": 0.0,
"horizontal_displacement_z_m": 1.0,
"horizontal_travel_m": 1.0,
"vertical_displacement_m": 0.0,
"vertical_min_displacement_m": 0.0,
"vertical_max_displacement_m": 0.0
},
"translation_availability": "measured",
"yaw": {
"heading_axis": "positive_z",
"net_yaw_deg": 0.0,
"unwrapped_yaw_deg": 0.0,
"yaw_travel_deg": 0.0
},
"yaw_availability": "measured"
},
"root_trajectory_availability": "measured",
"speed_mps": 1.0,
"speed_mps_availability": "measured"
}
}))
.expect("valid public measurement map")
}
#[test]
fn public_diff_api_accepts_deserialized_measurements() {
let deltas = PUBLIC_DIFF_MEASUREMENTS(&measurements(1.0), &measurements(1.1));
assert_eq!(deltas.len(), 1, "{deltas:?}");
let delta = &deltas[0];
assert_eq!(delta.clip, "walk");
assert_eq!(delta.metric, "duration_s");
assert_eq!(delta.before, Some(1.0));
assert_eq!(delta.after, Some(1.1));
assert_eq!(delta.note, "moved");
}
#[test]
fn threshold_constants_are_public_and_unchanged() {
assert_eq!(DURATION_THRESHOLD_S, 0.017);
assert_eq!(ROTATION_RANGE_THRESHOLD_DEG, 1.0);
assert_eq!(LOOP_POSITION_THRESHOLD_M, 0.001);
assert_eq!(LOOP_ROTATION_THRESHOLD_DEG, 0.1);
assert_eq!(LOOP_VELOCITY_THRESHOLD_MPS, 0.01);
assert_eq!(LOOP_ANGULAR_VELOCITY_THRESHOLD_DEGPS, 0.5);
assert_eq!(SEAM_THRESHOLD, 0.05);
assert_eq!(PHASE_THRESHOLD, 0.05);
assert_eq!(AMPLITUDE_THRESHOLD_M, 0.005);
assert_eq!(SPEED_THRESHOLD_MPS, 0.1);
assert_eq!(ROOT_TRAJECTORY_TRANSLATION_THRESHOLD_M, 0.001);
assert_eq!(ROOT_TRAJECTORY_YAW_THRESHOLD_DEG, 0.1);
}
#[test]
fn public_diff_treats_non_finite_measurements_as_absent() {
struct Case {
metric: &'static str,
finite: f64,
make_non_finite: fn(&mut ClipMeasurements),
appeared_note: &'static str,
disappeared_note: &'static str,
}
let cases = [
Case {
metric: "duration_s",
finite: 1.0,
make_non_finite: |clip| clip.duration_s = f64::NAN,
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "loop_seam_ratio",
finite: 0.2,
make_non_finite: |clip| clip.loop_seam_ratio = Some(f64::INFINITY),
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "gait.phase",
finite: 0.25,
make_non_finite: |clip| {
clip.gait.as_mut().expect("fixture gait").phase = Some(f64::NEG_INFINITY);
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "gait.lr_amplitude_m",
finite: 0.1,
make_non_finite: |clip| {
clip.gait.as_mut().expect("fixture gait").lr_amplitude_m = f64::NAN;
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "speed_mps",
finite: 1.0,
make_non_finite: |clip| clip.speed_mps = Some(f64::INFINITY),
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "bone_rotation_range_deg[hips]",
finite: 10.0,
make_non_finite: |clip| {
clip.bone_rotation_range_deg.insert("hips".into(), f64::NAN);
},
appeared_note: "bone now animated",
disappeared_note: "bone no longer animated",
},
Case {
metric: "loop_continuity.bones[0].position_delta_m",
finite: 0.02,
make_non_finite: |clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].position_delta_m = f64::NAN;
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "loop_continuity.bones[0].rotation_delta_deg",
finite: 2.0,
make_non_finite: |clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].rotation_delta_deg = f64::INFINITY;
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "loop_continuity.bones[0].seam_velocity_delta_mps",
finite: 0.2,
make_non_finite: |clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].seam_velocity_delta_mps =
f64::NEG_INFINITY;
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
Case {
metric: "loop_continuity.bones[0].seam_angular_velocity_delta_degps",
finite: 10.0,
make_non_finite: |clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].seam_angular_velocity_delta_degps =
f64::NEG_INFINITY;
},
appeared_note: "appeared",
disappeared_note: "disappeared",
},
];
for case in cases {
let finite = measurements(1.0);
let mut non_finite = finite.clone();
(case.make_non_finite)(non_finite.get_mut("walk").expect("fixture clip"));
assert!(
PUBLIC_DIFF_MEASUREMENTS(&non_finite, &non_finite).is_empty(),
"identical non-finite {} must not produce a false delta",
case.metric
);
let disappeared = PUBLIC_DIFF_MEASUREMENTS(&finite, &non_finite);
assert_eq!(disappeared.len(), 1, "{}: {disappeared:?}", case.metric);
assert_eq!(disappeared[0].metric, case.metric);
assert_eq!(disappeared[0].before, Some(case.finite));
assert_eq!(disappeared[0].after, None);
assert_eq!(disappeared[0].note, case.disappeared_note);
let appeared = PUBLIC_DIFF_MEASUREMENTS(&non_finite, &finite);
assert_eq!(appeared.len(), 1, "{}: {appeared:?}", case.metric);
assert_eq!(appeared[0].metric, case.metric);
assert_eq!(appeared[0].before, None);
assert_eq!(appeared[0].after, Some(case.finite));
assert_eq!(appeared[0].note, case.appeared_note);
}
}