Skip to main content

oxihuman_morph/
scapula_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Scapula morph — controls scapular prominence, elevation and winging.
6
7/// Configuration for scapula control.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct ScapulaConfig {
11    pub max_wing: f32,
12}
13
14/// Side selection.
15#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ScapulaSide {
18    Left,
19    Right,
20}
21
22/// Runtime state.
23#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct ScapulaState {
26    pub left_wing: f32,
27    pub right_wing: f32,
28    pub left_elevation: f32,
29    pub right_elevation: f32,
30}
31
32#[allow(dead_code)]
33pub fn default_scapula_config() -> ScapulaConfig {
34    ScapulaConfig { max_wing: 1.0 }
35}
36
37#[allow(dead_code)]
38pub fn new_scapula_state() -> ScapulaState {
39    ScapulaState {
40        left_wing: 0.0,
41        right_wing: 0.0,
42        left_elevation: 0.0,
43        right_elevation: 0.0,
44    }
45}
46
47#[allow(dead_code)]
48pub fn sc_set_wing(state: &mut ScapulaState, cfg: &ScapulaConfig, side: ScapulaSide, v: f32) {
49    let clamped = v.clamp(0.0, cfg.max_wing);
50    match side {
51        ScapulaSide::Left => state.left_wing = clamped,
52        ScapulaSide::Right => state.right_wing = clamped,
53    }
54}
55
56#[allow(dead_code)]
57pub fn sc_set_elevation(state: &mut ScapulaState, side: ScapulaSide, v: f32) {
58    let clamped = v.clamp(-1.0, 1.0);
59    match side {
60        ScapulaSide::Left => state.left_elevation = clamped,
61        ScapulaSide::Right => state.right_elevation = clamped,
62    }
63}
64
65#[allow(dead_code)]
66pub fn sc_set_both_wing(state: &mut ScapulaState, cfg: &ScapulaConfig, v: f32) {
67    let clamped = v.clamp(0.0, cfg.max_wing);
68    state.left_wing = clamped;
69    state.right_wing = clamped;
70}
71
72#[allow(dead_code)]
73pub fn sc_reset(state: &mut ScapulaState) {
74    *state = new_scapula_state();
75}
76
77#[allow(dead_code)]
78pub fn sc_is_neutral(state: &ScapulaState) -> bool {
79    let vals = [
80        state.left_wing,
81        state.right_wing,
82        state.left_elevation,
83        state.right_elevation,
84    ];
85    !vals.is_empty() && vals.iter().all(|v| v.abs() < 1e-6)
86}
87
88#[allow(dead_code)]
89pub fn sc_average_wing(state: &ScapulaState) -> f32 {
90    (state.left_wing + state.right_wing) * 0.5
91}
92
93#[allow(dead_code)]
94pub fn sc_symmetry(state: &ScapulaState) -> f32 {
95    (state.left_wing - state.right_wing).abs()
96}
97
98#[allow(dead_code)]
99pub fn sc_blend(a: &ScapulaState, b: &ScapulaState, t: f32) -> ScapulaState {
100    let t = t.clamp(0.0, 1.0);
101    ScapulaState {
102        left_wing: a.left_wing + (b.left_wing - a.left_wing) * t,
103        right_wing: a.right_wing + (b.right_wing - a.right_wing) * t,
104        left_elevation: a.left_elevation + (b.left_elevation - a.left_elevation) * t,
105        right_elevation: a.right_elevation + (b.right_elevation - a.right_elevation) * t,
106    }
107}
108
109#[allow(dead_code)]
110pub fn sc_to_weights(state: &ScapulaState) -> Vec<(String, f32)> {
111    vec![
112        ("scapula_wing_l".to_string(), state.left_wing),
113        ("scapula_wing_r".to_string(), state.right_wing),
114        ("scapula_elevation_l".to_string(), state.left_elevation),
115        ("scapula_elevation_r".to_string(), state.right_elevation),
116    ]
117}
118
119#[allow(dead_code)]
120pub fn sc_to_json(state: &ScapulaState) -> String {
121    format!(
122        r#"{{"left_wing":{:.4},"right_wing":{:.4},"left_elevation":{:.4},"right_elevation":{:.4}}}"#,
123        state.left_wing, state.right_wing, state.left_elevation, state.right_elevation
124    )
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn default_config() {
133        let cfg = default_scapula_config();
134        assert!((cfg.max_wing - 1.0).abs() < 1e-6);
135    }
136
137    #[test]
138    fn new_state_neutral() {
139        let s = new_scapula_state();
140        assert!(sc_is_neutral(&s));
141    }
142
143    #[test]
144    fn set_wing_left() {
145        let cfg = default_scapula_config();
146        let mut s = new_scapula_state();
147        sc_set_wing(&mut s, &cfg, ScapulaSide::Left, 0.5);
148        assert!((s.left_wing - 0.5).abs() < 1e-6);
149        assert_eq!(s.right_wing, 0.0);
150    }
151
152    #[test]
153    fn set_wing_clamps() {
154        let cfg = default_scapula_config();
155        let mut s = new_scapula_state();
156        sc_set_wing(&mut s, &cfg, ScapulaSide::Right, 10.0);
157        assert!((s.right_wing - 1.0).abs() < 1e-6);
158    }
159
160    #[test]
161    fn set_both_wing_equal() {
162        let cfg = default_scapula_config();
163        let mut s = new_scapula_state();
164        sc_set_both_wing(&mut s, &cfg, 0.7);
165        assert!(sc_symmetry(&s) < 1e-6);
166    }
167
168    #[test]
169    fn set_elevation_signed() {
170        let mut s = new_scapula_state();
171        sc_set_elevation(&mut s, ScapulaSide::Left, -0.5);
172        assert!((s.left_elevation + 0.5).abs() < 1e-6);
173    }
174
175    #[test]
176    fn average_wing() {
177        let cfg = default_scapula_config();
178        let mut s = new_scapula_state();
179        sc_set_wing(&mut s, &cfg, ScapulaSide::Left, 0.4);
180        sc_set_wing(&mut s, &cfg, ScapulaSide::Right, 0.6);
181        assert!((sc_average_wing(&s) - 0.5).abs() < 1e-6);
182    }
183
184    #[test]
185    fn reset_clears() {
186        let cfg = default_scapula_config();
187        let mut s = new_scapula_state();
188        sc_set_both_wing(&mut s, &cfg, 0.8);
189        sc_reset(&mut s);
190        assert!(sc_is_neutral(&s));
191    }
192
193    #[test]
194    fn blend_midpoint() {
195        let a = new_scapula_state();
196        let cfg = default_scapula_config();
197        let mut b = new_scapula_state();
198        sc_set_both_wing(&mut b, &cfg, 1.0);
199        let mid = sc_blend(&a, &b, 0.5);
200        assert!((mid.left_wing - 0.5).abs() < 1e-6);
201    }
202
203    #[test]
204    fn to_weights_count() {
205        let s = new_scapula_state();
206        assert_eq!(sc_to_weights(&s).len(), 4);
207    }
208}