use crate::clips::{self, ClipInfo};
use crate::glb::Glb;
use crate::rigs::{Playback, Rig};
pub const FRAME: f32 = 1.0 / 24.0;
pub const ROOT_MOTION_EPS: f32 = 1.0e-4;
pub const CYCLE_TOL_DEFAULT: f32 = 0.20;
pub const CYCLE_TOL_MEASURED: f32 = 0.02;
pub fn cycle_tolerance(slot: &crate::rigs::SlotDef, provenance_current: bool) -> f32 {
match (slot.tolerance, &slot.keep, provenance_current) {
(Some(t), _, _) => t,
(None, Some(_), _) => CYCLE_TOL_DEFAULT,
(None, None, true) => CYCLE_TOL_MEASURED,
(None, None, false) => CYCLE_TOL_DEFAULT,
}
}
pub const PHASE_TOL_OK: f32 = 0.05;
pub const CYCLE_JOINT_SPREAD_TOL: f32 = 0.10;
pub const LOOP_TOL_DEG: f32 = 2.0;
pub const ONESHOT_END_TOL_DEG: f32 = 15.0;
pub const SKATE_FLOOR_CM_PER_FRAME: f32 = 0.10;
pub const SKATE_FLOOR: f32 = SKATE_FLOOR_CM_PER_FRAME * 60.0 / 100.0;
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SkateReport {
pub slot: usize,
pub authored_speed: f32,
pub onset_low: f32,
pub onset_high: f32,
pub required_cps_at_max: f32,
pub clamped_cps_at_max: f32,
pub max_skate: f32,
pub max_skate_cm_frame: f32,
pub skating_ratio: f32,
}
pub fn skate_report(
slot: usize,
duration: f32,
cycle_distance: f32,
drive: (f32, f32),
) -> SkateReport {
let (lo, hi) = drive;
let authored = cycle_distance / duration;
let realized = |v: f32| {
crate::gait::gait_cycles_per_sec(v, 1.0, cycle_distance, 1.0 / duration) * cycle_distance
};
let (clamp_lo, clamp_hi) = crate::gait::PHASE_RATE_CLAMP;
let onset_low = authored * clamp_lo;
let onset_high = authored * clamp_hi;
let skate = |v: f32| (v - realized(v)).abs();
let max_skate = skate(lo).max(skate(hi));
let span = hi - lo;
let skating_ratio = if span > 0.0 {
let below = (hi.min(onset_low - SKATE_FLOOR) - lo).max(0.0);
let above = (hi - lo.max(onset_high + SKATE_FLOOR)).max(0.0);
((below + above) / span).clamp(0.0, 1.0)
} else if max_skate > SKATE_FLOOR {
1.0
} else {
0.0
};
SkateReport {
slot,
authored_speed: authored,
onset_low,
onset_high,
required_cps_at_max: hi / cycle_distance,
clamped_cps_at_max: crate::gait::gait_cycles_per_sec(hi, 1.0, cycle_distance, 1.0 / duration),
max_skate,
max_skate_cm_frame: max_skate * 100.0 / 60.0,
skating_ratio,
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Finding {
pub slot: Option<usize>,
pub level: Level,
pub text: String,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum Level {
Ok,
Note,
Bad,
}
impl Finding {
pub fn ok(text: String) -> Finding {
Finding { slot: None, level: Level::Ok, text }
}
pub fn note(text: String) -> Finding {
Finding { slot: None, level: Level::Note, text }
}
pub fn bad(text: String) -> Finding {
Finding { slot: None, level: Level::Bad, text }
}
pub fn at(mut self, slot: usize) -> Finding {
self.slot = Some(slot);
self
}
}
pub fn worst(findings: &[Finding]) -> Level {
findings.iter().map(|f| f.level).max().unwrap_or(Level::Ok)
}
pub fn clip_label(index: usize, clips: &[ClipInfo]) -> String {
match clips.get(index).and_then(|c| c.name.as_deref()) {
Some(name) => format!("clip {index} ({name})"),
None => format!("clip {index}"),
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SlotMeasure {
pub slot: usize,
pub duration: f32,
pub cycle_distance: Option<f32>,
pub phase_offset: Option<f32>,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct RigCheck {
pub findings: Vec<Finding>,
pub slots: Vec<SlotMeasure>,
pub skates: Vec<SkateReport>,
}
pub fn check_rig(glb: &Glb, rig: &Rig, provenance_current: bool) -> RigCheck {
let mut findings = Vec::new();
let mut slots = Vec::new();
let mut skates = Vec::new();
let scale = rig.scale;
let found = clips::clips(glb);
let root_name = rig.root_node();
let root_node = clips::node_index(glb, root_name);
let eps = rig.contact_eps;
let foot_name = rig.contact_joint();
let joint_names: Vec<&str> = if rig.contact_joints.is_empty() {
vec![foot_name]
} else {
rig.contact_joints.iter().map(String::as_str).collect()
};
let resolved: Vec<usize> = joint_names
.iter()
.filter_map(|n| clips::node_index(glb, n))
.collect();
let missing: Vec<&str> = joint_names
.iter()
.filter(|n| clips::node_index(glb, n).is_none())
.copied()
.collect();
let foot = joint_names.first().and_then(|n| clips::node_index(glb, n));
let gaits = rig
.slots
.iter()
.filter(|s| matches!(s.playback, Playback::Gait { .. }))
.count();
if gaits > 0 {
if root_node.is_none() {
findings.push(Finding::bad(format!(
"declares {gaits} gait slot(s) but the asset has no node named '{root_name}' — the \
in-place check cannot run; export the rig with its root bone named {root_name}, \
or set `root_node:` on the rig in rigs.ron"
)));
}
if !missing.is_empty() {
let reference_clip = rig.slots.iter().find_map(|s| {
matches!(s.playback, Playback::Gait { .. }).then_some(s.clip)
});
let suggestion = match reference_clip
.map(|c| clips::contact_candidates(glb, c))
.filter(|c| !c.is_empty())
{
Some(cands) => format!(
"; candidate contact joints: {}",
cands
.iter()
.take(4)
.map(|(n, f)| format!("{n} {f:.2}"))
.collect::<Vec<_>>()
.join(", ")
),
None => String::new(),
};
let listed = missing.join("', '");
let consequence = if foot.is_none() {
"cycle distance cannot be measured"
} else {
"the multi-joint corroboration is short-handed"
};
findings.push(Finding::bad(format!(
"declares {gaits} gait slot(s) but the asset has no node named '{listed}' — \
{consequence}; export the rig with its contact feet under these names, or fix \
`contact_joints:` on the rig in rigs.ron{suggestion}"
)));
}
}
for (i, slot) in rig.slots.iter().enumerate() {
let Some(c) = found.get(slot.clip) else {
findings.push(
Finding::bad(format!(
"slot {i} names clip {} but the asset has {}: {} — it was re-exported; \
re-measure and update rigs.ron",
slot.clip,
found.len(),
roster(&found),
))
.at(i),
);
continue;
};
if let Playback::Free { speed } | Playback::OneShot { speed } = slot.playback {
if let Some(sr) = clips::source_rate(glb, slot.clip) {
findings.push(
Finding::ok(format!(
"slot {i} keyed at {:.0} fps ({} keys); at x{speed:.2} = {:.1} authored \
keys per rendered frame at 60 Hz",
sr.fps,
sr.keys,
sr.fps * speed / 60.0
))
.at(i),
);
}
match slot.playback {
Playback::Free { .. } => match clips::loop_closure(glb, slot.clip) {
Some(lc) if lc.max_angle_deg >= LOOP_TOL_DEG => {
let translation = if lc.max_translation >= 1.0e-3 {
match &lc.worst_translation_joint {
Some(j) => format!(
"; {j} also translates {:.3} file units",
lc.max_translation
),
None => String::new(),
}
} else {
String::new()
};
findings.push(
Finding::note(format!(
"slot {i} loop pops: {} ends {:.1} deg from its start (worst of \
{} rotated joints, tolerance {LOOP_TOL_DEG} deg){translation} — \
re-export with the last key matching the first",
lc.worst_joint, lc.max_angle_deg, lc.joints
))
.at(i),
);
}
Some(lc) => {
findings.push(
Finding::ok(format!(
"slot {i} loop closes ({} joints within {LOOP_TOL_DEG} deg)",
lc.joints
))
.at(i),
);
}
None => findings.push(
Finding::note(format!(
"slot {i}: loop closure cannot be measured — the clip drives no \
rotation channel"
))
.at(i),
),
},
_ => {
let idle = rig
.slots
.first()
.filter(|s0| matches!(s0.playback, Playback::Free { .. }))
.map(|s0| s0.clip);
match idle.and_then(|r| clips::end_pose_delta(glb, slot.clip, r)) {
Some(pd) if pd.max_angle_deg >= ONESHOT_END_TOL_DEG => {
findings.push(
Finding::note(format!(
"slot {i} ends {:.1} deg from the idle pose at {} (slot 0 \
first frame is the idle convention) — the fade back to idle \
pops; end the clip nearer the idle pose, or this state is \
meant to be terminal",
pd.max_angle_deg, pd.worst_joint
))
.at(i),
);
}
Some(pd) => {
findings.push(
Finding::ok(format!(
"slot {i} ends on the idle pose ({} joints within \
{ONESHOT_END_TOL_DEG} deg)",
pd.joints
))
.at(i),
);
}
None => findings.push(
Finding::note(format!(
"slot {i}: no idle reference to compare the one-shot's end state \
against — slot 0 is not a free loop"
))
.at(i),
),
}
}
}
continue;
}
let Playback::Gait {
duration,
cycle_distance,
..
} = slot.playback
else {
continue;
};
let mut measure = SlotMeasure {
slot: i,
duration: c.duration,
cycle_distance: None,
phase_offset: None,
};
if (c.duration - duration).abs() >= FRAME {
findings.push(
Finding::bad(format!(
"slot {i} is {:.3}s in the asset, {duration:.3}s here — the shared phase maps \
onto the wrong part of the clip and feet drift",
c.duration
))
.at(i),
);
}
if let Some(drive) = rig.drive_speed {
let sk = skate_report(i, duration, cycle_distance, drive);
let (lo, hi) = drive;
if sk.max_skate <= SKATE_FLOOR {
findings.push(
Finding::ok(format!(
"slot {i} authored {:.2} u/s; drive {lo:.1}-{hi:.1} u/s stays inside the \
x0.5-x2 cadence clamp (skate-free band {:.2}-{:.2} u/s)",
sk.authored_speed, sk.onset_low, sk.onset_high
))
.at(i),
);
} else {
let mut sides = Vec::new();
if hi > sk.onset_high + SKATE_FLOOR {
sides.push(format!(
"drive above {:.2} u/s outruns the x2 cadence clamp (needs {:.2} cps, \
capped at {:.2})",
sk.onset_high, sk.required_cps_at_max, sk.clamped_cps_at_max
));
}
if lo < sk.onset_low - SKATE_FLOOR {
sides.push(format!(
"drive below {:.2} u/s underruns the x0.5 clamp floor — the legs cannot \
slow further",
sk.onset_low
));
}
findings.push(
Finding::note(format!(
"slot {i} authored {:.2} u/s: {} — feet slide up to {:.2} u/s \
(~{:.2} cm/frame at 60 Hz; mocap's own floor is ~0.10) over {:.0}% of \
the {lo:.1}-{hi:.1} u/s drive range, when this gait carries the phase \
alone; author a gait nearer the drive speed or narrow the range",
sk.authored_speed,
sides.join("; "),
sk.max_skate,
sk.max_skate_cm_frame,
sk.skating_ratio * 100.0
))
.at(i),
);
}
skates.push(sk);
}
if let Some(r) = root_node {
let m = clips::root_motion(glb, slot.clip, r);
if m.iter().any(|v| *v >= ROOT_MOTION_EPS) {
findings.push(
Finding::bad(format!(
"slot {i} moves {root_name} by {m:?} — a gait must be authored in place; \
the game drives the transform itself"
))
.at(i),
);
}
}
if let Some(f) = foot {
match clips::contact_track(glb, slot.clip, f, eps) {
Some(track) => {
let raw = track.cycle_distance();
measure.cycle_distance = Some(raw);
let measured = raw * scale;
let err = (measured - cycle_distance).abs() / cycle_distance;
let tol = cycle_tolerance(slot, provenance_current);
let why = match (slot.tolerance, &slot.keep, provenance_current) {
(Some(_), _, _) => "explicit",
(None, Some(_), _) => "kept",
(None, None, true) => "measured-and-adopted",
(None, None, false) => "hand-measured default",
};
let eps_why = if eps.is_some() { "declared" } else { "derived" };
let thr = track.threshold;
if err >= tol {
findings.push(
Finding::bad(format!(
"slot {i} measures {measured:.3} m/cycle, manifest says \
{cycle_distance:.3} ({:.0}% out, tolerance {:.0}% — {why}; \
contact eps {thr:.2}x stance, {eps_why}) — re-measure or adopt",
err * 100.0,
tol * 100.0
))
.at(i),
);
} else {
findings.push(
Finding::ok(format!(
"slot {i} measures {measured:.3} m/cycle vs {cycle_distance:.3} \
declared (tolerance {:.0}%; contact eps {thr:.2}x stance, \
{eps_why})",
tol * 100.0
))
.at(i),
);
}
if resolved.len() > 1 {
let per_joint: Vec<f32> = resolved
.iter()
.filter_map(|&jx| {
Some(clips::contact_track(glb, slot.clip, jx, eps)?.cycle_distance())
})
.collect();
if per_joint.len() > 1 {
let hi = per_joint.iter().fold(f32::MIN, |a, &b| a.max(b));
let lo = per_joint.iter().fold(f32::MAX, |a, &b| a.min(b));
let spread = (hi - lo) / hi.max(1.0e-6);
if spread >= CYCLE_JOINT_SPREAD_TOL {
let listing = resolved
.iter()
.zip(joint_names.iter())
.filter_map(|(&jx, name)| {
let cd = clips::contact_track(glb, slot.clip, jx, eps)?
.cycle_distance();
Some(format!("{name} {:.3}", cd * scale))
})
.collect::<Vec<_>>()
.join(", ");
findings.push(
Finding::note(format!(
"slot {i} cycle distance disagrees across contact \
joints: {listing} ({:.0}% spread) — one foot's stance \
labels are suspect; check the contact plots",
spread * 100.0
))
.at(i),
);
}
}
}
}
None => findings.push(
Finding::note(format!(
"slot {i}: no planted-foot stance to measure — the stance and swing \
speeds do not separate; declare `contact_eps:` on the rig if a human \
can see the stance"
))
.at(i),
),
}
}
slots.push(measure);
}
if !skates.is_empty()
&& let Some((lo, hi)) = rig.drive_speed
{
let top = skates.iter().map(|s| s.onset_high).fold(f32::MIN, f32::max);
let bottom = skates.iter().map(|s| s.onset_low).fold(f32::MAX, f32::min);
if hi > top + SKATE_FLOOR {
let fastest = skates
.iter()
.map(|s| s.authored_speed)
.fold(f32::MIN, f32::max);
findings.push(Finding::note(format!(
"no gait covers the top of the drive range: fastest authored {fastest:.2} u/s \
x2 = {top:.2} < drive max {hi:.1} — everything above {top:.2} u/s skates \
regardless of blend"
)));
}
if lo < bottom - SKATE_FLOOR {
let slowest = skates
.iter()
.map(|s| s.authored_speed)
.fold(f32::MAX, f32::min);
findings.push(Finding::note(format!(
"no gait covers the bottom of the drive range: slowest authored {slowest:.2} \
u/s x0.5 = {bottom:.2} > drive min {lo:.1} — everything below {bottom:.2} u/s \
skates regardless of blend"
)));
}
}
if foot.is_some() {
let gait_slots: Vec<(usize, usize, f32)> = rig
.slots
.iter()
.enumerate()
.filter_map(|(i, s)| match s.playback {
Playback::Gait { phase_offset, .. } => Some((i, s.clip, phase_offset)),
_ => None,
})
.collect();
if let Some(&(ref_slot, ref_clip, ref_declared)) = gait_slots.first() {
if let Some(sm) = slots.iter_mut().find(|sm| sm.slot == ref_slot) {
sm.phase_offset = Some(0.0);
}
for &(i, clip, declared) in gait_slots.iter().skip(1) {
match clips::phase_match_joints(glb, ref_clip, clip, &resolved) {
Some(m) => {
let measured = clips::signed_offset(wrap01(ref_declared + m.offset));
let diff = clips::signed_offset(wrap01(measured - declared)).abs();
if let Some(sm) = slots.iter_mut().find(|sm| sm.slot == i) {
sm.phase_offset = Some(clips::signed_offset(m.offset));
}
if diff < PHASE_TOL_OK {
findings.push(
Finding::ok(format!(
"slot {i} phase {measured:+.3} measured vs {declared:+.3} \
declared"
))
.at(i),
);
} else {
findings.push(
Finding::note(format!(
"slot {i} phase {measured:+.3} measured vs {declared:+.3} \
declared ({diff:.3} of a cycle apart) — the declared \
alignment does not match the asset; adopt the measured set \
once it looks right"
))
.at(i),
);
}
if m.ambiguous {
let tried = if resolved.len() > 1 {
format!(
" (marker intersection over {} joints did not break the tie)",
resolved.len()
)
} else {
String::new()
};
findings.push(
Finding::note(format!(
"slot {i}: phase is ambiguous against slot {ref_slot} — the \
clips step a different number of times per cycle, so the \
alignment is a convention, not a measurement{tried}"
))
.at(i),
);
}
}
None => findings.push(
Finding::note(format!(
"slot {i}: phase cannot be measured against slot {ref_slot} — one of \
the two clips has no resolvable foot track"
))
.at(i),
),
}
}
}
}
RigCheck { findings, slots, skates }
}
fn wrap01(x: f32) -> f32 {
x.rem_euclid(1.0)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Staleness {
NeverMeasured,
Current,
Stale,
}
pub fn staleness(rig: &Rig, live_hash: u64) -> Staleness {
match &rig.provenance {
None => Staleness::NeverMeasured,
Some(p) if p.glb_fnv1a == crate::rigs::fingerprint_string(live_hash) => Staleness::Current,
Some(_) => Staleness::Stale,
}
}
pub fn clip_list_diff(recorded: &[String], live: &[ClipInfo]) -> Vec<String> {
let live_names: Vec<String> = live
.iter()
.map(|c| c.name.clone().unwrap_or_default())
.collect();
if recorded == live_names.as_slice() {
return Vec::new();
}
let show = |s: &String| -> String {
if s.is_empty() { "(unnamed)".to_owned() } else { s.clone() }
};
if live_names.len() == recorded.len() + 1
&& let Some(k) = (0..live_names.len())
.find(|&k| recorded.get(k) != live_names.get(k))
.filter(|&k| recorded[k..] == live_names[k + 1..])
{
return vec![format!(
"{} added at index {k}; every clip index after it shifted up by one",
show(&live_names[k])
)];
}
if recorded.len() == live_names.len() + 1
&& let Some(k) = (0..recorded.len())
.find(|&k| recorded.get(k) != live_names.get(k))
.filter(|&k| live_names[k..] == recorded[k + 1..])
{
return vec![format!(
"{} removed at index {k}; every clip index after it shifted down by one",
show(&recorded[k])
)];
}
if recorded.len() == live_names.len() {
let mut out: Vec<String> = recorded
.iter()
.zip(&live_names)
.enumerate()
.filter(|(_, (a, b))| a != b)
.take(6)
.map(|(i, (a, b))| format!("clip {i} renamed: {} -> {}", show(a), show(b)))
.collect();
let changed = recorded.iter().zip(&live_names).filter(|(a, b)| a != b).count();
if changed > 6 {
out.push(format!("... and {} more renames", changed - 6));
}
return out;
}
let first = (0..recorded.len().max(live_names.len()))
.find(|&k| recorded.get(k) != live_names.get(k))
.unwrap_or(0);
vec![format!(
"clip list changed: {} -> {} clips, first divergence at index {first}",
recorded.len(),
live_names.len()
)]
}
pub fn civil_date_utc(secs: u64) -> String {
let z = (secs / 86_400) as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + i64::from(m <= 2);
format!("{y:04}-{m:02}-{d:02}")
}
fn roster(found: &[ClipInfo]) -> String {
const CAP: usize = 20;
let mut names: Vec<String> = found
.iter()
.take(CAP)
.map(|c| match &c.name {
Some(n) => format!("{} {n}", c.index),
None => format!("{}", c.index),
})
.collect();
if found.len() > CAP {
names.push("...".to_owned());
}
names.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rigs::SlotDef;
use serde_json::json;
fn tiny_glb(node_names: &[&str], clip_name: &str, duration: f32) -> Glb {
let nodes: Vec<_> = node_names.iter().map(|n| json!({ "name": n })).collect();
Glb {
json: json!({
"nodes": nodes,
"animations": [{
"name": clip_name,
"channels": [{ "sampler": 0, "target": { "node": 0, "path": "translation" } }],
"samplers": [{ "input": 0, "output": 1 }],
}],
"accessors": [{ "max": [duration] }, {}],
}),
bin: Vec::new(),
}
}
fn gait_slot(clip: usize) -> SlotDef {
SlotDef {
clip,
playback: Playback::Gait {
duration: 1.417,
phase_offset: 0.0,
cycle_distance: 1.388,
},
mask: None,
note: None,
state: None,
keep: None,
tolerance: None,
}
}
fn free_slot(clip: usize) -> SlotDef {
SlotDef {
clip,
playback: Playback::Free { speed: 1.0 },
mask: None,
note: None,
state: None,
keep: None,
tolerance: None,
}
}
fn rig(slots: Vec<SlotDef>) -> Rig {
Rig {
mesh: "characters/test.glb".to_owned(),
scale: 1.0,
drive_speed: None,
contact_eps: None,
root_node: None,
contact_joints: Vec::new(),
provenance: None,
slots,
}
}
#[test]
fn a_gait_rig_without_the_anchor_nodes_is_told_loudly() {
let glb = tiny_glb(&["pelvis"], "walk", 1.417);
let report = check_rig(&glb, &rig(vec![gait_slot(0)]), false);
let bad: Vec<_> = report
.findings
.iter()
.filter(|f| f.level == Level::Bad)
.collect();
assert_eq!(bad.len(), 2, "{:?}", report.findings);
assert!(bad[0].text.contains(crate::rigs::DEFAULT_ROOT_NODE), "{}", bad[0].text);
assert!(bad[1].text.contains(crate::rigs::DEFAULT_CONTACT_JOINT), "{}", bad[1].text);
}
#[test]
fn a_configured_anchor_replaces_the_conventional_name() {
let glb = tiny_glb(&["pelvis"], "walk", 1.417);
let mut r = rig(vec![gait_slot(0)]);
r.root_node = Some("pelvis".to_owned());
r.contact_joints = vec!["pelvis".to_owned()];
let report = check_rig(&glb, &r, false);
let anchors_missing = report
.findings
.iter()
.any(|f| f.text.contains("has no node named"));
assert!(!anchors_missing, "{:?}", report.findings);
}
#[test]
fn a_rig_with_no_gaits_owes_no_anchors() {
let glb = tiny_glb(&["shell"], "scuttle", 0.8);
let report = check_rig(&glb, &rig(vec![free_slot(0)]), false);
assert!(
report.findings.iter().all(|f| f.level != Level::Bad),
"{:?}",
report.findings
);
assert!(
!report.findings.iter().any(|f| f.text.contains("no node named")),
"{:?}",
report.findings
);
}
#[test]
fn a_missing_clip_names_what_the_asset_does_have() {
let glb = tiny_glb(&["pelvis"], "walk", 1.417);
let report = check_rig(&glb, &rig(vec![free_slot(7)]), false);
assert_eq!(report.findings.len(), 1);
let f = &report.findings[0];
assert_eq!(f.level, Level::Bad);
assert_eq!(f.slot, Some(0));
assert!(f.text.contains("0 walk"), "{}", f.text);
}
#[test]
fn the_shipped_valkyrie_produces_no_bad_findings() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.unwrap_or_else(|e| panic!("workspace root: {e}"));
let text = std::fs::read_to_string(root.join("assets/grimoire/rigs.ron"))
.unwrap_or_else(|e| panic!("rigs.ron: {e}"));
let rigs = crate::rigs::Rigs::parse(&text).unwrap_or_else(|e| panic!("{e}"));
let valk = rigs
.get("valkyrie")
.unwrap_or_else(|| panic!("no valkyrie in the manifest"));
let glb = Glb::open(&root.join("assets").join(&valk.mesh))
.unwrap_or_else(|e| panic!("{e}"));
let report = check_rig(&glb, valk, false);
let bad: Vec<_> = report
.findings
.iter()
.filter(|f| f.level == Level::Bad)
.collect();
assert!(bad.is_empty(), "{bad:?}");
assert_eq!(report.slots.len(), 6, "the valkyrie declares six gaits");
assert_eq!(report.skates.len(), 6, "one skate report per gait");
assert!(
report
.findings
.iter()
.any(|f| f.level == Level::Note && f.text.contains("cadence clamp")),
"the shipped drive range should produce at least one skate note"
);
}
#[test]
fn clip_labels_carry_the_asset_name_when_there_is_one() {
let glb = tiny_glb(&["pelvis"], "walk", 1.0);
let found = clips::clips(&glb);
assert_eq!(clip_label(0, &found), "clip 0 (walk)");
assert_eq!(clip_label(9, &found), "clip 9");
}
#[test]
fn staleness_is_a_three_way_answer() {
let mut r = rig(vec![free_slot(0)]);
assert_eq!(staleness(&r, 7), Staleness::NeverMeasured);
r.provenance = Some(crate::rigs::Provenance {
glb_fnv1a: crate::rigs::fingerprint_string(7),
clips: 0,
clip_names: Vec::new(),
tool: crate::rigs::BENCH_TOOL_VERSION,
date: "2026-08-06".to_owned(),
});
assert_eq!(staleness(&r, 7), Staleness::Current);
assert_eq!(staleness(&r, 8), Staleness::Stale);
}
fn infos(names: &[&str]) -> Vec<ClipInfo> {
names
.iter()
.enumerate()
.map(|(index, n)| ClipInfo {
index,
name: (!n.is_empty()).then(|| (*n).to_owned()),
duration: 1.0,
channels: 1,
})
.collect()
}
fn owned(names: &[&str]) -> Vec<String> {
names.iter().map(|n| (*n).to_owned()).collect()
}
#[test]
fn a_clip_list_diff_states_the_cause_not_the_symptom() {
let recorded = owned(&["idle", "walk", "run"]);
assert!(clip_list_diff(&recorded, &infos(&["idle", "walk", "run"])).is_empty());
let added = clip_list_diff(&recorded, &infos(&["idle", "strafe_l", "walk", "run"]));
assert_eq!(added.len(), 1);
assert!(
added[0].contains("strafe_l added at index 1") && added[0].contains("shifted"),
"{added:?}"
);
let removed = clip_list_diff(&recorded, &infos(&["idle", "run"]));
assert_eq!(removed.len(), 1);
assert!(removed[0].contains("walk removed at index 1"), "{removed:?}");
let renamed = clip_list_diff(&recorded, &infos(&["idle", "walk", "sprint"]));
assert_eq!(renamed.len(), 1);
assert!(renamed[0].contains("clip 2 renamed: run -> sprint"), "{renamed:?}");
let messy = clip_list_diff(&recorded, &infos(&["other"]));
assert_eq!(messy.len(), 1);
assert!(messy[0].contains("3 -> 1 clips"), "{messy:?}");
}
#[test]
fn civil_dates_come_out_gregorian() {
assert_eq!(civil_date_utc(45_000), "1970-01-01");
assert_eq!(civil_date_utc(951_827_400), "2000-02-29");
assert_eq!(civil_date_utc(1_786_019_400), "2026-08-06");
assert_eq!(civil_date_utc(946_643_400), "1999-12-31");
}
#[test]
fn the_cycle_tolerance_policy_is_a_truth_table() {
let mut slot = gait_slot(0);
assert_eq!(cycle_tolerance(&slot, false), CYCLE_TOL_DEFAULT);
assert_eq!(cycle_tolerance(&slot, true), CYCLE_TOL_MEASURED);
slot.keep = Some("authored feel".to_owned());
assert_eq!(cycle_tolerance(&slot, true), CYCLE_TOL_DEFAULT);
slot.tolerance = Some(0.07);
assert_eq!(cycle_tolerance(&slot, true), 0.07);
assert_eq!(cycle_tolerance(&slot, false), 0.07);
}
#[test]
fn unmeasurable_loop_closure_is_a_note_not_a_silent_pass() {
let glb = tiny_glb(&["pelvis"], "idle", 1.0);
let report = check_rig(&glb, &rig(vec![free_slot(0)]), false);
assert!(
report
.findings
.iter()
.any(|f| f.level == Level::Note && f.text.contains("loop closure cannot")),
"{:?}",
report.findings
);
}
#[test]
fn a_one_shot_without_an_idle_reference_is_told() {
let glb = tiny_glb(&["pelvis"], "fire", 1.0);
let mut shot = free_slot(0);
shot.playback = Playback::OneShot { speed: 1.0 };
let report = check_rig(&glb, &rig(vec![shot]), false);
assert!(
report
.findings
.iter()
.any(|f| f.level == Level::Note && f.text.contains("no idle reference")),
"{:?}",
report.findings
);
}
#[test]
fn the_skate_report_is_a_truth_table() {
let sk = skate_report(2, 1.4, 1.4, (0.6, 1.8));
assert_eq!(sk.slot, 2);
assert!((sk.authored_speed - 1.0).abs() < 1.0e-6);
assert!((sk.onset_low - 0.5).abs() < 1.0e-6 && (sk.onset_high - 2.0).abs() < 1.0e-6);
assert_eq!(sk.max_skate, 0.0);
assert_eq!(sk.skating_ratio, 0.0);
let sk = skate_report(0, 1.4, 1.4, (1.0, 3.0));
assert!((sk.max_skate - 1.0).abs() < 1.0e-5, "{}", sk.max_skate);
assert!((sk.max_skate_cm_frame - 1.0 * 100.0 / 60.0).abs() < 1.0e-4);
let expect = (3.0 - (2.0 + SKATE_FLOOR)) / 2.0;
assert!((sk.skating_ratio - expect).abs() < 1.0e-5, "{}", sk.skating_ratio);
let sk = skate_report(0, 1.4, 1.4, (0.1, 0.6));
assert!((sk.max_skate - 0.4).abs() < 1.0e-5, "{}", sk.max_skate);
let expect = ((0.5 - SKATE_FLOOR) - 0.1) / 0.5;
assert!((sk.skating_ratio - expect).abs() < 1.0e-5, "{}", sk.skating_ratio);
assert_eq!(skate_report(0, 1.4, 1.4, (3.0, 3.0)).skating_ratio, 1.0);
assert_eq!(skate_report(0, 1.4, 1.4, (1.0, 1.0)).skating_ratio, 0.0);
let sk = skate_report(0, 1.4, 1.4, (1.0, 3.0));
assert_eq!(
sk.clamped_cps_at_max,
crate::gait::gait_cycles_per_sec(3.0, 1.0, 1.4, 1.0 / 1.4)
);
assert!((sk.required_cps_at_max - 3.0 / 1.4).abs() < 1.0e-6);
}
#[test]
fn the_skate_check_notes_but_never_reddens() {
let glb = tiny_glb(&["Root", "foot_l"], "walk", 1.417);
let mut r = rig(vec![gait_slot(0)]);
r.drive_speed = Some((0.9, 6.0));
let report = check_rig(&glb, &r, false);
let skate_notes: Vec<_> = report
.findings
.iter()
.filter(|f| f.level == Level::Note && f.text.contains("cadence clamp"))
.collect();
assert!(!skate_notes.is_empty(), "{:?}", report.findings);
assert!(skate_notes[0].text.contains("cps"), "{}", skate_notes[0].text);
assert_eq!(report.skates.len(), 1);
assert!(
report.findings.iter().any(|f| f.text.contains("top of the drive range")),
"{:?}",
report.findings
);
let quiet = check_rig(&glb, &rig(vec![gait_slot(0)]), false);
assert!(quiet.skates.is_empty());
assert!(!quiet.findings.iter().any(|f| f.text.contains("clamp")));
}
#[test]
fn the_fingerprint_spelling_and_hash_are_pinned() {
assert_eq!(crate::glb::fnv1a(b""), 0xcbf2_9ce4_8422_2325);
assert_eq!(crate::glb::fnv1a(b"a"), 0xaf63_dc4c_8601_ec8c);
assert_eq!(
crate::rigs::fingerprint_string(0xaf63_dc4c_8601_ec8c),
"0xaf63dc4c8601ec8c"
);
}
}