animsmith-core 0.2.1

Engine-agnostic data model, sampling, measurements, and checks for the animsmith animation-clip linter
Documentation
//! The built-in check catalog. Each module is one check with its
//! defaults documented on the type. See DESIGN.md §6 for the tiers.

pub mod bind_pose;
pub mod constant_nonunit_scale;
pub mod constant_track;
pub mod duplicate_loop_endpoint;
pub mod duration_sanity;
pub mod foot_slide;
pub mod fps;
pub mod frozen_bone;
pub mod gait_group;
pub mod in_place;
pub mod loop_closure;
pub mod loop_seam;
pub mod loop_seam_rot;
pub mod loop_seam_vel;
pub mod missing_bones;
pub mod nan;
pub mod non_uniform_scale;
pub mod quat_flip;
pub mod quat_norm;
pub mod required_bones;
pub mod rest_world_scale;
pub mod root_motion_speed;
pub mod scale_keys;
pub mod sync_group;
pub mod time_complement;
pub mod time_monotonic;

mod vec3_trajectory;

use crate::evaluation::{CoverageGap, CoverageGapCode};
use crate::model::{Document, Track};
use crate::profile::{ResolvedRoles, Role};

/// Whether an `f32`-derived metric exceeds a user-facing `f64` cap.
///
/// Source transforms and sampled poses are `f32`, so decimal values such as
/// `0.1` can round slightly upward when promoted into measurement output. The
/// comparison quantizes both sides to the evidence precision so a value
/// authored exactly at an inclusive cap does not become a false positive.
pub(crate) fn exceeds_f32_cap(measured: f64, cap: f64) -> bool {
    (measured as f32) > (cap as f32)
}

/// Return the typed prerequisite gap for root-motion work, if any.
pub(crate) fn root_motion_gap(roles: &ResolvedRoles) -> Option<CoverageGap> {
    if roles.get(Role::Root).is_some() || roles.get(Role::Hips).is_some() {
        None
    } else {
        Some(CoverageGap::new(
            CoverageGapCode::ROLES_UNRESOLVED,
            format!(
                "root/hips role not resolved (rig profile '{}')",
                roles.profile
            ),
        ))
    }
}

/// Return the typed prerequisite gap for gait work, if any.
pub(crate) fn gait_gap(roles: &ResolvedRoles) -> Option<CoverageGap> {
    let has_foot = [
        Role::LeftFoot,
        Role::LeftToe,
        Role::RightFoot,
        Role::RightToe,
    ]
    .iter()
    .any(|&r| roles.get(r).is_some());
    if roles.get(Role::Hips).is_some() && has_foot {
        None
    } else {
        Some(CoverageGap::new(
            CoverageGapCode::ROLES_UNRESOLVED,
            format!(
                "hips/foot roles not resolved (rig profile '{}') — needs hips and at least one foot role",
                roles.profile
            ),
        ))
    }
}

/// Iterate `(clip name, bone name, track)` across a document.
pub(crate) fn tracks(doc: &Document) -> impl Iterator<Item = (&str, &str, &Track)> {
    doc.clips.iter().flat_map(move |clip| {
        clip.tracks.iter().map(move |track| {
            let bone = doc
                .skeleton
                .bones
                .get(track.bone)
                .map(|b| b.name.as_str())
                .unwrap_or("<unknown>");
            (clip.name.as_str(), bone, track)
        })
    })
}