Skip to main content

oxihuman_morph/
shoulder_pad_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Shoulder pad control — deltoid bulk and acromion prominence.
6
7/// Configuration for shoulder pad.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct ShoulderPadConfig {
11    pub max_bulk: f32,
12}
13
14/// Side selector.
15#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ShoulderPadSide {
18    Left,
19    Right,
20}
21
22/// Runtime state.
23#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct ShoulderPadState {
26    pub left_bulk: f32,
27    pub right_bulk: f32,
28    pub acromion_prominence: f32,
29}
30
31#[allow(dead_code)]
32pub fn default_shoulder_pad_config() -> ShoulderPadConfig {
33    ShoulderPadConfig { max_bulk: 1.0 }
34}
35
36#[allow(dead_code)]
37pub fn new_shoulder_pad_state() -> ShoulderPadState {
38    ShoulderPadState {
39        left_bulk: 0.0,
40        right_bulk: 0.0,
41        acromion_prominence: 0.0,
42    }
43}
44
45#[allow(dead_code)]
46pub fn spad_set_bulk(
47    state: &mut ShoulderPadState,
48    cfg: &ShoulderPadConfig,
49    side: ShoulderPadSide,
50    v: f32,
51) {
52    let clamped = v.clamp(0.0, cfg.max_bulk);
53    match side {
54        ShoulderPadSide::Left => state.left_bulk = clamped,
55        ShoulderPadSide::Right => state.right_bulk = clamped,
56    }
57}
58
59#[allow(dead_code)]
60pub fn spad_set_both(state: &mut ShoulderPadState, cfg: &ShoulderPadConfig, v: f32) {
61    let clamped = v.clamp(0.0, cfg.max_bulk);
62    state.left_bulk = clamped;
63    state.right_bulk = clamped;
64}
65
66#[allow(dead_code)]
67pub fn spad_set_acromion(state: &mut ShoulderPadState, v: f32) {
68    state.acromion_prominence = v.clamp(0.0, 1.0);
69}
70
71#[allow(dead_code)]
72pub fn spad_reset(state: &mut ShoulderPadState) {
73    *state = new_shoulder_pad_state();
74}
75
76#[allow(dead_code)]
77pub fn spad_is_neutral(state: &ShoulderPadState) -> bool {
78    state.left_bulk.abs() < 1e-6
79        && state.right_bulk.abs() < 1e-6
80        && state.acromion_prominence.abs() < 1e-6
81}
82
83#[allow(dead_code)]
84pub fn spad_average_bulk(state: &ShoulderPadState) -> f32 {
85    (state.left_bulk + state.right_bulk) * 0.5
86}
87
88#[allow(dead_code)]
89pub fn spad_symmetry(state: &ShoulderPadState) -> f32 {
90    (state.left_bulk - state.right_bulk).abs()
91}
92
93#[allow(dead_code)]
94pub fn spad_blend(a: &ShoulderPadState, b: &ShoulderPadState, t: f32) -> ShoulderPadState {
95    let t = t.clamp(0.0, 1.0);
96    ShoulderPadState {
97        left_bulk: a.left_bulk + (b.left_bulk - a.left_bulk) * t,
98        right_bulk: a.right_bulk + (b.right_bulk - a.right_bulk) * t,
99        acromion_prominence: a.acromion_prominence
100            + (b.acromion_prominence - a.acromion_prominence) * t,
101    }
102}
103
104#[allow(dead_code)]
105pub fn spad_to_weights(state: &ShoulderPadState) -> Vec<(String, f32)> {
106    vec![
107        ("shoulder_pad_bulk_l".to_string(), state.left_bulk),
108        ("shoulder_pad_bulk_r".to_string(), state.right_bulk),
109        ("acromion_prominence".to_string(), state.acromion_prominence),
110    ]
111}
112
113#[allow(dead_code)]
114pub fn spad_to_json(state: &ShoulderPadState) -> String {
115    format!(
116        r#"{{"left_bulk":{:.4},"right_bulk":{:.4},"acromion":{:.4}}}"#,
117        state.left_bulk, state.right_bulk, state.acromion_prominence
118    )
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn default_config() {
127        let cfg = default_shoulder_pad_config();
128        assert!((cfg.max_bulk - 1.0).abs() < 1e-6);
129    }
130
131    #[test]
132    fn new_state_neutral() {
133        let s = new_shoulder_pad_state();
134        assert!(spad_is_neutral(&s));
135    }
136
137    #[test]
138    fn set_bulk_left() {
139        let cfg = default_shoulder_pad_config();
140        let mut s = new_shoulder_pad_state();
141        spad_set_bulk(&mut s, &cfg, ShoulderPadSide::Left, 0.6);
142        assert!((s.left_bulk - 0.6).abs() < 1e-6);
143    }
144
145    #[test]
146    fn set_bulk_clamps() {
147        let cfg = default_shoulder_pad_config();
148        let mut s = new_shoulder_pad_state();
149        spad_set_bulk(&mut s, &cfg, ShoulderPadSide::Right, 5.0);
150        assert!((s.right_bulk - 1.0).abs() < 1e-6);
151    }
152
153    #[test]
154    fn set_both_symmetric() {
155        let cfg = default_shoulder_pad_config();
156        let mut s = new_shoulder_pad_state();
157        spad_set_both(&mut s, &cfg, 0.4);
158        assert!(spad_symmetry(&s) < 1e-6);
159    }
160
161    #[test]
162    fn set_acromion() {
163        let mut s = new_shoulder_pad_state();
164        spad_set_acromion(&mut s, 0.7);
165        assert!((s.acromion_prominence - 0.7).abs() < 1e-6);
166    }
167
168    #[test]
169    fn reset_clears() {
170        let cfg = default_shoulder_pad_config();
171        let mut s = new_shoulder_pad_state();
172        spad_set_both(&mut s, &cfg, 0.8);
173        spad_reset(&mut s);
174        assert!(spad_is_neutral(&s));
175    }
176
177    #[test]
178    fn blend_midpoint() {
179        let a = new_shoulder_pad_state();
180        let cfg = default_shoulder_pad_config();
181        let mut b = new_shoulder_pad_state();
182        spad_set_both(&mut b, &cfg, 1.0);
183        let m = spad_blend(&a, &b, 0.5);
184        assert!((m.left_bulk - 0.5).abs() < 1e-6);
185    }
186
187    #[test]
188    fn to_weights_count() {
189        let s = new_shoulder_pad_state();
190        assert_eq!(spad_to_weights(&s).len(), 3);
191    }
192
193    #[test]
194    fn to_json_fields() {
195        let s = new_shoulder_pad_state();
196        let j = spad_to_json(&s);
197        assert!(j.contains("left_bulk"));
198    }
199}