oxihuman_morph/
shoulder_acromion.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_4;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ShoulderSide {
12 Left,
13 Right,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct ShoulderAcromionConfig {
19 pub max_prominence: f32,
20}
21
22impl Default for ShoulderAcromionConfig {
23 fn default() -> Self {
24 Self {
25 max_prominence: 1.0,
26 }
27 }
28}
29
30#[allow(dead_code)]
31#[derive(Debug, Clone)]
32pub struct ShoulderAcromionState {
33 pub left: f32,
34 pub right: f32,
35 pub config: ShoulderAcromionConfig,
36}
37
38#[allow(dead_code)]
39pub fn default_shoulder_acromion_config() -> ShoulderAcromionConfig {
40 ShoulderAcromionConfig::default()
41}
42
43#[allow(dead_code)]
44pub fn new_shoulder_acromion_state(config: ShoulderAcromionConfig) -> ShoulderAcromionState {
45 ShoulderAcromionState {
46 left: 0.0,
47 right: 0.0,
48 config,
49 }
50}
51
52#[allow(dead_code)]
53pub fn sac_set(state: &mut ShoulderAcromionState, side: ShoulderSide, v: f32) {
54 let v = v.clamp(0.0, state.config.max_prominence);
55 match side {
56 ShoulderSide::Left => state.left = v,
57 ShoulderSide::Right => state.right = v,
58 }
59}
60
61#[allow(dead_code)]
62pub fn sac_set_both(state: &mut ShoulderAcromionState, v: f32) {
63 let v = v.clamp(0.0, state.config.max_prominence);
64 state.left = v;
65 state.right = v;
66}
67
68#[allow(dead_code)]
69pub fn sac_reset(state: &mut ShoulderAcromionState) {
70 state.left = 0.0;
71 state.right = 0.0;
72}
73
74#[allow(dead_code)]
75pub fn sac_is_neutral(state: &ShoulderAcromionState) -> bool {
76 state.left.abs() < 1e-6 && state.right.abs() < 1e-6
77}
78
79#[allow(dead_code)]
80pub fn sac_average(state: &ShoulderAcromionState) -> f32 {
81 (state.left + state.right) * 0.5
82}
83
84#[allow(dead_code)]
85pub fn sac_asymmetry(state: &ShoulderAcromionState) -> f32 {
86 (state.left - state.right).abs()
87}
88
89#[allow(dead_code)]
90pub fn sac_prominence_angle_rad(state: &ShoulderAcromionState) -> f32 {
91 sac_average(state) * FRAC_PI_4
92}
93
94#[allow(dead_code)]
95pub fn sac_to_weights(state: &ShoulderAcromionState) -> [f32; 2] {
96 let m = state.config.max_prominence;
97 let n = |v: f32| if m > 1e-9 { v / m } else { 0.0 };
98 [n(state.left), n(state.right)]
99}
100
101#[allow(dead_code)]
102pub fn sac_blend(a: &ShoulderAcromionState, b: &ShoulderAcromionState, t: f32) -> [f32; 2] {
103 let t = t.clamp(0.0, 1.0);
104 [
105 a.left * (1.0 - t) + b.left * t,
106 a.right * (1.0 - t) + b.right * t,
107 ]
108}
109
110#[allow(dead_code)]
111pub fn sac_to_json(state: &ShoulderAcromionState) -> String {
112 format!(
113 "{{\"left\":{:.4},\"right\":{:.4}}}",
114 state.left, state.right
115 )
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 #[test]
122 fn default_neutral() {
123 assert!(sac_is_neutral(&new_shoulder_acromion_state(
124 default_shoulder_acromion_config()
125 )));
126 }
127 #[test]
128 fn set_clamps() {
129 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
130 sac_set(&mut s, ShoulderSide::Left, 5.0);
131 assert!((0.0..=1.0).contains(&s.left));
132 }
133 #[test]
134 fn set_both_applies() {
135 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
136 sac_set_both(&mut s, 0.7);
137 assert!((s.right - 0.7).abs() < 1e-5);
138 }
139 #[test]
140 fn reset_zeroes() {
141 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
142 sac_set_both(&mut s, 0.5);
143 sac_reset(&mut s);
144 assert!(sac_is_neutral(&s));
145 }
146 #[test]
147 fn average_mid() {
148 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
149 sac_set(&mut s, ShoulderSide::Left, 0.4);
150 sac_set(&mut s, ShoulderSide::Right, 0.8);
151 assert!((sac_average(&s) - 0.6).abs() < 1e-5);
152 }
153 #[test]
154 fn asymmetry_abs_diff() {
155 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
156 sac_set(&mut s, ShoulderSide::Left, 0.2);
157 sac_set(&mut s, ShoulderSide::Right, 0.8);
158 assert!((sac_asymmetry(&s) - 0.6).abs() < 1e-5);
159 }
160 #[test]
161 fn angle_nonneg() {
162 let s = new_shoulder_acromion_state(default_shoulder_acromion_config());
163 assert!(sac_prominence_angle_rad(&s) >= 0.0);
164 }
165 #[test]
166 fn to_weights_max() {
167 let mut s = new_shoulder_acromion_state(default_shoulder_acromion_config());
168 sac_set(&mut s, ShoulderSide::Left, 1.0);
169 assert!((sac_to_weights(&s)[0] - 1.0).abs() < 1e-5);
170 }
171 #[test]
172 fn blend_at_half() {
173 let mut a = new_shoulder_acromion_state(default_shoulder_acromion_config());
174 let b = new_shoulder_acromion_state(default_shoulder_acromion_config());
175 sac_set(&mut a, ShoulderSide::Left, 0.6);
176 let w = sac_blend(&a, &b, 0.5);
177 assert!((w[0] - 0.3).abs() < 1e-5);
178 }
179 #[test]
180 fn to_json_has_left() {
181 assert!(sac_to_json(&new_shoulder_acromion_state(
182 default_shoulder_acromion_config()
183 ))
184 .contains("\"left\""));
185 }
186}