1#[allow(dead_code)]
13#[derive(Debug, Clone)]
14pub struct RigExportBone {
15 pub id: u32,
17 pub name: String,
19 pub parent_id: Option<u32>,
21 pub head: [f32; 3],
23 pub tail: [f32; 3],
25 pub rotation: [f32; 4],
27 pub length: f32,
29 pub bind_pose: ([f32; 3], [f32; 4]),
31}
32
33#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct ExportRig {
37 pub name: String,
39 pub bones: Vec<RigExportBone>,
41}
42
43#[allow(dead_code)]
45#[derive(Debug, Clone)]
46pub struct RigExportConfig {
47 pub include_bind_pose: bool,
49 pub precision: u32,
51 pub name_filter_prefix: String,
53}
54
55pub type RigValidationResult = Result<(), String>;
59
60#[allow(dead_code)]
64pub fn default_rig_export_config() -> RigExportConfig {
65 RigExportConfig {
66 include_bind_pose: true,
67 precision: 6,
68 name_filter_prefix: String::new(),
69 }
70}
71
72#[allow(dead_code)]
76pub fn new_export_rig(name: &str) -> ExportRig {
77 ExportRig {
78 name: name.to_string(),
79 bones: Vec::new(),
80 }
81}
82
83#[allow(dead_code)]
85pub fn add_bone(rig: &mut ExportRig, bone: RigExportBone) {
86 rig.bones.push(bone);
87}
88
89#[allow(dead_code)]
92pub fn remove_bone(rig: &mut ExportRig, id: u32) -> bool {
93 let before = rig.bones.len();
94 rig.bones.retain(|b| b.id != id);
95 rig.bones.len() < before
96}
97
98#[allow(dead_code)]
100pub fn bone_count(rig: &ExportRig) -> usize {
101 rig.bones.len()
102}
103
104#[allow(dead_code)]
108pub fn rig_root_bones(rig: &ExportRig) -> Vec<&RigExportBone> {
109 rig.bones.iter().filter(|b| b.parent_id.is_none()).collect()
110}
111
112#[allow(dead_code)]
114pub fn find_bone_by_name<'a>(rig: &'a ExportRig, name: &str) -> Option<&'a RigExportBone> {
115 rig.bones.iter().find(|b| b.name == name)
116}
117
118#[allow(dead_code)]
122pub fn bone_chain(rig: &ExportRig, start_id: u32) -> Vec<&RigExportBone> {
123 let mut result = Vec::new();
124 let mut current_id = Some(start_id);
125 let mut visited = std::collections::HashSet::new();
126 while let Some(cid) = current_id {
127 if !visited.insert(cid) {
128 break; }
130 if let Some(bone) = rig.bones.iter().find(|b| b.id == cid) {
131 result.push(bone);
132 current_id = bone.parent_id;
133 } else {
134 break;
135 }
136 }
137 result
138}
139
140#[allow(dead_code)]
143pub fn rig_depth(rig: &ExportRig) -> usize {
144 fn depth_of(rig: &ExportRig, id: u32, visited: &mut std::collections::HashSet<u32>) -> usize {
145 if !visited.insert(id) {
146 return 0;
147 }
148 let children: Vec<u32> = rig
149 .bones
150 .iter()
151 .filter(|b| b.parent_id == Some(id))
152 .map(|b| b.id)
153 .collect();
154 if children.is_empty() {
155 return 1;
156 }
157 1 + children
158 .into_iter()
159 .map(|cid| depth_of(rig, cid, visited))
160 .max()
161 .unwrap_or(0)
162 }
163 rig_root_bones(rig)
164 .iter()
165 .map(|r| depth_of(rig, r.id, &mut std::collections::HashSet::new()))
166 .max()
167 .unwrap_or(0)
168}
169
170#[allow(dead_code)]
173pub fn validate_rig(rig: &ExportRig) -> RigValidationResult {
174 let ids: std::collections::HashSet<u32> = rig.bones.iter().map(|b| b.id).collect();
176 for bone in &rig.bones {
177 if let Some(pid) = bone.parent_id {
178 if !ids.contains(&pid) {
179 return Err(format!(
180 "bone '{}' references non-existent parent id {}",
181 bone.name, pid
182 ));
183 }
184 }
185 }
186 for bone in &rig.bones {
188 let mut visited = std::collections::HashSet::new();
189 let mut cur = bone.parent_id;
190 while let Some(pid) = cur {
191 if !visited.insert(pid) {
192 return Err(format!("cycle detected involving bone id {pid}"));
193 }
194 cur = rig
195 .bones
196 .iter()
197 .find(|b| b.id == pid)
198 .and_then(|b| b.parent_id);
199 }
200 }
201 Ok(())
202}
203
204#[allow(dead_code)]
206pub fn total_bone_length(rig: &ExportRig) -> f32 {
207 rig.bones.iter().map(|b| b.length).sum()
208}
209
210#[allow(dead_code)]
213pub fn set_bone_bind_pose(rig: &mut ExportRig, id: u32, pos: [f32; 3], rot: [f32; 4]) -> bool {
214 if let Some(bone) = rig.bones.iter_mut().find(|b| b.id == id) {
215 bone.bind_pose = (pos, rot);
216 true
217 } else {
218 false
219 }
220}
221
222#[allow(dead_code)]
226pub fn rig_to_json(rig: &ExportRig) -> String {
227 let bone_strs: Vec<String> = rig
228 .bones
229 .iter()
230 .map(|b| {
231 let parent = match b.parent_id {
232 Some(p) => format!("{p}"),
233 None => "null".to_string(),
234 };
235 let (bp, br) = b.bind_pose;
236 format!(
237 r#"{{"id":{},"name":"{}","parent_id":{},"head":[{},{},{}],"tail":[{},{},{}],"rotation":[{},{},{},{}],"length":{},"bind_pose":{{"pos":[{},{},{}],"rot":[{},{},{},{}]}}}}"#,
238 b.id,
239 b.name,
240 parent,
241 b.head[0], b.head[1], b.head[2],
242 b.tail[0], b.tail[1], b.tail[2],
243 b.rotation[0], b.rotation[1], b.rotation[2], b.rotation[3],
244 b.length,
245 bp[0], bp[1], bp[2],
246 br[0], br[1], br[2], br[3],
247 )
248 })
249 .collect();
250 format!(
251 r#"{{"name":"{}","bones":[{}]}}"#,
252 rig.name,
253 bone_strs.join(",")
254 )
255}
256
257#[allow(dead_code)]
259pub fn rig_to_csv(rig: &ExportRig) -> String {
260 let mut out = String::from("id,name,parent_id,head_x,head_y,head_z,tail_x,tail_y,tail_z,rot_x,rot_y,rot_z,rot_w,length\n");
261 for b in &rig.bones {
262 let parent = match b.parent_id {
263 Some(p) => format!("{p}"),
264 None => "".to_string(),
265 };
266 out.push_str(&format!(
267 "{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
268 b.id,
269 b.name,
270 parent,
271 b.head[0],
272 b.head[1],
273 b.head[2],
274 b.tail[0],
275 b.tail[1],
276 b.tail[2],
277 b.rotation[0],
278 b.rotation[1],
279 b.rotation[2],
280 b.rotation[3],
281 b.length,
282 ));
283 }
284 out
285}
286
287#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn make_bone(id: u32, name: &str, parent: Option<u32>) -> RigExportBone {
294 RigExportBone {
295 id,
296 name: name.to_string(),
297 parent_id: parent,
298 head: [0.0, f32::from(id as u8), 0.0],
299 tail: [0.0, f32::from(id as u8) + 1.0, 0.0],
300 rotation: [0.0, 0.0, 0.0, 1.0],
301 length: 1.0,
302 bind_pose: ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]),
303 }
304 }
305
306 #[test]
307 fn test_default_rig_export_config() {
308 let cfg = default_rig_export_config();
309 assert!(cfg.include_bind_pose);
310 assert_eq!(cfg.precision, 6);
311 assert!(cfg.name_filter_prefix.is_empty());
312 }
313
314 #[test]
315 fn test_new_export_rig() {
316 let rig = new_export_rig("human");
317 assert_eq!(rig.name, "human");
318 assert!(rig.bones.is_empty());
319 }
320
321 #[test]
322 fn test_add_bone() {
323 let mut rig = new_export_rig("r");
324 add_bone(&mut rig, make_bone(0, "root", None));
325 assert_eq!(bone_count(&rig), 1);
326 }
327
328 #[test]
329 fn test_remove_bone_found() {
330 let mut rig = new_export_rig("r");
331 add_bone(&mut rig, make_bone(0, "root", None));
332 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
333 let removed = remove_bone(&mut rig, 1);
334 assert!(removed);
335 assert_eq!(bone_count(&rig), 1);
336 }
337
338 #[test]
339 fn test_remove_bone_not_found() {
340 let mut rig = new_export_rig("r");
341 add_bone(&mut rig, make_bone(0, "root", None));
342 let removed = remove_bone(&mut rig, 99);
343 assert!(!removed);
344 assert_eq!(bone_count(&rig), 1);
345 }
346
347 #[test]
348 fn test_bone_count_empty() {
349 let rig = new_export_rig("r");
350 assert_eq!(bone_count(&rig), 0);
351 }
352
353 #[test]
354 fn test_rig_root_bones() {
355 let mut rig = new_export_rig("r");
356 add_bone(&mut rig, make_bone(0, "hip", None));
357 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
358 add_bone(&mut rig, make_bone(2, "neck", None));
359 let roots = rig_root_bones(&rig);
360 assert_eq!(roots.len(), 2);
361 }
362
363 #[test]
364 fn test_find_bone_by_name_found() {
365 let mut rig = new_export_rig("r");
366 add_bone(&mut rig, make_bone(0, "hip", None));
367 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
368 let bone = find_bone_by_name(&rig, "spine");
369 assert!(bone.is_some());
370 assert_eq!(bone.expect("should succeed").id, 1);
371 }
372
373 #[test]
374 fn test_find_bone_by_name_missing() {
375 let rig = new_export_rig("r");
376 assert!(find_bone_by_name(&rig, "missing").is_none());
377 }
378
379 #[test]
380 fn test_bone_chain_single_root() {
381 let mut rig = new_export_rig("r");
382 add_bone(&mut rig, make_bone(0, "hip", None));
383 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
384 add_bone(&mut rig, make_bone(2, "chest", Some(1)));
385 let chain = bone_chain(&rig, 2);
386 assert_eq!(chain.len(), 3);
387 assert_eq!(chain[0].id, 2);
388 assert_eq!(chain[1].id, 1);
389 assert_eq!(chain[2].id, 0);
390 }
391
392 #[test]
393 fn test_bone_chain_nonexistent() {
394 let rig = new_export_rig("r");
395 let chain = bone_chain(&rig, 99);
396 assert!(chain.is_empty());
397 }
398
399 #[test]
400 fn test_rig_depth_empty() {
401 let rig = new_export_rig("r");
402 assert_eq!(rig_depth(&rig), 0);
403 }
404
405 #[test]
406 fn test_rig_depth_three_levels() {
407 let mut rig = new_export_rig("r");
408 add_bone(&mut rig, make_bone(0, "hip", None));
409 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
410 add_bone(&mut rig, make_bone(2, "chest", Some(1)));
411 assert_eq!(rig_depth(&rig), 3);
412 }
413
414 #[test]
415 fn test_validate_rig_ok() {
416 let mut rig = new_export_rig("r");
417 add_bone(&mut rig, make_bone(0, "hip", None));
418 add_bone(&mut rig, make_bone(1, "spine", Some(0)));
419 assert!(validate_rig(&rig).is_ok());
420 }
421
422 #[test]
423 fn test_validate_rig_bad_parent() {
424 let mut rig = new_export_rig("r");
425 add_bone(&mut rig, make_bone(5, "orphan", Some(99)));
426 assert!(validate_rig(&rig).is_err());
427 }
428
429 #[test]
430 fn test_total_bone_length() {
431 let mut rig = new_export_rig("r");
432 let mut b0 = make_bone(0, "hip", None);
433 b0.length = 2.0;
434 let mut b1 = make_bone(1, "spine", Some(0));
435 b1.length = 3.0;
436 add_bone(&mut rig, b0);
437 add_bone(&mut rig, b1);
438 assert!((total_bone_length(&rig) - 5.0).abs() < 1e-5);
439 }
440
441 #[test]
442 fn test_set_bone_bind_pose_found() {
443 let mut rig = new_export_rig("r");
444 add_bone(&mut rig, make_bone(0, "hip", None));
445 let ok = set_bone_bind_pose(&mut rig, 0, [1.0, 2.0, 3.0], [0.0, 0.0, 0.0, 1.0]);
446 assert!(ok);
447 assert_eq!(rig.bones[0].bind_pose.0, [1.0, 2.0, 3.0]);
448 }
449
450 #[test]
451 fn test_set_bone_bind_pose_not_found() {
452 let mut rig = new_export_rig("r");
453 let ok = set_bone_bind_pose(&mut rig, 99, [0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 1.0]);
454 assert!(!ok);
455 }
456
457 #[test]
458 fn test_rig_to_json_nonempty() {
459 let mut rig = new_export_rig("test_rig");
460 add_bone(&mut rig, make_bone(0, "hip", None));
461 let json = rig_to_json(&rig);
462 assert!(!json.is_empty());
463 assert!(json.contains("test_rig"));
464 assert!(json.contains("hip"));
465 }
466
467 #[test]
468 fn test_rig_to_csv_has_header() {
469 let mut rig = new_export_rig("r");
470 add_bone(&mut rig, make_bone(0, "hip", None));
471 let csv = rig_to_csv(&rig);
472 assert!(csv.starts_with("id,name"));
473 assert!(csv.contains("hip"));
474 }
475}