oxihuman_morph/
shoulder_slope_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_4;
8
9#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum ShoulderSide {
13 Left,
14 Right,
15 Both,
16}
17
18#[allow(dead_code)]
20#[derive(Clone, Debug)]
21pub struct ShoulderSlopeState {
22 pub slope_left: f32,
24 pub slope_right: f32,
25 pub height_left: f32,
27 pub height_right: f32,
28}
29
30#[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#[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}