use crate::finding::Severity;
use crate::metrics::MIN_STRIDE_STEP_M;
use crate::profile::Role;
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Pinned {
pub value: f64,
pub tolerance: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SeveritySetting {
Off,
Note,
#[serde(alias = "warning")]
Warn,
Error,
}
impl SeveritySetting {
pub fn as_severity(self) -> Option<Severity> {
match self {
SeveritySetting::Off => None,
SeveritySetting::Note => Some(Severity::Note),
SeveritySetting::Warn => Some(Severity::Warning),
SeveritySetting::Error => Some(Severity::Error),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheckSettings {
pub severity: Option<SeveritySetting>,
pub max_ratio: Option<f64>,
pub min_stride_step_m: Option<f64>,
pub min_rotation_deg: Option<f64>,
pub max_mean_rest_delta_deg: Option<f64>,
pub contact_height_m: Option<f64>,
pub max_slide_mps: Option<f64>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClipExpectations {
#[serde(rename = "loop")]
pub looping: Option<bool>,
pub speed_mps: Option<Pinned>,
pub in_place: Option<bool>,
pub fps: Option<f64>,
pub animates_bones: Option<Vec<String>>,
}
impl ClipExpectations {
fn merged_with(&self, other: &ClipExpectations) -> ClipExpectations {
ClipExpectations {
looping: other.looping.or(self.looping),
speed_mps: other.speed_mps.or(self.speed_mps),
in_place: other.in_place.or(self.in_place),
fps: other.fps.or(self.fps),
animates_bones: other
.animates_bones
.clone()
.or_else(|| self.animates_bones.clone()),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GaitGroup {
pub clips: Vec<String>,
pub max_gait_phase_spread: f64,
#[serde(default)]
pub min_lr_amplitude_m: f64,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RigConfig {
#[serde(default = "default_profile")]
pub profile: String,
#[serde(default)]
pub roles: BTreeMap<Role, String>,
}
fn default_profile() -> String {
"auto".into()
}
impl Default for RigConfig {
fn default() -> Self {
Self {
profile: default_profile(),
roles: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub rig: RigConfig,
#[serde(default)]
pub checks: BTreeMap<String, CheckSettings>,
#[serde(default)]
pub clips: BTreeMap<String, ClipExpectations>,
#[serde(default)]
pub gait_groups: BTreeMap<String, GaitGroup>,
}
impl Config {
pub fn expectations_for(&self, clip: &str) -> ClipExpectations {
let mut out = ClipExpectations::default();
for (pattern, exp) in &self.clips {
if pattern != clip && glob_match(pattern, clip) {
out = out.merged_with(exp);
}
}
if let Some(exact) = self.clips.get(clip) {
out = out.merged_with(exact);
}
out
}
pub fn check_settings(&self, id: &str) -> CheckSettings {
self.checks.get(id).cloned().unwrap_or_default()
}
pub fn loop_seam_min_stride_step_m(&self) -> f64 {
self.check_settings("loop-seam")
.min_stride_step_m
.unwrap_or(MIN_STRIDE_STEP_M)
}
}
pub fn glob_match(pattern: &str, name: &str) -> bool {
fn inner(p: &[u8], n: &[u8]) -> bool {
match p.split_first() {
None => n.is_empty(),
Some((b'*', rest)) => (0..=n.len()).any(|skip| inner(rest, &n[skip..])),
Some((c, rest)) => n
.split_first()
.is_some_and(|(nc, nrest)| nc == c && inner(rest, nrest)),
}
}
inner(pattern.as_bytes(), name.as_bytes())
}