use crate::check::{Check, CheckCtx, Readiness};
use crate::checks::root_motion_readiness;
use crate::finding::{Finding, Severity};
use crate::metrics::root_motion_speed_mps;
pub const STRAY_PIN_FLOOR_MPS: f64 = 0.5;
pub struct RootMotionSpeed;
impl RootMotionSpeed {
fn has_pending_work(ctx: &CheckCtx) -> bool {
ctx.clip_expectations()
.iter()
.any(|e| e.speed_mps.is_some() && e.in_place != Some(true))
}
}
impl Check for RootMotionSpeed {
fn id(&self) -> &'static str {
"root-motion-speed"
}
fn readiness(&self, ctx: &CheckCtx) -> Readiness {
if Self::has_pending_work(ctx) {
root_motion_readiness(ctx.roles)
} else {
Readiness::Idle
}
}
fn run(&self, ctx: &CheckCtx, out: &mut Vec<Finding>) {
for (index, clip) in ctx.doc.clips.iter().enumerate() {
let expectations = ctx.expectations(index);
let Some(pin) = expectations.speed_mps else {
continue;
};
if expectations.in_place == Some(true) {
continue;
}
let measured = ctx
.grid(index)
.and_then(|grid| root_motion_speed_mps(&grid, ctx.roles));
let Some(measured) = measured else {
continue; };
if measured < STRAY_PIN_FLOOR_MPS && pin.value >= STRAY_PIN_FLOOR_MPS {
out.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"declared speed {:.2} m/s but the clip carries almost no \
root motion ({measured:.2} m/s) — stray pin, or an \
in-place clip declared as root-motion",
pin.value
),
)
.clip(&clip.name)
.measured(measured)
.expected(pin.value),
);
continue;
}
if (measured - pin.value).abs() > pin.tolerance {
out.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"measured root-motion speed {measured:.2} m/s disagrees with \
the declared {:.2} ± {:.2} m/s — playback scaled by this pin \
will slide or moonwalk",
pin.value, pin.tolerance
),
)
.clip(&clip.name)
.measured(measured)
.expected(pin.value),
);
}
}
}
}