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(crate) fn get_with_name(&self, role: Role) -> Option<(BoneId, &str)> {
112 self.map
113 .get(&role)
114 .map(|bone| (bone.id, bone.name.as_str()))
115 }
116
117 pub fn len(&self) -> usize {
119 self.map.len()
120 }
121
122 pub fn is_empty(&self) -> bool {
124 self.map.is_empty()
125 }
126
127 pub fn iter(&self) -> impl Iterator<Item = (Role, BoneId)> + '_ {
129 self.map.iter().map(|(&role, bone)| (role, bone.id))
130 }
131
132 pub(crate) fn iter_with_names(&self) -> impl Iterator<Item = (Role, BoneId, &str)> + '_ {
133 self.map
134 .iter()
135 .map(|(&role, bone)| (role, bone.id, bone.name.as_str()))
136 }
137
138 pub fn from_names(
142 skeleton: &Skeleton,
143 names: impl IntoIterator<Item = (Role, String)>,
144 ) -> Self {
145 let mut map = BTreeMap::new();
146 for (role, name) in names {
147 if let Some(id) = skeleton.bones.iter().position(|b| b.name == name) {
148 map.insert(
149 role,
150 ResolvedBone {
151 id,
152 name: skeleton.bones[id].name.clone(),
153 },
154 );
155 }
156 }
157 Self {
158 profile: "custom".into(),
159 map,
160 }
161 }
162}
163
164impl RigProfile {
165 pub fn resolve(&self, skeleton: &Skeleton) -> ResolvedRoles {
167 let mut map = BTreeMap::new();
168 for (role, matcher) in &self.bindings {
169 if let Some(id) = skeleton.bones.iter().position(|b| matcher.matches(&b.name)) {
170 map.insert(
171 *role,
172 ResolvedBone {
173 id,
174 name: skeleton.bones[id].name.clone(),
175 },
176 );
177 }
178 }
179 ResolvedRoles {
180 profile: self.name.into(),
181 map,
182 }
183 }
184}
185
186pub fn builtin_profiles() -> Vec<RigProfile> {
188 use NameMatcher::Exact;
189 use Role::*;
190 vec![
191 RigProfile {
192 name: "mixamo",
193 bindings: vec![
194 (Hips, Exact("mixamorig:Hips")),
195 (Spine, Exact("mixamorig:Spine")),
196 (Head, Exact("mixamorig:Head")),
197 (LeftFoot, Exact("mixamorig:LeftFoot")),
198 (RightFoot, Exact("mixamorig:RightFoot")),
199 (LeftToe, Exact("mixamorig:LeftToeBase")),
200 (RightToe, Exact("mixamorig:RightToeBase")),
201 (LeftHand, Exact("mixamorig:LeftHand")),
202 (RightHand, Exact("mixamorig:RightHand")),
203 ],
204 },
205 RigProfile {
206 name: "ue-mannequin",
207 bindings: vec![
208 (Root, Exact("root")),
209 (Hips, Exact("pelvis")),
210 (Spine, Exact("spine_01")),
211 (Head, Exact("head")),
212 (LeftFoot, Exact("foot_l")),
213 (RightFoot, Exact("foot_r")),
214 (LeftToe, Exact("ball_l")),
215 (RightToe, Exact("ball_r")),
216 (LeftHand, Exact("hand_l")),
217 (RightHand, Exact("hand_r")),
218 ],
219 },
220 RigProfile {
221 name: "humanoid",
222 bindings: vec![
223 (Root, Exact("root")),
224 (Hips, Exact("humanoid_ Pelvis")),
225 (Spine, Exact("humanoid_ Spine")),
226 (Head, Exact("humanoid_ Head")),
227 (LeftFoot, Exact("humanoid_ L Foot")),
228 (RightFoot, Exact("humanoid_ R Foot")),
229 (LeftToe, Exact("humanoid_ L Toe0")),
230 (RightToe, Exact("humanoid_ R Toe0")),
231 (LeftHand, Exact("humanoid_ L Hand")),
232 (RightHand, Exact("humanoid_ R Hand")),
233 ],
234 },
235 ]
236}
237
238pub fn detect_profile(skeleton: &Skeleton) -> Option<ResolvedRoles> {
242 builtin_profiles()
243 .iter()
244 .map(|p| p.resolve(skeleton))
245 .filter(|r| r.len() >= 2)
246 .max_by_key(ResolvedRoles::len)
247}
248
249pub fn resolve_named(skeleton: &Skeleton, profile: &str) -> Option<ResolvedRoles> {
251 if profile == "auto" {
252 return detect_profile(skeleton);
253 }
254 builtin_profiles()
255 .iter()
256 .find(|p| p.name == profile)
257 .map(|p| p.resolve(skeleton))
258}
259
260pub fn resolve_configured_roles(skeleton: &Skeleton, rig: &RigConfig) -> ResolvedRoles {
267 let base = resolve_named(skeleton, &rig.profile).unwrap_or_default();
268 let base_contributed = !base.is_empty();
269 let inline_contributed = rig
270 .roles
271 .values()
272 .any(|name| skeleton.bones.iter().any(|bone| bone.name == *name));
273
274 let mut pairs: Vec<_> = base
275 .iter_with_names()
276 .map(|(role, _, name)| (role, name.to_owned()))
277 .collect();
278 pairs.extend(rig.roles.iter().map(|(role, name)| (*role, name.clone())));
279
280 let mut resolved = ResolvedRoles::from_names(skeleton, pairs);
281 resolved.profile = match (base_contributed, inline_contributed) {
282 (false, false) => "unknown".into(),
283 (false, true) => "custom".into(),
284 (true, false) => base.profile,
285 (true, true) => format!("{}+custom", base.profile),
286 };
287 resolved
288}