use crate::check::{Check, CheckCtx};
use crate::finding::{Finding, Severity};
use crate::metrics::rotation_range_deg;
pub const DEFAULT_MIN_ROTATION_DEG: f64 = 1.0;
pub struct FrozenBone;
impl Check for FrozenBone {
fn id(&self) -> &'static str {
"frozen-bone"
}
fn run(&self, ctx: &CheckCtx, out: &mut Vec<Finding>) {
let floor = ctx
.config
.check_settings(self.id())
.min_rotation_deg
.unwrap_or(DEFAULT_MIN_ROTATION_DEG);
for (index, clip) in ctx.doc.clips.iter().enumerate() {
let Some(required) = ctx.expectations(index).animates_bones.as_deref() else {
continue;
};
for bone_name in required {
let Some(bone_id) = ctx
.doc
.skeleton
.bones
.iter()
.position(|b| &b.name == bone_name)
else {
continue; };
let mut has_any_track = false;
let mut max_deg = 0.0f64;
for track in clip.tracks.iter().filter(|t| t.bone == bone_id) {
has_any_track = true;
if let Some(deg) = rotation_range_deg(track) {
max_deg = max_deg.max(deg);
}
}
if has_any_track && max_deg < floor {
out.push(
Finding::new(
self.id(),
Severity::Error,
format!(
"required bone rotates only {max_deg:.2}° over the clip \
(floor {floor:.2}°) — frozen/T-posed limb or a \
wrong-source slice"
),
)
.clip(&clip.name)
.bone(bone_name.clone())
.measured(max_deg)
.expected(floor),
);
}
}
}
}
}