oxihuman_morph/
sternum_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_4;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct SternumConfig {
13 pub xiphoid_ref_rad: f32,
15}
16
17impl Default for SternumConfig {
18 fn default() -> Self {
19 SternumConfig {
20 xiphoid_ref_rad: FRAC_PI_4,
21 }
22 }
23}
24
25#[allow(dead_code)]
27#[derive(Debug, Clone)]
28pub struct SternumState {
29 length: f32,
31 manubrium: f32,
33 xiphoid_angle: f32,
35 config: SternumConfig,
36}
37
38pub fn default_sternum_config() -> SternumConfig {
40 SternumConfig::default()
41}
42
43pub fn new_sternum_state(config: SternumConfig) -> SternumState {
45 SternumState {
46 length: 0.5,
47 manubrium: 0.0,
48 xiphoid_angle: 0.0,
49 config,
50 }
51}
52
53pub fn stc_set_length(state: &mut SternumState, v: f32) {
55 state.length = v.clamp(0.0, 1.0);
56}
57
58pub fn stc_set_manubrium(state: &mut SternumState, v: f32) {
60 state.manubrium = v.clamp(0.0, 1.0);
61}
62
63pub fn stc_set_xiphoid_angle(state: &mut SternumState, v: f32) {
65 state.xiphoid_angle = v.clamp(-1.0, 1.0);
66}
67
68pub fn stc_reset(state: &mut SternumState) {
70 state.length = 0.5;
71 state.manubrium = 0.0;
72 state.xiphoid_angle = 0.0;
73}
74
75pub fn stc_is_neutral(state: &SternumState) -> bool {
77 (state.length - 0.5).abs() < 1e-5 && state.manubrium < 1e-5 && state.xiphoid_angle.abs() < 1e-5
78}
79
80pub fn stc_xiphoid_angle_rad(state: &SternumState) -> f32 {
82 state.xiphoid_angle * state.config.xiphoid_ref_rad
83}
84
85pub fn stc_to_weights(state: &SternumState) -> [f32; 3] {
87 [
88 state.length,
89 state.manubrium,
90 (state.xiphoid_angle * 0.5 + 0.5).clamp(0.0, 1.0),
91 ]
92}
93
94pub fn stc_blend(a: &SternumState, b: &SternumState, t: f32) -> SternumState {
96 let t = t.clamp(0.0, 1.0);
97 SternumState {
98 length: a.length + (b.length - a.length) * t,
99 manubrium: a.manubrium + (b.manubrium - a.manubrium) * t,
100 xiphoid_angle: a.xiphoid_angle + (b.xiphoid_angle - a.xiphoid_angle) * t,
101 config: a.config.clone(),
102 }
103}
104
105pub fn stc_to_json(state: &SternumState) -> String {
107 format!(
108 r#"{{"length":{:.4},"manubrium":{:.4},"xiphoid_angle":{:.4}}}"#,
109 state.length, state.manubrium, state.xiphoid_angle
110 )
111}
112
113#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn make() -> SternumState {
121 new_sternum_state(default_sternum_config())
122 }
123
124 #[test]
125 fn neutral_on_creation() {
126 assert!(stc_is_neutral(&make()));
127 }
128
129 #[test]
130 fn set_length_clamps() {
131 let mut s = make();
132 stc_set_length(&mut s, 5.0);
133 assert!((s.length - 1.0).abs() < 1e-5);
134 }
135
136 #[test]
137 fn reset_restores_neutral() {
138 let mut s = make();
139 stc_set_manubrium(&mut s, 0.8);
140 stc_reset(&mut s);
141 assert!(stc_is_neutral(&s));
142 }
143
144 #[test]
145 fn xiphoid_angle_rad_computation() {
146 let mut s = make();
147 stc_set_xiphoid_angle(&mut s, 1.0);
148 assert!(stc_xiphoid_angle_rad(&s) > 0.0);
149 }
150
151 #[test]
152 fn weights_in_range() {
153 let s = make();
154 for v in stc_to_weights(&s) {
155 assert!((0.0..=1.0).contains(&v));
156 }
157 }
158
159 #[test]
160 fn blend_midpoint() {
161 let mut a = make();
162 let mut b = make();
163 stc_set_length(&mut a, 0.0);
164 stc_set_length(&mut b, 1.0);
165 let m = stc_blend(&a, &b, 0.5);
166 assert!((m.length - 0.5).abs() < 1e-5);
167 }
168
169 #[test]
170 fn blend_at_zero_is_a() {
171 let a = make();
172 let r = stc_blend(&a, &make(), 0.0);
173 assert!((r.length - a.length).abs() < 1e-5);
174 }
175
176 #[test]
177 fn json_has_length() {
178 assert!(stc_to_json(&make()).contains("length"));
179 }
180
181 #[test]
182 fn xiphoid_clamped_negative() {
183 let mut s = make();
184 stc_set_xiphoid_angle(&mut s, -5.0);
185 assert!((s.xiphoid_angle + 1.0).abs() < 1e-5);
186 }
187}