Skip to main content

oxihuman_export/
dynamic_bone_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// Export dynamic bone / spring bone chain data.
6#[allow(dead_code)]
7pub struct DynamicBone {
8    pub name: String,
9    pub root_bone: String,
10    pub stiffness: f32,
11    pub damping: f32,
12    pub gravity: [f32; 3],
13    pub radius: f32,
14    pub end_length: f32,
15    pub colliders: Vec<String>,
16}
17
18#[allow(dead_code)]
19pub struct DynamicBoneExport {
20    pub chains: Vec<DynamicBone>,
21}
22
23#[allow(dead_code)]
24pub fn new_dynamic_bone_export() -> DynamicBoneExport {
25    DynamicBoneExport { chains: vec![] }
26}
27
28#[allow(dead_code)]
29pub fn add_dynamic_bone(export: &mut DynamicBoneExport, bone: DynamicBone) {
30    export.chains.push(bone);
31}
32
33#[allow(dead_code)]
34pub fn dynamic_bone_count(export: &DynamicBoneExport) -> usize {
35    export.chains.len()
36}
37
38#[allow(dead_code)]
39pub fn find_dynamic_bone<'a>(export: &'a DynamicBoneExport, name: &str) -> Option<&'a DynamicBone> {
40    export.chains.iter().find(|b| b.name == name)
41}
42
43#[allow(dead_code)]
44pub fn default_dynamic_bone(name: &str, root: &str) -> DynamicBone {
45    DynamicBone {
46        name: name.to_string(),
47        root_bone: root.to_string(),
48        stiffness: 0.2,
49        damping: 0.2,
50        gravity: [0.0, -9.81, 0.0],
51        radius: 0.02,
52        end_length: 0.0,
53        colliders: vec![],
54    }
55}
56
57#[allow(dead_code)]
58pub fn validate_dynamic_bone(bone: &DynamicBone) -> bool {
59    !bone.name.is_empty()
60        && !bone.root_bone.is_empty()
61        && (0.0..=1.0).contains(&bone.stiffness)
62        && (0.0..=1.0).contains(&bone.damping)
63        && bone.radius >= 0.0
64}
65
66#[allow(dead_code)]
67pub fn total_colliders(export: &DynamicBoneExport) -> usize {
68    export.chains.iter().map(|b| b.colliders.len()).sum()
69}
70
71#[allow(dead_code)]
72pub fn dynamic_bone_to_json(bone: &DynamicBone) -> String {
73    format!(
74        "{{\"name\":\"{}\",\"root\":\"{}\",\"stiffness\":{},\"damping\":{}}}",
75        bone.name, bone.root_bone, bone.stiffness, bone.damping
76    )
77}
78
79#[allow(dead_code)]
80pub fn dynamic_bone_export_to_json(export: &DynamicBoneExport) -> String {
81    format!("{{\"chain_count\":{}}}", export.chains.len())
82}
83
84#[allow(dead_code)]
85pub fn simulate_gravity_offset(bone: &DynamicBone, dt: f32) -> [f32; 3] {
86    let g = bone.gravity;
87    let stiffness = bone.stiffness.clamp(0.0, 1.0);
88    let influence = (1.0 - stiffness) * dt;
89    [g[0] * influence, g[1] * influence, g[2] * influence]
90}
91
92#[allow(dead_code)]
93pub fn bones_with_colliders(export: &DynamicBoneExport) -> Vec<&DynamicBone> {
94    export
95        .chains
96        .iter()
97        .filter(|b| !b.colliders.is_empty())
98        .collect()
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn hair_bone() -> DynamicBone {
106        default_dynamic_bone("hair_chain", "hair_root")
107    }
108
109    #[test]
110    fn test_add_dynamic_bone() {
111        let mut e = new_dynamic_bone_export();
112        add_dynamic_bone(&mut e, hair_bone());
113        assert_eq!(dynamic_bone_count(&e), 1);
114    }
115
116    #[test]
117    fn test_find_dynamic_bone() {
118        let mut e = new_dynamic_bone_export();
119        add_dynamic_bone(&mut e, hair_bone());
120        assert!(find_dynamic_bone(&e, "hair_chain").is_some());
121    }
122
123    #[test]
124    fn test_validate_default() {
125        let b = hair_bone();
126        assert!(validate_dynamic_bone(&b));
127    }
128
129    #[test]
130    fn test_validate_bad_stiffness() {
131        let mut b = hair_bone();
132        b.stiffness = 2.0;
133        assert!(!validate_dynamic_bone(&b));
134    }
135
136    #[test]
137    fn test_gravity_offset_nonzero() {
138        let b = hair_bone();
139        let off = simulate_gravity_offset(&b, 0.016);
140        assert!(off[1] < 0.0);
141    }
142
143    #[test]
144    fn test_bones_with_colliders_empty() {
145        let mut e = new_dynamic_bone_export();
146        add_dynamic_bone(&mut e, hair_bone());
147        assert_eq!(bones_with_colliders(&e).len(), 0);
148    }
149
150    #[test]
151    fn test_bones_with_colliders_found() {
152        let mut e = new_dynamic_bone_export();
153        let mut b = hair_bone();
154        b.colliders.push("head_collider".to_string());
155        add_dynamic_bone(&mut e, b);
156        assert_eq!(bones_with_colliders(&e).len(), 1);
157    }
158
159    #[test]
160    fn test_to_json() {
161        let b = hair_bone();
162        let j = dynamic_bone_to_json(&b);
163        assert!(j.contains("hair_chain"));
164    }
165
166    #[test]
167    fn test_export_to_json() {
168        let mut e = new_dynamic_bone_export();
169        add_dynamic_bone(&mut e, hair_bone());
170        let j = dynamic_bone_export_to_json(&e);
171        assert!(j.contains("chain_count"));
172    }
173
174    #[test]
175    fn test_total_colliders() {
176        let mut e = new_dynamic_bone_export();
177        let mut b = hair_bone();
178        b.colliders = vec!["c1".to_string(), "c2".to_string()];
179        add_dynamic_bone(&mut e, b);
180        assert_eq!(total_colliders(&e), 2);
181    }
182}