Skip to main content

oxihuman_export/
blend_shape_driver_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// Export blend shape driver data (driver -> driven shape mapping).
6#[allow(dead_code)]
7pub struct BlendShapeDriver {
8    pub name: String,
9    pub driver_bone: String,
10    pub axis: DriverAxis,
11    pub input_range: [f32; 2],
12    pub driven_shapes: Vec<DrivenShape>,
13}
14
15#[allow(dead_code)]
16pub enum DriverAxis {
17    RotX,
18    RotY,
19    RotZ,
20    TransX,
21    TransY,
22    TransZ,
23}
24
25#[allow(dead_code)]
26pub struct DrivenShape {
27    pub shape_name: String,
28    pub weight_at_min: f32,
29    pub weight_at_max: f32,
30}
31
32#[allow(dead_code)]
33pub struct BlendShapeDriverExport {
34    pub drivers: Vec<BlendShapeDriver>,
35}
36
37#[allow(dead_code)]
38pub fn new_blend_shape_driver_export() -> BlendShapeDriverExport {
39    BlendShapeDriverExport { drivers: vec![] }
40}
41
42#[allow(dead_code)]
43pub fn add_driver(export: &mut BlendShapeDriverExport, driver: BlendShapeDriver) {
44    export.drivers.push(driver);
45}
46
47#[allow(dead_code)]
48pub fn driver_count(export: &BlendShapeDriverExport) -> usize {
49    export.drivers.len()
50}
51
52#[allow(dead_code)]
53pub fn total_driven_shapes(export: &BlendShapeDriverExport) -> usize {
54    export.drivers.iter().map(|d| d.driven_shapes.len()).sum()
55}
56
57#[allow(dead_code)]
58pub fn find_driver_by_name<'a>(
59    export: &'a BlendShapeDriverExport,
60    name: &str,
61) -> Option<&'a BlendShapeDriver> {
62    export.drivers.iter().find(|d| d.name == name)
63}
64
65#[allow(dead_code)]
66pub fn evaluate_driver(driver: &BlendShapeDriver, input: f32) -> Vec<(String, f32)> {
67    let [min_in, max_in] = driver.input_range;
68    let range = (max_in - min_in).abs();
69    let t = if range < 1e-10 {
70        0.0
71    } else {
72        ((input - min_in) / range).clamp(0.0, 1.0)
73    };
74    driver
75        .driven_shapes
76        .iter()
77        .map(|s| {
78            let w = s.weight_at_min + (s.weight_at_max - s.weight_at_min) * t;
79            (s.shape_name.clone(), w.clamp(0.0, 1.0))
80        })
81        .collect()
82}
83
84#[allow(dead_code)]
85pub fn driver_to_json(driver: &BlendShapeDriver) -> String {
86    format!(
87        "{{\"name\":\"{}\",\"driver_bone\":\"{}\",\"driven_shapes\":{}}}",
88        driver.name,
89        driver.driver_bone,
90        driver.driven_shapes.len()
91    )
92}
93
94#[allow(dead_code)]
95pub fn blend_shape_driver_export_to_json(export: &BlendShapeDriverExport) -> String {
96    format!(
97        "{{\"driver_count\":{},\"total_driven_shapes\":{}}}",
98        export.drivers.len(),
99        total_driven_shapes(export)
100    )
101}
102
103#[allow(dead_code)]
104pub fn validate_driver(driver: &BlendShapeDriver) -> bool {
105    driver.input_range[0] < driver.input_range[1]
106        && driver
107            .driven_shapes
108            .iter()
109            .all(|s| !s.shape_name.is_empty())
110}
111
112#[allow(dead_code)]
113pub fn axis_name(axis: &DriverAxis) -> &'static str {
114    match axis {
115        DriverAxis::RotX => "rot_x",
116        DriverAxis::RotY => "rot_y",
117        DriverAxis::RotZ => "rot_z",
118        DriverAxis::TransX => "trans_x",
119        DriverAxis::TransY => "trans_y",
120        DriverAxis::TransZ => "trans_z",
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn sample_driver() -> BlendShapeDriver {
129        BlendShapeDriver {
130            name: "smile_driver".to_string(),
131            driver_bone: "jaw".to_string(),
132            axis: DriverAxis::RotX,
133            input_range: [0.0, 90.0],
134            driven_shapes: vec![
135                DrivenShape {
136                    shape_name: "smile".to_string(),
137                    weight_at_min: 0.0,
138                    weight_at_max: 1.0,
139                },
140                DrivenShape {
141                    shape_name: "cheek_puff".to_string(),
142                    weight_at_min: 0.0,
143                    weight_at_max: 0.5,
144                },
145            ],
146        }
147    }
148
149    #[test]
150    fn test_add_driver() {
151        let mut e = new_blend_shape_driver_export();
152        add_driver(&mut e, sample_driver());
153        assert_eq!(driver_count(&e), 1);
154    }
155
156    #[test]
157    fn test_total_driven_shapes() {
158        let mut e = new_blend_shape_driver_export();
159        add_driver(&mut e, sample_driver());
160        assert_eq!(total_driven_shapes(&e), 2);
161    }
162
163    #[test]
164    fn test_find_driver_found() {
165        let mut e = new_blend_shape_driver_export();
166        add_driver(&mut e, sample_driver());
167        assert!(find_driver_by_name(&e, "smile_driver").is_some());
168    }
169
170    #[test]
171    fn test_find_driver_not_found() {
172        let e = new_blend_shape_driver_export();
173        assert!(find_driver_by_name(&e, "missing").is_none());
174    }
175
176    #[test]
177    fn test_evaluate_driver_at_max() {
178        let d = sample_driver();
179        let result = evaluate_driver(&d, 90.0);
180        assert_eq!(result.len(), 2);
181        assert!((result[0].1 - 1.0).abs() < 1e-4);
182    }
183
184    #[test]
185    fn test_evaluate_driver_at_min() {
186        let d = sample_driver();
187        let result = evaluate_driver(&d, 0.0);
188        assert!((result[0].1).abs() < 1e-4);
189    }
190
191    #[test]
192    fn test_evaluate_driver_midpoint() {
193        let d = sample_driver();
194        let result = evaluate_driver(&d, 45.0);
195        assert!((result[0].1 - 0.5).abs() < 1e-3);
196    }
197
198    #[test]
199    fn test_validate_driver_valid() {
200        assert!(validate_driver(&sample_driver()));
201    }
202
203    #[test]
204    fn test_to_json() {
205        let d = sample_driver();
206        let j = driver_to_json(&d);
207        assert!(j.contains("smile_driver"));
208    }
209
210    #[test]
211    fn test_axis_name() {
212        assert_eq!(axis_name(&DriverAxis::RotX), "rot_x");
213        assert_eq!(axis_name(&DriverAxis::TransZ), "trans_z");
214    }
215}