Skip to main content

oxihuman_export/
capsule_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5use std::f32::consts::PI;
6
7/// Export capsule collision/proxy shape data.
8#[allow(dead_code)]
9pub struct CapsuleExport {
10    pub name: String,
11    pub center: [f32; 3],
12    pub axis: [f32; 3],
13    pub radius: f32,
14    pub half_height: f32,
15}
16
17#[allow(dead_code)]
18pub struct CapsuleBundle {
19    pub capsules: Vec<CapsuleExport>,
20}
21
22#[allow(dead_code)]
23pub fn new_capsule_bundle() -> CapsuleBundle {
24    CapsuleBundle { capsules: vec![] }
25}
26
27#[allow(dead_code)]
28pub fn add_capsule(bundle: &mut CapsuleBundle, cap: CapsuleExport) {
29    bundle.capsules.push(cap);
30}
31
32#[allow(dead_code)]
33pub fn capsule_count(bundle: &CapsuleBundle) -> usize {
34    bundle.capsules.len()
35}
36
37#[allow(dead_code)]
38pub fn capsule_volume(cap: &CapsuleExport) -> f32 {
39    let r = cap.radius;
40    let h = cap.half_height * 2.0;
41    // Cylinder + sphere
42    PI * r * r * h + (4.0 / 3.0) * PI * r * r * r
43}
44
45#[allow(dead_code)]
46pub fn capsule_surface_area(cap: &CapsuleExport) -> f32 {
47    let r = cap.radius;
48    let h = cap.half_height * 2.0;
49    2.0 * PI * r * h + 4.0 * PI * r * r
50}
51
52#[allow(dead_code)]
53pub fn capsule_total_length(cap: &CapsuleExport) -> f32 {
54    cap.half_height * 2.0 + cap.radius * 2.0
55}
56
57#[allow(dead_code)]
58pub fn capsule_to_json(cap: &CapsuleExport) -> String {
59    format!(
60        "{{\"name\":\"{}\",\"radius\":{},\"half_height\":{},\"center\":[{},{},{}]}}",
61        cap.name, cap.radius, cap.half_height, cap.center[0], cap.center[1], cap.center[2]
62    )
63}
64
65#[allow(dead_code)]
66pub fn capsule_bundle_to_json(bundle: &CapsuleBundle) -> String {
67    format!("{{\"capsule_count\":{}}}", bundle.capsules.len())
68}
69
70#[allow(dead_code)]
71pub fn validate_capsule(cap: &CapsuleExport) -> bool {
72    cap.radius > 0.0 && cap.half_height >= 0.0 && !cap.name.is_empty()
73}
74
75#[allow(dead_code)]
76pub fn find_capsule_by_name<'a>(
77    bundle: &'a CapsuleBundle,
78    name: &str,
79) -> Option<&'a CapsuleExport> {
80    bundle.capsules.iter().find(|c| c.name == name)
81}
82
83#[allow(dead_code)]
84pub fn default_capsule(name: &str) -> CapsuleExport {
85    CapsuleExport {
86        name: name.to_string(),
87        center: [0.0; 3],
88        axis: [0.0, 1.0, 0.0],
89        radius: 0.1,
90        half_height: 0.5,
91    }
92}
93
94#[allow(dead_code)]
95pub fn total_capsule_volume(bundle: &CapsuleBundle) -> f32 {
96    bundle.capsules.iter().map(capsule_volume).sum()
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn arm_capsule() -> CapsuleExport {
104        default_capsule("upper_arm")
105    }
106
107    #[test]
108    fn test_add_capsule() {
109        let mut b = new_capsule_bundle();
110        add_capsule(&mut b, arm_capsule());
111        assert_eq!(capsule_count(&b), 1);
112    }
113
114    #[test]
115    fn test_capsule_volume_positive() {
116        let cap = arm_capsule();
117        assert!(capsule_volume(&cap) > 0.0);
118    }
119
120    #[test]
121    fn test_capsule_surface_area_positive() {
122        let cap = arm_capsule();
123        assert!(capsule_surface_area(&cap) > 0.0);
124    }
125
126    #[test]
127    fn test_capsule_total_length() {
128        let cap = arm_capsule();
129        let expected = cap.half_height * 2.0 + cap.radius * 2.0;
130        assert!((capsule_total_length(&cap) - expected).abs() < 1e-5);
131    }
132
133    #[test]
134    fn test_validate_capsule() {
135        let cap = arm_capsule();
136        assert!(validate_capsule(&cap));
137    }
138
139    #[test]
140    fn test_find_capsule_found() {
141        let mut b = new_capsule_bundle();
142        add_capsule(&mut b, arm_capsule());
143        assert!(find_capsule_by_name(&b, "upper_arm").is_some());
144    }
145
146    #[test]
147    fn test_find_capsule_missing() {
148        let b = new_capsule_bundle();
149        assert!(find_capsule_by_name(&b, "arm").is_none());
150    }
151
152    #[test]
153    fn test_total_volume() {
154        let mut b = new_capsule_bundle();
155        add_capsule(&mut b, arm_capsule());
156        add_capsule(&mut b, arm_capsule());
157        let single = capsule_volume(&arm_capsule());
158        assert!((total_capsule_volume(&b) - 2.0 * single).abs() < 1e-5);
159    }
160
161    #[test]
162    fn test_to_json() {
163        let cap = arm_capsule();
164        let j = capsule_to_json(&cap);
165        assert!(j.contains("upper_arm"));
166        assert!(j.contains("radius"));
167    }
168
169    #[test]
170    fn test_bundle_to_json() {
171        let mut b = new_capsule_bundle();
172        add_capsule(&mut b, arm_capsule());
173        let j = capsule_bundle_to_json(&b);
174        assert!(j.contains("capsule_count"));
175    }
176}