Skip to main content

oxihuman_morph/
corrective_pose_driver.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Corrective pose driver — triggers corrective shape keys based on joint pose.
6
7/// Configuration for a corrective pose driver.
8#[derive(Debug, Clone)]
9pub struct CorrectivePoseDriverConfig {
10    /// Activation angle threshold in radians.
11    pub threshold_rad: f32,
12    /// Maximum weight when fully activated.
13    pub max_weight: f32,
14}
15
16impl Default for CorrectivePoseDriverConfig {
17    fn default() -> Self {
18        CorrectivePoseDriverConfig {
19            threshold_rad: 0.3,
20            max_weight: 1.0,
21        }
22    }
23}
24
25/// A single corrective pose driver binding.
26#[derive(Debug, Clone)]
27pub struct CorrectivePoseDriver {
28    pub joint_name: String,
29    pub target_shape: String,
30    pub config: CorrectivePoseDriverConfig,
31    pub current_weight: f32,
32}
33
34impl CorrectivePoseDriver {
35    pub fn new(joint_name: &str, target_shape: &str) -> Self {
36        CorrectivePoseDriver {
37            joint_name: joint_name.to_string(),
38            target_shape: target_shape.to_string(),
39            config: CorrectivePoseDriverConfig::default(),
40            current_weight: 0.0,
41        }
42    }
43}
44
45/// Create a new corrective pose driver.
46pub fn new_corrective_pose_driver(joint_name: &str, target_shape: &str) -> CorrectivePoseDriver {
47    CorrectivePoseDriver::new(joint_name, target_shape)
48}
49
50/// Evaluate the driver weight for the given joint angle.
51pub fn evaluate_pose_driver(driver: &mut CorrectivePoseDriver, joint_angle_rad: f32) -> f32 {
52    let t = driver.config.threshold_rad;
53    let w = if joint_angle_rad >= t {
54        (joint_angle_rad - t) / (std::f32::consts::PI - t)
55    } else {
56        0.0
57    };
58    driver.current_weight = w.clamp(0.0, driver.config.max_weight);
59    driver.current_weight
60}
61
62/// Reset the driver weight to zero.
63pub fn reset_pose_driver(driver: &mut CorrectivePoseDriver) {
64    driver.current_weight = 0.0;
65}
66
67/// Return the driver weight as a JSON-like string.
68pub fn pose_driver_to_json(driver: &CorrectivePoseDriver) -> String {
69    format!(
70        r#"{{"joint":"{}","shape":"{}","weight":{:.4}}}"#,
71        driver.joint_name, driver.target_shape, driver.current_weight
72    )
73}
74
75/// Set the threshold angle.
76pub fn set_pose_driver_threshold(driver: &mut CorrectivePoseDriver, threshold_rad: f32) {
77    driver.config.threshold_rad = threshold_rad.clamp(0.0, std::f32::consts::PI);
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_new_driver_zero_weight() {
86        let d = new_corrective_pose_driver("shoulder", "shoulder_corrective");
87        assert!((d.current_weight).abs() < 1e-6, /* weight should be 0 */);
88    }
89
90    #[test]
91    fn test_evaluate_below_threshold_is_zero() {
92        let mut d = new_corrective_pose_driver("elbow", "elbow_bulge");
93        let w = evaluate_pose_driver(&mut d, 0.1);
94        assert!((w).abs() < 1e-6, /* below threshold should give 0 weight */);
95    }
96
97    #[test]
98    fn test_evaluate_above_threshold_positive() {
99        let mut d = new_corrective_pose_driver("elbow", "elbow_bulge");
100        let w = evaluate_pose_driver(&mut d, 1.5);
101        assert!(w > 0.0, /* above threshold should give positive weight */);
102    }
103
104    #[test]
105    fn test_evaluate_clamps_to_max_weight() {
106        let mut d = new_corrective_pose_driver("knee", "knee_corrective");
107        let w = evaluate_pose_driver(&mut d, std::f32::consts::PI);
108        assert!(w <= d.config.max_weight, /* weight must not exceed max */);
109    }
110
111    #[test]
112    fn test_reset_clears_weight() {
113        let mut d = new_corrective_pose_driver("hip", "hip_corrective");
114        evaluate_pose_driver(&mut d, 2.0);
115        reset_pose_driver(&mut d);
116        assert!((d.current_weight).abs() < 1e-6, /* reset should zero the weight */);
117    }
118
119    #[test]
120    fn test_set_threshold() {
121        let mut d = new_corrective_pose_driver("wrist", "wrist_corrective");
122        set_pose_driver_threshold(&mut d, 0.8);
123        assert!((d.config.threshold_rad - 0.8).abs() < 1e-6, /* threshold should be updated */);
124    }
125
126    #[test]
127    fn test_to_json_contains_joint_name() {
128        let d = new_corrective_pose_driver("ankle", "ankle_corrective");
129        let s = pose_driver_to_json(&d);
130        assert!(s.contains("ankle"), /* JSON should contain joint name */);
131    }
132
133    #[test]
134    fn test_threshold_clamps_to_pi() {
135        let mut d = new_corrective_pose_driver("test", "test_shape");
136        set_pose_driver_threshold(&mut d, 99.0);
137        assert!(d.config.threshold_rad <= std::f32::consts::PI, /* threshold clamped to PI */);
138    }
139
140    #[test]
141    fn test_joint_name_stored() {
142        let d = new_corrective_pose_driver("shoulder_l", "shoulder_bulge");
143        assert_eq!(d.joint_name, "shoulder_l" /* joint name must match */,);
144    }
145
146    #[test]
147    fn test_shape_name_stored() {
148        let d = new_corrective_pose_driver("hip", "hip_crease");
149        assert_eq!(
150            d.target_shape,
151            "hip_crease", /* shape name must match */
152        );
153    }
154}