use crate::model::{BoneId, Skeleton};
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Role {
Root,
Hips,
Spine,
Head,
LeftFoot,
RightFoot,
LeftToe,
RightToe,
LeftHand,
RightHand,
}
impl Role {
pub fn as_str(self) -> &'static str {
match self {
Role::Root => "root",
Role::Hips => "hips",
Role::Spine => "spine",
Role::Head => "head",
Role::LeftFoot => "left_foot",
Role::RightFoot => "right_foot",
Role::LeftToe => "left_toe",
Role::RightToe => "right_toe",
Role::LeftHand => "left_hand",
Role::RightHand => "right_hand",
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum NameMatcher {
Exact(&'static str),
}
impl NameMatcher {
fn matches(&self, bone_name: &str) -> bool {
let NameMatcher::Exact(wanted) = self;
if bone_name == *wanted {
return true;
}
bone_name
.rsplit_once(':')
.is_some_and(|(_, stripped)| stripped == *wanted)
}
}
#[derive(Debug, Clone)]
pub struct RigProfile {
pub name: &'static str,
pub bindings: Vec<(Role, NameMatcher)>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedRoles {
pub profile: String,
map: BTreeMap<Role, BoneId>,
}
impl ResolvedRoles {
pub fn get(&self, role: Role) -> Option<BoneId> {
self.map.get(&role).copied()
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (Role, BoneId)> + '_ {
self.map.iter().map(|(&r, &b)| (r, b))
}
pub fn from_names(
skeleton: &Skeleton,
names: impl IntoIterator<Item = (Role, String)>,
) -> Self {
let mut map = BTreeMap::new();
for (role, name) in names {
if let Some(id) = skeleton.bones.iter().position(|b| b.name == name) {
map.insert(role, id);
}
}
Self {
profile: "custom".into(),
map,
}
}
}
impl RigProfile {
pub fn resolve(&self, skeleton: &Skeleton) -> ResolvedRoles {
let mut map = BTreeMap::new();
for (role, matcher) in &self.bindings {
if let Some(id) = skeleton.bones.iter().position(|b| matcher.matches(&b.name)) {
map.insert(*role, id);
}
}
ResolvedRoles {
profile: self.name.into(),
map,
}
}
}
pub fn builtin_profiles() -> Vec<RigProfile> {
use NameMatcher::Exact;
use Role::*;
vec![
RigProfile {
name: "mixamo",
bindings: vec![
(Hips, Exact("mixamorig:Hips")),
(Spine, Exact("mixamorig:Spine")),
(Head, Exact("mixamorig:Head")),
(LeftFoot, Exact("mixamorig:LeftFoot")),
(RightFoot, Exact("mixamorig:RightFoot")),
(LeftToe, Exact("mixamorig:LeftToeBase")),
(RightToe, Exact("mixamorig:RightToeBase")),
(LeftHand, Exact("mixamorig:LeftHand")),
(RightHand, Exact("mixamorig:RightHand")),
],
},
RigProfile {
name: "ue-mannequin",
bindings: vec![
(Root, Exact("root")),
(Hips, Exact("pelvis")),
(Spine, Exact("spine_01")),
(Head, Exact("head")),
(LeftFoot, Exact("foot_l")),
(RightFoot, Exact("foot_r")),
(LeftToe, Exact("ball_l")),
(RightToe, Exact("ball_r")),
(LeftHand, Exact("hand_l")),
(RightHand, Exact("hand_r")),
],
},
RigProfile {
name: "humanoid",
bindings: vec![
(Root, Exact("root")),
(Hips, Exact("humanoid_ Pelvis")),
(Spine, Exact("humanoid_ Spine")),
(Head, Exact("humanoid_ Head")),
(LeftFoot, Exact("humanoid_ L Foot")),
(RightFoot, Exact("humanoid_ R Foot")),
(LeftToe, Exact("humanoid_ L Toe0")),
(RightToe, Exact("humanoid_ R Toe0")),
(LeftHand, Exact("humanoid_ L Hand")),
(RightHand, Exact("humanoid_ R Hand")),
],
},
]
}
pub fn detect_profile(skeleton: &Skeleton) -> Option<ResolvedRoles> {
builtin_profiles()
.iter()
.map(|p| p.resolve(skeleton))
.filter(|r| r.len() >= 2)
.max_by_key(ResolvedRoles::len)
}
pub fn resolve_named(skeleton: &Skeleton, profile: &str) -> Option<ResolvedRoles> {
if profile == "auto" {
return detect_profile(skeleton);
}
builtin_profiles()
.iter()
.find(|p| p.name == profile)
.map(|p| p.resolve(skeleton))
}