use crate::check::{Check, CheckCtx};
use crate::finding::{Finding, Severity};
use crate::model::Property;
pub const DEFAULT_MAX_MEAN_REST_DELTA_DEG: f64 = 45.0;
pub struct BindPose;
impl Check for BindPose {
fn id(&self) -> &'static str {
"bind-pose"
}
fn run(&self, ctx: &CheckCtx, out: &mut Vec<Finding>) {
let cap = ctx
.config
.check_settings(self.id())
.max_mean_rest_delta_deg
.unwrap_or(DEFAULT_MAX_MEAN_REST_DELTA_DEG);
for clip in &ctx.doc.clips {
let mut total_deg = 0.0f64;
let mut counted = 0usize;
let mut worst: Option<(f64, &str)> = None;
for track in &clip.tracks {
if track.property != Property::Rotation || track.key_count() == 0 {
continue;
}
let Some(bone) = ctx.doc.skeleton.bones.get(track.bone) else {
continue;
};
let Some(first) = track.key_quat(0) else {
continue;
};
if !first.is_finite() || first.length_squared() == 0.0 {
continue;
}
let deg = bone
.rest
.rotation
.normalize()
.angle_between(first.normalize())
.to_degrees() as f64;
total_deg += deg;
counted += 1;
if worst.is_none_or(|(w, _)| deg > w) {
worst = Some((deg, bone.name.as_str()));
}
}
if counted < 3 {
continue; }
let mean = total_deg / counted as f64;
if mean > cap {
let (worst_deg, worst_bone) = worst.expect("counted > 0");
out.push(
Finding::new(
self.id(),
Severity::Warning,
format!(
"first frame deviates from the rest pose by {mean:.0}° on \
average across {counted} bones (worst {worst_bone}: \
{worst_deg:.0}°) — authored against a different bind?"
),
)
.clip(&clip.name)
.time(0.0)
.measured(mean)
.expected(cap),
);
}
}
}
}