1use crate::config::RigConfig;
8use crate::model::{BoneId, Skeleton};
9use serde::Deserialize;
10use std::collections::BTreeMap;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
14#[serde(rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum Role {
17 Root,
19 Hips,
21 Spine,
23 Head,
25 LeftFoot,
27 RightFoot,
29 LeftToe,
31 RightToe,
33 LeftHand,
35 RightHand,
37}
38
39impl Role {
40 pub fn as_str(self) -> &'static str {
42 match self {
43 Role::Root => "root",
44 Role::Hips => "hips",
45 Role::Spine => "spine",
46 Role::Head => "head",
47 Role::LeftFoot => "left_foot",
48 Role::RightFoot => "right_foot",
49 Role::LeftToe => "left_toe",
50 Role::RightToe => "right_toe",
51 Role::LeftHand => "left_hand",
52 Role::RightHand => "right_hand",
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
60#[non_exhaustive]
61pub enum NameMatcher {
62 Exact(&'static str),
64}
65
66impl NameMatcher {
67 fn matches(&self, bone_name: &str) -> bool {
68 let NameMatcher::Exact(wanted) = self;
69 if bone_name == *wanted {
70 return true;
71 }
72 bone_name
74 .rsplit_once(':')
75 .is_some_and(|(_, stripped)| stripped == *wanted)
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct RigProfile {
82 pub name: &'static str,
84 pub bindings: Vec<(Role, NameMatcher)>,
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct ResolvedRoles {
91 pub profile: String,
94 map: BTreeMap<Role, ResolvedBone>,
95}
96
97#[derive(Debug, Clone)]
98struct ResolvedBone {
99 id: BoneId,
100 name: String,
101}
102
103impl ResolvedRoles {
104 pub fn get(&self, role: Role) -> Option<BoneId> {
106 self.map.get(&role).map(|bone| bone.id)
107 }
108
109 pub fn len(&self) -> usize {
111 self.map.len()
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.map.is_empty()
117 }
118
119 pub fn iter(&self) -> impl Iterator<Item = (Role, BoneId)> + '_ {
121 self.map.iter().map(|(&role, bone)| (role, bone.id))
122 }
123
124 pub(crate) fn iter_with_names(&self) -> impl Iterator<Item = (Role, BoneId, &str)> + '_ {
125 self.map
126 .iter()
127 .map(|(&role, bone)| (role, bone.id, bone.name.as_str()))
128 }
129
130 pub fn from_names(
134 skeleton: &Skeleton,
135 names: impl IntoIterator<Item = (Role, String)>,
136 ) -> Self {
137 let mut map = BTreeMap::new();
138 for (role, name) in names {
139 if let Some(id) = skeleton.bones.iter().position(|b| b.name == name) {
140 map.insert(
141 role,
142 ResolvedBone {
143 id,
144 name: skeleton.bones[id].name.clone(),
145 },
146 );
147 }
148 }
149 Self {
150 profile: "custom".into(),
151 map,
152 }
153 }
154}
155
156impl RigProfile {
157 pub fn resolve(&self, skeleton: &Skeleton) -> ResolvedRoles {
159 let mut map = BTreeMap::new();
160 for (role, matcher) in &self.bindings {
161 if let Some(id) = skeleton.bones.iter().position(|b| matcher.matches(&b.name)) {
162 map.insert(
163 *role,
164 ResolvedBone {
165 id,
166 name: skeleton.bones[id].name.clone(),
167 },
168 );
169 }
170 }
171 ResolvedRoles {
172 profile: self.name.into(),
173 map,
174 }
175 }
176}
177
178pub fn builtin_profiles() -> Vec<RigProfile> {
180 use NameMatcher::Exact;
181 use Role::*;
182 vec![
183 RigProfile {
184 name: "mixamo",
185 bindings: vec![
186 (Hips, Exact("mixamorig:Hips")),
187 (Spine, Exact("mixamorig:Spine")),
188 (Head, Exact("mixamorig:Head")),
189 (LeftFoot, Exact("mixamorig:LeftFoot")),
190 (RightFoot, Exact("mixamorig:RightFoot")),
191 (LeftToe, Exact("mixamorig:LeftToeBase")),
192 (RightToe, Exact("mixamorig:RightToeBase")),
193 (LeftHand, Exact("mixamorig:LeftHand")),
194 (RightHand, Exact("mixamorig:RightHand")),
195 ],
196 },
197 RigProfile {
198 name: "ue-mannequin",
199 bindings: vec![
200 (Root, Exact("root")),
201 (Hips, Exact("pelvis")),
202 (Spine, Exact("spine_01")),
203 (Head, Exact("head")),
204 (LeftFoot, Exact("foot_l")),
205 (RightFoot, Exact("foot_r")),
206 (LeftToe, Exact("ball_l")),
207 (RightToe, Exact("ball_r")),
208 (LeftHand, Exact("hand_l")),
209 (RightHand, Exact("hand_r")),
210 ],
211 },
212 RigProfile {
213 name: "humanoid",
214 bindings: vec![
215 (Root, Exact("root")),
216 (Hips, Exact("humanoid_ Pelvis")),
217 (Spine, Exact("humanoid_ Spine")),
218 (Head, Exact("humanoid_ Head")),
219 (LeftFoot, Exact("humanoid_ L Foot")),
220 (RightFoot, Exact("humanoid_ R Foot")),
221 (LeftToe, Exact("humanoid_ L Toe0")),
222 (RightToe, Exact("humanoid_ R Toe0")),
223 (LeftHand, Exact("humanoid_ L Hand")),
224 (RightHand, Exact("humanoid_ R Hand")),
225 ],
226 },
227 ]
228}
229
230pub fn detect_profile(skeleton: &Skeleton) -> Option<ResolvedRoles> {
234 builtin_profiles()
235 .iter()
236 .map(|p| p.resolve(skeleton))
237 .filter(|r| r.len() >= 2)
238 .max_by_key(ResolvedRoles::len)
239}
240
241pub fn resolve_named(skeleton: &Skeleton, profile: &str) -> Option<ResolvedRoles> {
243 if profile == "auto" {
244 return detect_profile(skeleton);
245 }
246 builtin_profiles()
247 .iter()
248 .find(|p| p.name == profile)
249 .map(|p| p.resolve(skeleton))
250}
251
252pub fn resolve_configured_roles(skeleton: &Skeleton, rig: &RigConfig) -> ResolvedRoles {
259 let base = resolve_named(skeleton, &rig.profile).unwrap_or_default();
260 let base_contributed = !base.is_empty();
261 let inline_contributed = rig
262 .roles
263 .values()
264 .any(|name| skeleton.bones.iter().any(|bone| bone.name == *name));
265
266 let mut pairs: Vec<_> = base
267 .iter_with_names()
268 .map(|(role, _, name)| (role, name.to_owned()))
269 .collect();
270 pairs.extend(rig.roles.iter().map(|(role, name)| (*role, name.clone())));
271
272 let mut resolved = ResolvedRoles::from_names(skeleton, pairs);
273 resolved.profile = match (base_contributed, inline_contributed) {
274 (false, false) => "unknown".into(),
275 (false, true) => "custom".into(),
276 (true, false) => base.profile,
277 (true, true) => format!("{}+custom", base.profile),
278 };
279 resolved
280}