oxihuman_morph/
scapula_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct ScapulaConfig {
11 pub max_wing: f32,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ScapulaSide {
18 Left,
19 Right,
20}
21
22#[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}