1use std::collections::HashMap;
10
11#[allow(dead_code)]
15#[derive(Debug, Clone, PartialEq)]
16pub enum BvhChannel {
17 Xposition,
18 Yposition,
19 Zposition,
20 Xrotation,
21 Yrotation,
22 Zrotation,
23}
24
25impl BvhChannel {
26 fn from_str(s: &str) -> Result<Self, String> {
27 match s {
28 "Xposition" => Ok(BvhChannel::Xposition),
29 "Yposition" => Ok(BvhChannel::Yposition),
30 "Zposition" => Ok(BvhChannel::Zposition),
31 "Xrotation" => Ok(BvhChannel::Xrotation),
32 "Yrotation" => Ok(BvhChannel::Yrotation),
33 "Zrotation" => Ok(BvhChannel::Zrotation),
34 _ => Err(format!("Unknown BVH channel: '{s}'")),
35 }
36 }
37
38 fn as_str(&self) -> &'static str {
39 match self {
40 BvhChannel::Xposition => "Xposition",
41 BvhChannel::Yposition => "Yposition",
42 BvhChannel::Zposition => "Zposition",
43 BvhChannel::Xrotation => "Xrotation",
44 BvhChannel::Yrotation => "Yrotation",
45 BvhChannel::Zrotation => "Zrotation",
46 }
47 }
48
49 #[allow(dead_code)]
51 pub fn is_translation(&self) -> bool {
52 matches!(
53 self,
54 BvhChannel::Xposition | BvhChannel::Yposition | BvhChannel::Zposition
55 )
56 }
57
58 #[allow(dead_code)]
60 pub fn is_rotation(&self) -> bool {
61 matches!(
62 self,
63 BvhChannel::Xrotation | BvhChannel::Yrotation | BvhChannel::Zrotation
64 )
65 }
66}
67
68#[allow(dead_code)]
72#[derive(Debug, Clone)]
73pub struct BvhJoint {
74 pub name: String,
76 pub offset: [f32; 3],
78 pub channels: Vec<BvhChannel>,
80 pub children: Vec<usize>,
82 pub parent: Option<usize>,
84}
85
86impl BvhJoint {
87 fn new(name: String, offset: [f32; 3], parent: Option<usize>) -> Self {
88 Self {
89 name,
90 offset,
91 channels: Vec::new(),
92 children: Vec::new(),
93 parent,
94 }
95 }
96}
97
98#[allow(dead_code)]
102#[derive(Debug, Clone)]
103pub struct BvhSkeleton {
104 pub joints: Vec<BvhJoint>,
106 pub root_index: usize,
108}
109
110impl BvhSkeleton {
111 #[allow(dead_code)]
113 pub fn joint_count(&self) -> usize {
114 self.joints.len()
115 }
116
117 #[allow(dead_code)]
119 pub fn find_joint(&self, name: &str) -> Option<usize> {
120 self.joints.iter().position(|j| j.name == name)
121 }
122
123 #[allow(dead_code)]
125 pub fn channel_count(&self) -> usize {
126 self.joints.iter().map(|j| j.channels.len()).sum()
127 }
128
129 #[allow(dead_code)]
131 pub fn joint_names(&self) -> Vec<&str> {
132 self.joints.iter().map(|j| j.name.as_str()).collect()
133 }
134
135 #[allow(dead_code)]
137 pub fn parent_of(&self, joint_idx: usize) -> Option<usize> {
138 self.joints.get(joint_idx)?.parent
139 }
140
141 #[allow(dead_code)]
143 pub fn children_of(&self, joint_idx: usize) -> &[usize] {
144 self.joints
145 .get(joint_idx)
146 .map(|j| j.children.as_slice())
147 .unwrap_or(&[])
148 }
149
150 #[allow(dead_code)]
152 pub fn channel_offset_for(&self, joint_idx: usize) -> usize {
153 self.joints[..joint_idx]
154 .iter()
155 .map(|j| j.channels.len())
156 .sum()
157 }
158}
159
160#[allow(dead_code)]
164#[derive(Debug, Clone)]
165pub struct BvhFrame {
166 pub values: Vec<f32>,
168}
169
170impl BvhFrame {
171 #[allow(dead_code)]
173 pub fn get_channels(&self, joint: &BvhJoint, joint_channel_offset: usize) -> &[f32] {
174 let len = joint.channels.len();
175 let end = (joint_channel_offset + len).min(self.values.len());
176 &self.values[joint_channel_offset..end]
177 }
178}
179
180#[allow(dead_code)]
184#[derive(Debug, Clone)]
185pub struct BvhFile {
186 pub skeleton: BvhSkeleton,
188 pub frames: Vec<BvhFrame>,
190 pub frame_time: f32,
192}
193
194impl BvhFile {
195 #[allow(dead_code)]
197 pub fn frame_count(&self) -> usize {
198 self.frames.len()
199 }
200
201 #[allow(dead_code)]
203 pub fn duration_seconds(&self) -> f32 {
204 self.frames.len() as f32 * self.frame_time
205 }
206
207 #[allow(dead_code)]
209 pub fn fps(&self) -> f32 {
210 if self.frame_time > 0.0 {
211 1.0 / self.frame_time
212 } else {
213 0.0
214 }
215 }
216
217 #[allow(dead_code)]
223 pub fn root_translation(&self, frame: usize) -> [f32; 3] {
224 let root = &self.skeleton.joints[self.skeleton.root_index];
225 let frame_data = &self.frames[frame];
226 let mut tx = 0.0_f32;
227 let mut ty = 0.0_f32;
228 let mut tz = 0.0_f32;
229 for (offset, ch) in root.channels.iter().enumerate() {
230 let v = frame_data.values.get(offset).copied().unwrap_or(0.0);
231 match ch {
232 BvhChannel::Xposition => tx = v,
233 BvhChannel::Yposition => ty = v,
234 BvhChannel::Zposition => tz = v,
235 _ => {}
236 }
237 }
238 [tx, ty, tz]
239 }
240
241 #[allow(dead_code)]
245 pub fn joint_rotation(&self, frame: usize, joint_idx: usize) -> [f32; 3] {
246 let joint = &self.skeleton.joints[joint_idx];
247 let ch_offset = self.skeleton.channel_offset_for(joint_idx);
248 let frame_data = &self.frames[frame];
249 let mut rx = 0.0_f32;
250 let mut ry = 0.0_f32;
251 let mut rz = 0.0_f32;
252 for (i, ch) in joint.channels.iter().enumerate() {
253 let v = frame_data.values.get(ch_offset + i).copied().unwrap_or(0.0);
254 match ch {
255 BvhChannel::Xrotation => rx = v,
256 BvhChannel::Yrotation => ry = v,
257 BvhChannel::Zrotation => rz = v,
258 _ => {}
259 }
260 }
261 [rx, ry, rz]
262 }
263
264 #[allow(dead_code)]
266 pub fn interpolate_frame(&self, frame_a: usize, frame_b: usize, t: f32) -> BvhFrame {
267 let a = &self.frames[frame_a].values;
268 let b = &self.frames[frame_b].values;
269 let len = a.len().min(b.len());
270 let values = (0..len).map(|i| a[i] + (b[i] - a[i]) * t).collect();
271 BvhFrame { values }
272 }
273
274 #[allow(dead_code)]
279 pub fn sample_at(&self, time_seconds: f32) -> BvhFrame {
280 if self.frames.is_empty() {
281 return BvhFrame { values: vec![] };
282 }
283 if self.frame_time <= 0.0 || self.frames.len() == 1 {
284 return self.frames[0].clone();
285 }
286 let total = self.frames.len() - 1;
287 let frame_f = (time_seconds / self.frame_time).max(0.0);
288 let frame_a = (frame_f.floor() as usize).min(total);
289 let frame_b = (frame_a + 1).min(total);
290 let t = frame_f.fract();
291 self.interpolate_frame(frame_a, frame_b, t)
292 }
293}
294
295#[allow(dead_code)]
301pub fn parse_bvh(content: &str) -> Result<BvhFile, String> {
302 let mut lines = content.lines().peekable();
303
304 loop {
307 match lines.peek() {
308 None => return Err("Unexpected end of file before HIERARCHY".into()),
309 Some(l) => {
310 if l.trim() == "HIERARCHY" {
311 lines.next();
312 break;
313 }
314 lines.next();
315 }
316 }
317 }
318
319 let mut joints: Vec<BvhJoint> = Vec::new();
321 let mut parent_stack: Vec<usize> = Vec::new();
323 let mut root_index: Option<usize> = None;
324 let mut end_site_depth: i32 = 0;
326 let mut in_end_site = false;
327
328 loop {
329 let raw = match lines.next() {
330 None => return Err("Unexpected end of file inside HIERARCHY".into()),
331 Some(l) => l,
332 };
333 let line = raw.trim();
334
335 if line == "MOTION" {
336 break;
337 }
338 if line.is_empty() {
339 continue;
340 }
341
342 if line.starts_with("End Site") {
344 in_end_site = true;
345 end_site_depth = 0;
346 continue;
347 }
348 if in_end_site {
349 if line == "{" {
350 end_site_depth += 1;
351 } else if line == "}" {
352 end_site_depth -= 1;
353 if end_site_depth == 0 {
354 in_end_site = false;
355 }
359 }
360 continue;
362 }
363
364 if line == "{" {
365 if let Some(&last) = joints.last().map(|_| joints.len() - 1).as_ref() {
367 parent_stack.push(last);
368 }
369 } else if line == "}" {
370 parent_stack.pop();
371 } else if let Some(rest) = line.strip_prefix("ROOT ") {
372 let name = rest.trim().to_string();
373 let idx = joints.len();
374 joints.push(BvhJoint::new(name, [0.0; 3], None));
375 root_index = Some(idx);
376 } else if let Some(rest) = line.strip_prefix("JOINT ") {
377 let name = rest.trim().to_string();
378 let idx = joints.len();
379 let parent = parent_stack.last().copied();
380 joints.push(BvhJoint::new(name, [0.0; 3], parent));
381 if let Some(p) = parent {
382 joints[p].children.push(idx);
383 }
384 } else if let Some(rest) = line.strip_prefix("OFFSET ") {
385 let nums: Vec<f32> = rest
386 .split_whitespace()
387 .filter_map(|s| s.parse().ok())
388 .collect();
389 if nums.len() < 3 {
390 return Err(format!("OFFSET line has fewer than 3 values: '{line}'"));
391 }
392 if let Some(&idx) = parent_stack.last() {
393 joints[idx].offset = [nums[0], nums[1], nums[2]];
394 }
395 } else if let Some(rest) = line.strip_prefix("CHANNELS ") {
396 let mut parts = rest.split_whitespace();
397 let count: usize = parts
398 .next()
399 .and_then(|s| s.parse().ok())
400 .ok_or_else(|| format!("Invalid CHANNELS count in: '{line}'"))?;
401 let mut channels = Vec::with_capacity(count);
402 for _ in 0..count {
403 let ch_str = parts
404 .next()
405 .ok_or_else(|| format!("Not enough channel names in: '{line}'"))?;
406 channels.push(BvhChannel::from_str(ch_str)?);
407 }
408 if let Some(&idx) = parent_stack.last() {
409 joints[idx].channels = channels;
410 }
411 }
412 }
413
414 let root_index = root_index.ok_or("No ROOT joint found in HIERARCHY")?;
415
416 let frames_line =
419 next_non_empty(&mut lines).ok_or("Missing 'Frames:' line in MOTION section")?;
420 let frames_line = frames_line.trim();
421 let frame_count: usize = frames_line
422 .strip_prefix("Frames:")
423 .or_else(|| frames_line.strip_prefix("Frames: "))
424 .and_then(|s| s.trim().parse().ok())
425 .ok_or_else(|| format!("Cannot parse frame count from: '{frames_line}'"))?;
426
427 let ftime_line =
429 next_non_empty(&mut lines).ok_or("Missing 'Frame Time:' line in MOTION section")?;
430 let ftime_line = ftime_line.trim();
431 let frame_time: f32 = ftime_line
432 .strip_prefix("Frame Time:")
433 .or_else(|| ftime_line.strip_prefix("Frame Time: "))
434 .and_then(|s| s.trim().parse().ok())
435 .ok_or_else(|| format!("Cannot parse frame time from: '{ftime_line}'"))?;
436
437 let channel_count: usize = joints.iter().map(|j| j.channels.len()).sum();
439
440 let mut frames: Vec<BvhFrame> = Vec::with_capacity(frame_count);
442 let mut remaining = frame_count;
443
444 let mut token_buf: Vec<f32> = Vec::new();
446
447 for raw in lines {
448 let line = raw.trim();
449 if line.is_empty() {
450 continue;
451 }
452 for tok in line.split_whitespace() {
453 match tok.parse::<f32>() {
454 Ok(v) => token_buf.push(v),
455 Err(_) => {
456 return Err(format!("Non-numeric token '{tok}' in MOTION data"));
457 }
458 }
459 if channel_count > 0 && token_buf.len() == channel_count {
460 frames.push(BvhFrame {
461 values: token_buf.clone(),
462 });
463 token_buf.clear();
464 remaining = remaining.saturating_sub(1);
465 }
466 }
467 if remaining == 0 {
468 break;
469 }
470 }
471
472 if channel_count == 0 {
474 for _ in 0..frame_count {
475 frames.push(BvhFrame { values: vec![] });
476 }
477 }
478
479 let skeleton = BvhSkeleton { joints, root_index };
480
481 Ok(BvhFile {
482 skeleton,
483 frames,
484 frame_time,
485 })
486}
487
488fn next_non_empty<'a, I: Iterator<Item = &'a str>>(iter: &mut I) -> Option<&'a str> {
490 iter.find(|l| !l.trim().is_empty())
491}
492
493#[allow(dead_code)]
497pub fn write_bvh(file: &BvhFile) -> String {
498 let mut out = String::new();
499 out.push_str("HIERARCHY\n");
500
501 write_joint_recursive(&file.skeleton, file.skeleton.root_index, 0, &mut out);
503
504 out.push_str("MOTION\n");
505 out.push_str(&format!("Frames: {}\n", file.frames.len()));
506 out.push_str(&format!("Frame Time: {:.6}\n", file.frame_time));
507
508 for frame in &file.frames {
509 let line: Vec<String> = frame.values.iter().map(|v| format!("{v:.6}")).collect();
510 out.push_str(&line.join(" "));
511 out.push('\n');
512 }
513
514 out
515}
516
517fn write_joint_recursive(skel: &BvhSkeleton, idx: usize, depth: usize, out: &mut String) {
518 let indent = " ".repeat(depth);
519 let joint = &skel.joints[idx];
520
521 if depth == 0 {
522 out.push_str(&format!("{indent}ROOT {}\n", joint.name));
523 } else {
524 out.push_str(&format!("{indent}JOINT {}\n", joint.name));
525 }
526 out.push_str(&format!("{indent}{{\n"));
527 out.push_str(&format!(
528 "{indent} OFFSET {:.2} {:.2} {:.2}\n",
529 joint.offset[0], joint.offset[1], joint.offset[2]
530 ));
531 if !joint.channels.is_empty() {
532 let ch_names: Vec<&str> = joint.channels.iter().map(|c| c.as_str()).collect();
533 out.push_str(&format!(
534 "{indent} CHANNELS {} {}\n",
535 joint.channels.len(),
536 ch_names.join(" ")
537 ));
538 }
539 if joint.children.is_empty() {
540 out.push_str(&format!("{indent} End Site\n"));
542 out.push_str(&format!("{indent} {{\n"));
543 out.push_str(&format!("{indent} OFFSET 0.00 0.00 0.00\n"));
544 out.push_str(&format!("{indent} }}\n"));
545 } else {
546 for &child_idx in &joint.children {
547 write_joint_recursive(skel, child_idx, depth + 1, out);
548 }
549 }
550 out.push_str(&format!("{indent}}}\n"));
551}
552
553#[allow(dead_code)]
560pub fn map_bvh_to_oxihuman(bvh: &BvhFile) -> HashMap<String, String> {
561 let table: &[(&str, &str)] = &[
563 ("Hips", "pelvis"),
565 ("Spine", "spine_01"),
566 ("Spine1", "spine_02"),
567 ("Spine2", "spine_03"),
568 ("Neck", "neck_01"),
569 ("Neck1", "neck_02"),
570 ("Head", "head"),
571 ("LeftShoulder", "clavicle_l"),
573 ("LeftArm", "upperarm_l"),
574 ("LeftForeArm", "lowerarm_l"),
575 ("LeftHand", "hand_l"),
576 ("RightShoulder", "clavicle_r"),
578 ("RightArm", "upperarm_r"),
579 ("RightForeArm", "lowerarm_r"),
580 ("RightHand", "hand_r"),
581 ("LeftUpLeg", "thigh_l"),
583 ("LeftLeg", "calf_l"),
584 ("LeftFoot", "foot_l"),
585 ("LeftToeBase", "ball_l"),
586 ("RightUpLeg", "thigh_r"),
588 ("RightLeg", "calf_r"),
589 ("RightFoot", "foot_r"),
590 ("RightToeBase", "ball_r"),
591 ("LeftHandThumb1", "thumb_01_l"),
593 ("LeftHandThumb2", "thumb_02_l"),
594 ("LeftHandThumb3", "thumb_03_l"),
595 ("LeftHandIndex1", "index_01_l"),
596 ("LeftHandIndex2", "index_02_l"),
597 ("LeftHandIndex3", "index_03_l"),
598 ("LeftHandMiddle1", "middle_01_l"),
599 ("LeftHandMiddle2", "middle_02_l"),
600 ("LeftHandMiddle3", "middle_03_l"),
601 ("LeftHandRing1", "ring_01_l"),
602 ("LeftHandRing2", "ring_02_l"),
603 ("LeftHandRing3", "ring_03_l"),
604 ("LeftHandPinky1", "pinky_01_l"),
605 ("LeftHandPinky2", "pinky_02_l"),
606 ("LeftHandPinky3", "pinky_03_l"),
607 ("RightHandThumb1", "thumb_01_r"),
609 ("RightHandThumb2", "thumb_02_r"),
610 ("RightHandThumb3", "thumb_03_r"),
611 ("RightHandIndex1", "index_01_r"),
612 ("RightHandIndex2", "index_02_r"),
613 ("RightHandIndex3", "index_03_r"),
614 ("RightHandMiddle1", "middle_01_r"),
615 ("RightHandMiddle2", "middle_02_r"),
616 ("RightHandMiddle3", "middle_03_r"),
617 ("RightHandRing1", "ring_01_r"),
618 ("RightHandRing2", "ring_02_r"),
619 ("RightHandRing3", "ring_03_r"),
620 ("RightHandPinky1", "pinky_01_r"),
621 ("RightHandPinky2", "pinky_02_r"),
622 ("RightHandPinky3", "pinky_03_r"),
623 ];
624
625 let lookup: HashMap<&str, &str> = table.iter().copied().collect();
626 let mut result = HashMap::new();
627
628 for joint in &bvh.skeleton.joints {
629 if let Some(&oxi_name) = lookup.get(joint.name.as_str()) {
630 result.insert(joint.name.clone(), oxi_name.to_string());
631 }
632 }
633
634 result
635}
636
637#[allow(dead_code)]
645pub fn retarget_scale(frame: &BvhFrame, scale: f32, translation_channels: usize) -> BvhFrame {
646 let values = frame
647 .values
648 .iter()
649 .enumerate()
650 .map(|(i, &v)| {
651 if i < translation_channels {
652 v * scale
653 } else {
654 v
655 }
656 })
657 .collect();
658 BvhFrame { values }
659}
660
661#[cfg(test)]
664mod tests {
665 use super::*;
666
667 fn minimal_bvh() -> &'static str {
669 "HIERARCHY
670ROOT Hips
671{
672 OFFSET 0.00 0.00 0.00
673 CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
674 JOINT Spine
675 {
676 OFFSET 0.00 5.21 0.00
677 CHANNELS 3 Zrotation Xrotation Yrotation
678 End Site
679 {
680 OFFSET 0.00 5.00 0.00
681 }
682 }
683}
684MOTION
685Frames: 2
686Frame Time: 0.033333
6870.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
6881.0 2.0 3.0 1.0 2.0 3.0 4.0 5.0 6.0
689"
690 }
691
692 #[test]
694 fn test_parse_minimal_bvh() {
695 let bvh = parse_bvh(minimal_bvh()).expect("parse failed");
696 assert_eq!(bvh.skeleton.joint_count(), 2);
697 assert_eq!(bvh.frame_count(), 2);
698 }
699
700 #[test]
702 fn test_joint_names() {
703 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
704 let names = bvh.skeleton.joint_names();
705 assert!(names.contains(&"Hips"));
706 assert!(names.contains(&"Spine"));
707 }
708
709 #[test]
711 fn test_channel_count() {
712 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
713 assert_eq!(bvh.skeleton.channel_count(), 9);
715 }
716
717 #[test]
719 fn test_fps() {
720 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
721 let fps = bvh.fps();
722 assert!((fps - 30.0).abs() < 0.5, "fps ≈ 30, got {fps}");
723 }
724
725 #[test]
727 fn test_duration() {
728 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
729 let dur = bvh.duration_seconds();
730 assert!(dur > 0.0);
731 assert!((dur - 2.0 * 0.033333_f32).abs() < 1e-4);
732 }
733
734 #[test]
736 fn test_root_translation_frame0() {
737 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
738 let t = bvh.root_translation(0);
739 assert_eq!(t, [0.0, 0.0, 0.0]);
740 }
741
742 #[test]
743 fn test_root_translation_frame1() {
744 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
745 let t = bvh.root_translation(1);
746 assert_eq!(t, [1.0, 2.0, 3.0]);
747 }
748
749 #[test]
751 fn test_joint_rotation_spine_frame1() {
752 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
753 let spine_idx = bvh.skeleton.find_joint("Spine").expect("Spine not found");
754 let rot = bvh.joint_rotation(1, spine_idx);
755 assert_eq!(rot[0], 5.0); assert_eq!(rot[2], 4.0); }
759
760 #[test]
762 fn test_interpolate_frame_midpoint() {
763 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
764 let mid = bvh.interpolate_frame(0, 1, 0.5);
765 assert!((mid.values[0] - 0.5).abs() < 1e-5);
766 assert!((mid.values[1] - 1.0).abs() < 1e-5);
767 }
768
769 #[test]
771 fn test_sample_at_beginning() {
772 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
773 let f = bvh.sample_at(0.0);
774 assert_eq!(f.values, bvh.frames[0].values);
775 }
776
777 #[test]
779 fn test_write_bvh_round_trip() {
780 let original = parse_bvh(minimal_bvh()).expect("should succeed");
781 let text = write_bvh(&original);
782 let reparsed = parse_bvh(&text).expect("re-parse failed");
783 assert_eq!(
784 reparsed.skeleton.joint_count(),
785 original.skeleton.joint_count()
786 );
787 assert_eq!(reparsed.frame_count(), original.frame_count());
788 }
789
790 #[test]
792 fn test_retarget_scale() {
793 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
794 let frame1 = bvh.frames[1].clone();
795 let scaled = retarget_scale(&frame1, 2.0, 3);
796 assert!((scaled.values[0] - 2.0).abs() < 1e-5); assert!((scaled.values[1] - 4.0).abs() < 1e-5); assert!((scaled.values[3] - 1.0).abs() < 1e-5); }
800
801 #[test]
803 fn test_map_bvh_to_oxihuman() {
804 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
805 let map = map_bvh_to_oxihuman(&bvh);
806 assert_eq!(map.get("Hips").map(|s| s.as_str()), Some("pelvis"));
807 assert_eq!(map.get("Spine").map(|s| s.as_str()), Some("spine_01"));
808 }
809
810 #[test]
812 fn test_find_joint() {
813 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
814 assert!(bvh.skeleton.find_joint("Hips").is_some());
815 assert!(bvh.skeleton.find_joint("DoesNotExist").is_none());
816 }
817
818 #[test]
820 fn test_parent_children() {
821 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
822 let hips_idx = bvh.skeleton.find_joint("Hips").expect("should succeed");
823 let spine_idx = bvh.skeleton.find_joint("Spine").expect("should succeed");
824 assert_eq!(bvh.skeleton.parent_of(hips_idx), None);
825 assert_eq!(bvh.skeleton.parent_of(spine_idx), Some(hips_idx));
826 assert!(bvh.skeleton.children_of(hips_idx).contains(&spine_idx));
827 }
828
829 #[test]
831 fn test_channel_helpers() {
832 assert!(BvhChannel::Xposition.is_translation());
833 assert!(!BvhChannel::Xposition.is_rotation());
834 assert!(BvhChannel::Yrotation.is_rotation());
835 assert!(!BvhChannel::Yrotation.is_translation());
836 }
837
838 #[test]
840 fn test_write_to_tmp() {
841 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
842 let text = write_bvh(&bvh);
843 std::fs::write("/tmp/test_mocap_bvh_output.bvh", &text)
844 .expect("failed to write /tmp/test_mocap_bvh_output.bvh");
845 assert!(text.contains("HIERARCHY"));
846 assert!(text.contains("MOTION"));
847 }
848
849 #[test]
851 fn test_sample_at_beyond_end() {
852 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
853 let f = bvh.sample_at(9999.0);
855 assert_eq!(f.values.len(), bvh.frames[0].values.len());
856 }
857
858 #[test]
860 fn test_get_channels() {
861 let bvh = parse_bvh(minimal_bvh()).expect("should succeed");
862 let hips_idx = bvh.skeleton.find_joint("Hips").expect("should succeed");
863 let hips = &bvh.skeleton.joints[hips_idx];
864 let ch_offset = bvh.skeleton.channel_offset_for(hips_idx);
865 let frame0 = &bvh.frames[0];
866 let ch = frame0.get_channels(hips, ch_offset);
867 assert_eq!(ch.len(), 6);
868 }
869}