oxihuman_morph/
hand_metacarpal_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_4;
8
9pub const MC_COUNT: usize = 5;
11
12#[allow(dead_code)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HandSide {
15 Left,
16 Right,
17}
18
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct HandMetacarpalConfig {
22 pub max_length: f32,
23}
24
25impl Default for HandMetacarpalConfig {
26 fn default() -> Self {
27 Self { max_length: 1.0 }
28 }
29}
30
31#[allow(dead_code)]
32#[derive(Debug, Clone)]
33pub struct HandMetacarpalState {
34 pub left: [f32; MC_COUNT],
35 pub right: [f32; MC_COUNT],
36 pub config: HandMetacarpalConfig,
37}
38
39#[allow(dead_code)]
40pub fn default_hand_metacarpal_config() -> HandMetacarpalConfig {
41 HandMetacarpalConfig::default()
42}
43
44#[allow(dead_code)]
45pub fn new_hand_metacarpal_state(config: HandMetacarpalConfig) -> HandMetacarpalState {
46 HandMetacarpalState {
47 left: [0.0; MC_COUNT],
48 right: [0.0; MC_COUNT],
49 config,
50 }
51}
52
53#[allow(dead_code)]
54pub fn hmc_set(state: &mut HandMetacarpalState, side: HandSide, bone: usize, v: f32) {
55 if bone < MC_COUNT {
56 let v = v.clamp(0.0, state.config.max_length);
57 match side {
58 HandSide::Left => state.left[bone] = v,
59 HandSide::Right => state.right[bone] = v,
60 }
61 }
62}
63
64#[allow(dead_code)]
65pub fn hmc_set_all(state: &mut HandMetacarpalState, v: f32) {
66 let v = v.clamp(0.0, state.config.max_length);
67 #[allow(clippy::needless_range_loop)]
68 for i in 0..MC_COUNT {
69 state.left[i] = v;
70 state.right[i] = v;
71 }
72}
73
74#[allow(dead_code)]
75pub fn hmc_reset(state: &mut HandMetacarpalState) {
76 state.left = [0.0; MC_COUNT];
77 state.right = [0.0; MC_COUNT];
78}
79
80#[allow(dead_code)]
81pub fn hmc_is_neutral(state: &HandMetacarpalState) -> bool {
82 state.left.iter().all(|v| v.abs() < 1e-6) && state.right.iter().all(|v| v.abs() < 1e-6)
83}
84
85#[allow(dead_code)]
86pub fn hmc_average(state: &HandMetacarpalState, side: HandSide) -> f32 {
87 let arr = match side {
88 HandSide::Left => &state.left,
89 HandSide::Right => &state.right,
90 };
91 arr.iter().sum::<f32>() / MC_COUNT as f32
92}
93
94#[allow(dead_code)]
95pub fn hmc_span_angle_rad(state: &HandMetacarpalState) -> f32 {
96 let avg = (hmc_average(state, HandSide::Left) + hmc_average(state, HandSide::Right)) * 0.5;
97 avg * FRAC_PI_4
98}
99
100#[allow(dead_code)]
101pub fn hmc_to_weights(state: &HandMetacarpalState) -> [f32; MC_COUNT] {
102 let m = state.config.max_length;
103 let mut w = [0.0f32; MC_COUNT];
104 #[allow(clippy::needless_range_loop)]
105 for i in 0..MC_COUNT {
106 w[i] = if m > 1e-9 {
107 (state.left[i] + state.right[i]) * 0.5 / m
108 } else {
109 0.0
110 };
111 }
112 w
113}
114
115#[allow(dead_code)]
116pub fn hmc_to_json(state: &HandMetacarpalState) -> String {
117 format!(
118 "{{\"left_avg\":{:.4},\"right_avg\":{:.4}}}",
119 hmc_average(state, HandSide::Left),
120 hmc_average(state, HandSide::Right)
121 )
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 #[test]
128 fn default_neutral() {
129 assert!(hmc_is_neutral(&new_hand_metacarpal_state(
130 default_hand_metacarpal_config()
131 )));
132 }
133 #[test]
134 fn set_clamps() {
135 let mut s = new_hand_metacarpal_state(default_hand_metacarpal_config());
136 hmc_set(&mut s, HandSide::Left, 0, 5.0);
137 assert!((0.0..=1.0).contains(&s.left[0]));
138 }
139 #[test]
140 fn set_all_applies() {
141 let mut s = new_hand_metacarpal_state(default_hand_metacarpal_config());
142 hmc_set_all(&mut s, 0.5);
143 assert!((s.left[0] - 0.5).abs() < 1e-5);
144 }
145 #[test]
146 fn reset_zeroes() {
147 let mut s = new_hand_metacarpal_state(default_hand_metacarpal_config());
148 hmc_set_all(&mut s, 0.7);
149 hmc_reset(&mut s);
150 assert!(hmc_is_neutral(&s));
151 }
152 #[test]
153 fn average_is_zero_by_default() {
154 let s = new_hand_metacarpal_state(default_hand_metacarpal_config());
155 assert!(hmc_average(&s, HandSide::Left).abs() < 1e-6);
156 }
157 #[test]
158 fn span_angle_nonneg() {
159 let s = new_hand_metacarpal_state(default_hand_metacarpal_config());
160 assert!(hmc_span_angle_rad(&s) >= 0.0);
161 }
162 #[test]
163 fn to_weights_max() {
164 let mut s = new_hand_metacarpal_state(default_hand_metacarpal_config());
165 hmc_set_all(&mut s, 1.0);
166 let w = hmc_to_weights(&s);
167 assert!((w[0] - 1.0).abs() < 1e-5);
168 }
169 #[test]
170 fn out_of_range_ignored() {
171 let mut s = new_hand_metacarpal_state(default_hand_metacarpal_config());
172 hmc_set(&mut s, HandSide::Left, 99, 1.0);
173 assert!(hmc_is_neutral(&s));
174 }
175 #[test]
176 fn to_json_has_left_avg() {
177 assert!(
178 hmc_to_json(&new_hand_metacarpal_state(default_hand_metacarpal_config()))
179 .contains("left_avg")
180 );
181 }
182 #[test]
183 fn mc_count_correct() {
184 assert_eq!(MC_COUNT, 5);
185 }
186}