Skip to main content

oxihuman_morph/
shoulder_acromion.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Shoulder acromion process prominence control.
6
7use 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}