Skip to main content

oxihuman_morph/
sdk_driven_shape.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Set-driven-key shape driver — maps a driver attribute to shape weights.
6
7/// A single control point in the set-driven-key curve.
8#[derive(Debug, Clone, Copy)]
9pub struct SdkCurvePoint {
10    pub driver_value: f32,
11    pub shape_weight: f32,
12}
13
14/// Set-driven-key shape driver.
15#[derive(Debug, Clone)]
16pub struct SdkDrivenShape {
17    pub driver_attr: String,
18    pub shape_name: String,
19    pub curve: Vec<SdkCurvePoint>,
20    pub current_weight: f32,
21}
22
23impl SdkDrivenShape {
24    pub fn new(driver_attr: &str, shape_name: &str) -> Self {
25        SdkDrivenShape {
26            driver_attr: driver_attr.to_string(),
27            shape_name: shape_name.to_string(),
28            curve: Vec::new(),
29            current_weight: 0.0,
30        }
31    }
32}
33
34/// Create a new SDK driven shape.
35pub fn new_sdk_driven_shape(driver_attr: &str, shape_name: &str) -> SdkDrivenShape {
36    SdkDrivenShape::new(driver_attr, shape_name)
37}
38
39/// Add a curve control point.
40pub fn sdk_add_point(shape: &mut SdkDrivenShape, driver_value: f32, shape_weight: f32) {
41    shape.curve.push(SdkCurvePoint {
42        driver_value,
43        shape_weight: shape_weight.clamp(0.0, 1.0),
44    });
45    shape.curve.sort_by(|a, b| {
46        a.driver_value
47            .partial_cmp(&b.driver_value)
48            .unwrap_or(std::cmp::Ordering::Equal)
49    });
50}
51
52/// Evaluate the shape weight for a given driver value.
53pub fn sdk_evaluate(shape: &mut SdkDrivenShape, driver_value: f32) -> f32 {
54    if shape.curve.is_empty() {
55        shape.current_weight = 0.0;
56        return 0.0;
57    }
58    let first = shape.curve[0];
59    let last = shape.curve[shape.curve.len() - 1];
60    if driver_value <= first.driver_value {
61        shape.current_weight = first.shape_weight;
62        return first.shape_weight;
63    }
64    if driver_value >= last.driver_value {
65        shape.current_weight = last.shape_weight;
66        return last.shape_weight;
67    }
68    for i in 0..shape.curve.len().saturating_sub(1) {
69        let a = shape.curve[i];
70        let b = shape.curve[i + 1];
71        if driver_value >= a.driver_value && driver_value <= b.driver_value {
72            let t = (driver_value - a.driver_value) / (b.driver_value - a.driver_value);
73            let w = a.shape_weight + t * (b.shape_weight - a.shape_weight);
74            shape.current_weight = w;
75            return w;
76        }
77    }
78    shape.current_weight = 0.0;
79    0.0
80}
81
82/// Reset the shape to zero weight.
83pub fn sdk_reset(shape: &mut SdkDrivenShape) {
84    shape.current_weight = 0.0;
85}
86
87/// Return a JSON-like string representation.
88pub fn sdk_to_json(shape: &SdkDrivenShape) -> String {
89    format!(
90        r#"{{"driver":"{}","shape":"{}","weight":{:.4},"points":{}}}"#,
91        shape.driver_attr,
92        shape.shape_name,
93        shape.current_weight,
94        shape.curve.len()
95    )
96}
97
98/// Return the number of control points.
99pub fn sdk_point_count(shape: &SdkDrivenShape) -> usize {
100    shape.curve.len()
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_new_sdk_shape_empty() {
109        let s = new_sdk_driven_shape("jaw_open", "mouth_open");
110        assert_eq!(sdk_point_count(&s), 0 /* new shape has no points */,);
111    }
112
113    #[test]
114    fn test_add_point_increases_count() {
115        let mut s = new_sdk_driven_shape("jaw_open", "mouth_open");
116        sdk_add_point(&mut s, 0.0, 0.0);
117        sdk_add_point(&mut s, 1.0, 1.0);
118        assert_eq!(sdk_point_count(&s), 2 /* two points should be added */,);
119    }
120
121    #[test]
122    fn test_evaluate_below_min_returns_first() {
123        let mut s = new_sdk_driven_shape("brow", "brow_raise");
124        sdk_add_point(&mut s, 0.5, 0.2);
125        sdk_add_point(&mut s, 1.0, 1.0);
126        let w = sdk_evaluate(&mut s, 0.0);
127        assert!((w - 0.2).abs() < 1e-5, /* below range returns first point weight */);
128    }
129
130    #[test]
131    fn test_evaluate_above_max_returns_last() {
132        let mut s = new_sdk_driven_shape("brow", "brow_raise");
133        sdk_add_point(&mut s, 0.0, 0.0);
134        sdk_add_point(&mut s, 1.0, 0.8);
135        let w = sdk_evaluate(&mut s, 2.0);
136        assert!((w - 0.8).abs() < 1e-5, /* above range returns last point weight */);
137    }
138
139    #[test]
140    fn test_evaluate_midpoint_interpolates() {
141        let mut s = new_sdk_driven_shape("eye", "eye_wide");
142        sdk_add_point(&mut s, 0.0, 0.0);
143        sdk_add_point(&mut s, 1.0, 1.0);
144        let w = sdk_evaluate(&mut s, 0.5);
145        assert!((w - 0.5).abs() < 1e-5, /* midpoint should interpolate linearly */);
146    }
147
148    #[test]
149    fn test_reset_zeroes_weight() {
150        let mut s = new_sdk_driven_shape("lip", "lip_compress");
151        sdk_add_point(&mut s, 0.0, 0.0);
152        sdk_add_point(&mut s, 1.0, 1.0);
153        sdk_evaluate(&mut s, 1.0);
154        sdk_reset(&mut s);
155        assert!((s.current_weight).abs() < 1e-6, /* reset should zero weight */);
156    }
157
158    #[test]
159    fn test_empty_evaluate_returns_zero() {
160        let mut s = new_sdk_driven_shape("chin", "chin_raise");
161        let w = sdk_evaluate(&mut s, 0.5);
162        assert!((w).abs() < 1e-6 /* empty curve returns 0 */,);
163    }
164
165    #[test]
166    fn test_points_sorted_by_driver_value() {
167        let mut s = new_sdk_driven_shape("test", "shape");
168        sdk_add_point(&mut s, 1.0, 0.8);
169        sdk_add_point(&mut s, 0.0, 0.0);
170        assert!(s.curve[0].driver_value < s.curve[1].driver_value, /* must be sorted */);
171    }
172
173    #[test]
174    fn test_to_json_contains_driver() {
175        let s = new_sdk_driven_shape("jaw_open", "mouth_shape");
176        let j = sdk_to_json(&s);
177        assert!(j.contains("jaw_open"), /* JSON must contain driver name */);
178    }
179
180    #[test]
181    fn test_weight_clamped_to_one() {
182        let mut s = new_sdk_driven_shape("test", "shape");
183        sdk_add_point(&mut s, 0.0, 2.0);
184        assert!(s.curve[0].shape_weight <= 1.0, /* weight should be clamped */);
185    }
186
187    #[test]
188    fn test_driver_attr_stored() {
189        let s = new_sdk_driven_shape("shoulder_rot", "shoulder_shape");
190        assert_eq!(
191            s.driver_attr,
192            "shoulder_rot", /* driver attr must match */
193        );
194    }
195}