Skip to main content

oxihuman_export/
joint_scale_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Per-joint non-uniform scale export for skeleton rigs.
6
7#[allow(dead_code)]
8#[derive(Debug, Clone)]
9pub struct JointScaleEntry {
10    pub joint_name: String,
11    pub scale: [f32; 3],
12}
13
14#[allow(dead_code)]
15#[derive(Debug, Clone)]
16pub struct JointScaleExport {
17    pub entries: Vec<JointScaleEntry>,
18}
19
20#[allow(dead_code)]
21pub fn new_joint_scale_export() -> JointScaleExport {
22    JointScaleExport {
23        entries: Vec::new(),
24    }
25}
26
27#[allow(dead_code)]
28pub fn add_joint_scale(exp: &mut JointScaleExport, joint: &str, scale: [f32; 3]) {
29    exp.entries.push(JointScaleEntry {
30        joint_name: joint.to_string(),
31        scale,
32    });
33}
34
35#[allow(dead_code)]
36pub fn joint_scale_count(exp: &JointScaleExport) -> usize {
37    exp.entries.len()
38}
39
40#[allow(dead_code)]
41pub fn find_joint_scale<'a>(exp: &'a JointScaleExport, joint: &str) -> Option<&'a JointScaleEntry> {
42    exp.entries.iter().find(|e| e.joint_name == joint)
43}
44
45#[allow(dead_code)]
46pub fn is_uniform_scale(entry: &JointScaleEntry) -> bool {
47    let [x, y, z] = entry.scale;
48    (x - y).abs() < 1e-5 && (y - z).abs() < 1e-5
49}
50
51#[allow(dead_code)]
52pub fn uniform_scale_count(exp: &JointScaleExport) -> usize {
53    exp.entries.iter().filter(|e| is_uniform_scale(e)).count()
54}
55
56#[allow(dead_code)]
57pub fn avg_scale_magnitude(exp: &JointScaleExport) -> f32 {
58    if exp.entries.is_empty() {
59        return 0.0;
60    }
61    let sum: f32 = exp
62        .entries
63        .iter()
64        .map(|e| {
65            let [x, y, z] = e.scale;
66            (x * x + y * y + z * z).sqrt() / 3.0_f32.sqrt()
67        })
68        .sum();
69    sum / exp.entries.len() as f32
70}
71
72#[allow(dead_code)]
73pub fn set_scale(exp: &mut JointScaleExport, joint: &str, scale: [f32; 3]) {
74    if let Some(e) = exp.entries.iter_mut().find(|e| e.joint_name == joint) {
75        e.scale = scale;
76    }
77}
78
79#[allow(dead_code)]
80pub fn joint_scale_to_json(exp: &JointScaleExport) -> String {
81    format!(
82        "{{\"entry_count\":{},\"uniform_count\":{}}}",
83        joint_scale_count(exp),
84        uniform_scale_count(exp)
85    )
86}
87
88#[allow(dead_code)]
89pub fn scales_positive(exp: &JointScaleExport) -> bool {
90    exp.entries.iter().all(|e| e.scale.iter().all(|&s| s > 0.0))
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_empty() {
99        let exp = new_joint_scale_export();
100        assert_eq!(joint_scale_count(&exp), 0);
101    }
102
103    #[test]
104    fn test_add_entry() {
105        let mut exp = new_joint_scale_export();
106        add_joint_scale(&mut exp, "spine", [1.0, 1.2, 1.0]);
107        assert_eq!(joint_scale_count(&exp), 1);
108    }
109
110    #[test]
111    fn test_find_entry() {
112        let mut exp = new_joint_scale_export();
113        add_joint_scale(&mut exp, "hip", [1.0; 3]);
114        assert!(find_joint_scale(&exp, "hip").is_some());
115    }
116
117    #[test]
118    fn test_is_uniform() {
119        let e = JointScaleEntry {
120            joint_name: "x".to_string(),
121            scale: [2.0, 2.0, 2.0],
122        };
123        assert!(is_uniform_scale(&e));
124    }
125
126    #[test]
127    fn test_not_uniform() {
128        let e = JointScaleEntry {
129            joint_name: "x".to_string(),
130            scale: [1.0, 2.0, 1.0],
131        };
132        assert!(!is_uniform_scale(&e));
133    }
134
135    #[test]
136    fn test_uniform_count() {
137        let mut exp = new_joint_scale_export();
138        add_joint_scale(&mut exp, "a", [1.0; 3]);
139        add_joint_scale(&mut exp, "b", [1.0, 2.0, 1.0]);
140        assert_eq!(uniform_scale_count(&exp), 1);
141    }
142
143    #[test]
144    fn test_set_scale() {
145        let mut exp = new_joint_scale_export();
146        add_joint_scale(&mut exp, "knee", [1.0; 3]);
147        set_scale(&mut exp, "knee", [2.0, 2.0, 2.0]);
148        let e = find_joint_scale(&exp, "knee").expect("should succeed");
149        assert!((e.scale[0] - 2.0).abs() < 1e-5);
150    }
151
152    #[test]
153    fn test_json_output() {
154        let exp = new_joint_scale_export();
155        let j = joint_scale_to_json(&exp);
156        assert!(j.contains("entry_count"));
157    }
158
159    #[test]
160    fn test_scales_positive() {
161        let mut exp = new_joint_scale_export();
162        add_joint_scale(&mut exp, "ok", [1.0, 0.5, 2.0]);
163        assert!(scales_positive(&exp));
164    }
165
166    #[test]
167    fn test_avg_scale_magnitude_one() {
168        let mut exp = new_joint_scale_export();
169        add_joint_scale(&mut exp, "unit", [1.0; 3]);
170        assert!((avg_scale_magnitude(&exp) - 1.0).abs() < 1e-4);
171    }
172}