Skip to main content

oxihuman_morph/
shoulder_slope_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Shoulder slope / height asymmetry control.
6
7use std::f32::consts::FRAC_PI_4;
8
9/// Side.
10#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum ShoulderSide {
13    Left,
14    Right,
15    Both,
16}
17
18/// State.
19#[allow(dead_code)]
20#[derive(Clone, Debug)]
21pub struct ShoulderSlopeState {
22    /// Slope in normalised units (-1 = drooping, 0 = flat, 1 = raised).
23    pub slope_left: f32,
24    pub slope_right: f32,
25    /// Shoulder height offset (-1..1).
26    pub height_left: f32,
27    pub height_right: f32,
28}
29
30/// Config.
31#[allow(dead_code)]
32#[derive(Clone, Debug)]
33pub struct ShoulderSlopeConfig {
34    pub max_slope: f32,
35    pub max_height: f32,
36}
37
38impl Default for ShoulderSlopeConfig {
39    fn default() -> Self {
40        Self {
41            max_slope: 1.0,
42            max_height: 1.0,
43        }
44    }
45}
46impl Default for ShoulderSlopeState {
47    fn default() -> Self {
48        Self {
49            slope_left: 0.0,
50            slope_right: 0.0,
51            height_left: 0.0,
52            height_right: 0.0,
53        }
54    }
55}
56
57#[allow(dead_code)]
58pub fn new_shoulder_slope_state() -> ShoulderSlopeState {
59    ShoulderSlopeState::default()
60}
61
62#[allow(dead_code)]
63pub fn default_shoulder_slope_config() -> ShoulderSlopeConfig {
64    ShoulderSlopeConfig::default()
65}
66
67#[allow(dead_code)]
68pub fn ss_set_slope(
69    state: &mut ShoulderSlopeState,
70    cfg: &ShoulderSlopeConfig,
71    side: ShoulderSide,
72    v: f32,
73) {
74    let v = v.clamp(-cfg.max_slope, cfg.max_slope);
75    match side {
76        ShoulderSide::Left => state.slope_left = v,
77        ShoulderSide::Right => state.slope_right = v,
78        ShoulderSide::Both => {
79            state.slope_left = v;
80            state.slope_right = v;
81        }
82    }
83}
84
85#[allow(dead_code)]
86pub fn ss_set_height(
87    state: &mut ShoulderSlopeState,
88    cfg: &ShoulderSlopeConfig,
89    side: ShoulderSide,
90    v: f32,
91) {
92    let v = v.clamp(-cfg.max_height, cfg.max_height);
93    match side {
94        ShoulderSide::Left => state.height_left = v,
95        ShoulderSide::Right => state.height_right = v,
96        ShoulderSide::Both => {
97            state.height_left = v;
98            state.height_right = v;
99        }
100    }
101}
102
103#[allow(dead_code)]
104pub fn ss_reset(state: &mut ShoulderSlopeState) {
105    *state = ShoulderSlopeState::default();
106}
107
108#[allow(dead_code)]
109pub fn ss_is_neutral(state: &ShoulderSlopeState) -> bool {
110    state.slope_left.abs() < 1e-4
111        && state.slope_right.abs() < 1e-4
112        && state.height_left.abs() < 1e-4
113        && state.height_right.abs() < 1e-4
114}
115
116#[allow(dead_code)]
117pub fn ss_blend(a: &ShoulderSlopeState, b: &ShoulderSlopeState, t: f32) -> ShoulderSlopeState {
118    let t = t.clamp(0.0, 1.0);
119    ShoulderSlopeState {
120        slope_left: a.slope_left + (b.slope_left - a.slope_left) * t,
121        slope_right: a.slope_right + (b.slope_right - a.slope_right) * t,
122        height_left: a.height_left + (b.height_left - a.height_left) * t,
123        height_right: a.height_right + (b.height_right - a.height_right) * t,
124    }
125}
126
127#[allow(dead_code)]
128pub fn ss_symmetry(state: &ShoulderSlopeState) -> f32 {
129    1.0 - (state.slope_left - state.slope_right).abs().min(1.0)
130}
131
132/// Slope in radians (heuristic: slope * PI/4).
133#[allow(dead_code)]
134pub fn ss_slope_rad(state: &ShoulderSlopeState, side: ShoulderSide) -> f32 {
135    let s = match side {
136        ShoulderSide::Left => state.slope_left,
137        ShoulderSide::Right => state.slope_right,
138        ShoulderSide::Both => (state.slope_left + state.slope_right) * 0.5,
139    };
140    s * FRAC_PI_4
141}
142
143#[allow(dead_code)]
144pub fn ss_to_weights(state: &ShoulderSlopeState) -> [f32; 4] {
145    [
146        state.slope_left,
147        state.slope_right,
148        state.height_left,
149        state.height_right,
150    ]
151}
152
153#[allow(dead_code)]
154pub fn ss_to_json(state: &ShoulderSlopeState) -> String {
155    format!(
156        "{{\"slope_l\":{:.4},\"slope_r\":{:.4},\"h_l\":{:.4},\"h_r\":{:.4}}}",
157        state.slope_left, state.slope_right, state.height_left, state.height_right
158    )
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn default_neutral() {
167        assert!(ss_is_neutral(&new_shoulder_slope_state()));
168    }
169
170    #[test]
171    fn slope_clamps_max() {
172        let mut s = new_shoulder_slope_state();
173        let cfg = default_shoulder_slope_config();
174        ss_set_slope(&mut s, &cfg, ShoulderSide::Left, 5.0);
175        assert!(s.slope_left <= cfg.max_slope);
176    }
177
178    #[test]
179    fn slope_clamps_min() {
180        let mut s = new_shoulder_slope_state();
181        let cfg = default_shoulder_slope_config();
182        ss_set_slope(&mut s, &cfg, ShoulderSide::Right, -5.0);
183        assert!(s.slope_right >= -cfg.max_slope);
184    }
185
186    #[test]
187    fn both_sides() {
188        let mut s = new_shoulder_slope_state();
189        let cfg = default_shoulder_slope_config();
190        ss_set_slope(&mut s, &cfg, ShoulderSide::Both, 0.5);
191        assert!((s.slope_left - s.slope_right).abs() < 1e-5);
192    }
193
194    #[test]
195    fn reset_neutral() {
196        let mut s = new_shoulder_slope_state();
197        let cfg = default_shoulder_slope_config();
198        ss_set_slope(&mut s, &cfg, ShoulderSide::Both, 0.5);
199        ss_reset(&mut s);
200        assert!(ss_is_neutral(&s));
201    }
202
203    #[test]
204    fn blend_midpoint() {
205        let cfg = default_shoulder_slope_config();
206        let mut a = new_shoulder_slope_state();
207        let mut b = new_shoulder_slope_state();
208        ss_set_slope(&mut a, &cfg, ShoulderSide::Left, 0.0);
209        ss_set_slope(&mut b, &cfg, ShoulderSide::Left, 1.0);
210        let m = ss_blend(&a, &b, 0.5);
211        assert!((m.slope_left - 0.5).abs() < 1e-4);
212    }
213
214    #[test]
215    fn symmetry_equal() {
216        let s = new_shoulder_slope_state();
217        assert!((ss_symmetry(&s) - 1.0).abs() < 1e-5);
218    }
219
220    #[test]
221    fn slope_rad_zero() {
222        let s = new_shoulder_slope_state();
223        assert!((ss_slope_rad(&s, ShoulderSide::Left)).abs() < 1e-5);
224    }
225
226    #[test]
227    fn weights_len() {
228        assert_eq!(ss_to_weights(&new_shoulder_slope_state()).len(), 4);
229    }
230
231    #[test]
232    fn json_has_slope() {
233        assert!(ss_to_json(&new_shoulder_slope_state()).contains("slope"));
234    }
235}