1use crate::model::{BoneId, Skeleton};
8use serde::Deserialize;
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
13#[serde(rename_all = "snake_case")]
14#[non_exhaustive]
15pub enum Role {
16 Root,
18 Hips,
20 Spine,
22 Head,
24 LeftFoot,
26 RightFoot,
28 LeftToe,
30 RightToe,
32 LeftHand,
34 RightHand,
36}
37
38impl Role {
39 pub fn as_str(self) -> &'static str {
41 match self {
42 Role::Root => "root",
43 Role::Hips => "hips",
44 Role::Spine => "spine",
45 Role::Head => "head",
46 Role::LeftFoot => "left_foot",
47 Role::RightFoot => "right_foot",
48 Role::LeftToe => "left_toe",
49 Role::RightToe => "right_toe",
50 Role::LeftHand => "left_hand",
51 Role::RightHand => "right_hand",
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
59#[non_exhaustive]
60pub enum NameMatcher {
61 Exact(&'static str),
63}
64
65impl NameMatcher {
66 fn matches(&self, bone_name: &str) -> bool {
67 let NameMatcher::Exact(wanted) = self;
68 if bone_name == *wanted {
69 return true;
70 }
71 bone_name
73 .rsplit_once(':')
74 .is_some_and(|(_, stripped)| stripped == *wanted)
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct RigProfile {
81 pub name: &'static str,
83 pub bindings: Vec<(Role, NameMatcher)>,
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct ResolvedRoles {
90 pub profile: String,
93 map: BTreeMap<Role, BoneId>,
94}
95
96impl ResolvedRoles {
97 pub fn get(&self, role: Role) -> Option<BoneId> {
99 self.map.get(&role).copied()
100 }
101
102 pub fn len(&self) -> usize {
104 self.map.len()
105 }
106
107 pub fn is_empty(&self) -> bool {
109 self.map.is_empty()
110 }
111
112 pub fn iter(&self) -> impl Iterator<Item = (Role, BoneId)> + '_ {
114 self.map.iter().map(|(&r, &b)| (r, b))
115 }
116
117 pub fn from_names(
121 skeleton: &Skeleton,
122 names: impl IntoIterator<Item = (Role, String)>,
123 ) -> Self {
124 let mut map = BTreeMap::new();
125 for (role, name) in names {
126 if let Some(id) = skeleton.bones.iter().position(|b| b.name == name) {
127 map.insert(role, id);
128 }
129 }
130 Self {
131 profile: "custom".into(),
132 map,
133 }
134 }
135}
136
137impl RigProfile {
138 pub fn resolve(&self, skeleton: &Skeleton) -> ResolvedRoles {
140 let mut map = BTreeMap::new();
141 for (role, matcher) in &self.bindings {
142 if let Some(id) = skeleton.bones.iter().position(|b| matcher.matches(&b.name)) {
143 map.insert(*role, id);
144 }
145 }
146 ResolvedRoles {
147 profile: self.name.into(),
148 map,
149 }
150 }
151}
152
153pub fn builtin_profiles() -> Vec<RigProfile> {
155 use NameMatcher::Exact;
156 use Role::*;
157 vec![
158 RigProfile {
159 name: "mixamo",
160 bindings: vec![
161 (Hips, Exact("mixamorig:Hips")),
162 (Spine, Exact("mixamorig:Spine")),
163 (Head, Exact("mixamorig:Head")),
164 (LeftFoot, Exact("mixamorig:LeftFoot")),
165 (RightFoot, Exact("mixamorig:RightFoot")),
166 (LeftToe, Exact("mixamorig:LeftToeBase")),
167 (RightToe, Exact("mixamorig:RightToeBase")),
168 (LeftHand, Exact("mixamorig:LeftHand")),
169 (RightHand, Exact("mixamorig:RightHand")),
170 ],
171 },
172 RigProfile {
173 name: "ue-mannequin",
174 bindings: vec![
175 (Root, Exact("root")),
176 (Hips, Exact("pelvis")),
177 (Spine, Exact("spine_01")),
178 (Head, Exact("head")),
179 (LeftFoot, Exact("foot_l")),
180 (RightFoot, Exact("foot_r")),
181 (LeftToe, Exact("ball_l")),
182 (RightToe, Exact("ball_r")),
183 (LeftHand, Exact("hand_l")),
184 (RightHand, Exact("hand_r")),
185 ],
186 },
187 RigProfile {
188 name: "humanoid",
189 bindings: vec![
190 (Root, Exact("root")),
191 (Hips, Exact("humanoid_ Pelvis")),
192 (Spine, Exact("humanoid_ Spine")),
193 (Head, Exact("humanoid_ Head")),
194 (LeftFoot, Exact("humanoid_ L Foot")),
195 (RightFoot, Exact("humanoid_ R Foot")),
196 (LeftToe, Exact("humanoid_ L Toe0")),
197 (RightToe, Exact("humanoid_ R Toe0")),
198 (LeftHand, Exact("humanoid_ L Hand")),
199 (RightHand, Exact("humanoid_ R Hand")),
200 ],
201 },
202 ]
203}
204
205pub fn detect_profile(skeleton: &Skeleton) -> Option<ResolvedRoles> {
209 builtin_profiles()
210 .iter()
211 .map(|p| p.resolve(skeleton))
212 .filter(|r| r.len() >= 2)
213 .max_by_key(ResolvedRoles::len)
214}
215
216pub fn resolve_named(skeleton: &Skeleton, profile: &str) -> Option<ResolvedRoles> {
218 if profile == "auto" {
219 return detect_profile(skeleton);
220 }
221 builtin_profiles()
222 .iter()
223 .find(|p| p.name == profile)
224 .map(|p| p.resolve(skeleton))
225}