oxihuman_morph/
hand_knuckle_control.rs1#![allow(dead_code)]
4
5pub const FINGER_COUNT: usize = 4;
9
10#[allow(dead_code)]
12#[derive(Clone, Debug)]
13pub struct KnuckleState {
14 pub prominence: [f32; FINGER_COUNT],
16 pub definition: [f32; FINGER_COUNT],
18}
19
20#[allow(dead_code)]
22#[derive(Clone, Debug)]
23pub struct KnuckleConfig {
24 pub max_prominence: f32,
25}
26
27impl Default for KnuckleConfig {
28 fn default() -> Self {
29 Self {
30 max_prominence: 1.0,
31 }
32 }
33}
34impl Default for KnuckleState {
35 fn default() -> Self {
36 Self {
37 prominence: [0.0; FINGER_COUNT],
38 definition: [0.5; FINGER_COUNT],
39 }
40 }
41}
42
43#[allow(dead_code)]
44pub fn new_knuckle_state() -> KnuckleState {
45 KnuckleState::default()
46}
47
48#[allow(dead_code)]
49pub fn default_knuckle_config() -> KnuckleConfig {
50 KnuckleConfig::default()
51}
52
53#[allow(dead_code)]
54pub fn kk_set_prominence(state: &mut KnuckleState, cfg: &KnuckleConfig, finger: usize, v: f32) {
55 if finger < FINGER_COUNT {
56 state.prominence[finger] = v.clamp(0.0, cfg.max_prominence);
57 }
58}
59
60#[allow(dead_code)]
61pub fn kk_set_definition(state: &mut KnuckleState, finger: usize, v: f32) {
62 if finger < FINGER_COUNT {
63 state.definition[finger] = v.clamp(0.0, 1.0);
64 }
65}
66
67#[allow(dead_code)]
68pub fn kk_set_all_prominence(state: &mut KnuckleState, cfg: &KnuckleConfig, v: f32) {
69 let v = v.clamp(0.0, cfg.max_prominence);
70 #[allow(clippy::needless_range_loop)]
71 for i in 0..FINGER_COUNT {
72 state.prominence[i] = v;
73 }
74}
75
76#[allow(dead_code)]
77pub fn kk_reset(state: &mut KnuckleState) {
78 *state = KnuckleState::default();
79}
80
81#[allow(dead_code)]
82pub fn kk_average_prominence(state: &KnuckleState) -> f32 {
83 state.prominence.iter().sum::<f32>() / FINGER_COUNT as f32
84}
85
86#[allow(dead_code)]
87pub fn kk_blend(a: &KnuckleState, b: &KnuckleState, t: f32) -> KnuckleState {
88 let t = t.clamp(0.0, 1.0);
89 let mut out = KnuckleState::default();
90 #[allow(clippy::needless_range_loop)]
91 for i in 0..FINGER_COUNT {
92 out.prominence[i] = a.prominence[i] + (b.prominence[i] - a.prominence[i]) * t;
93 out.definition[i] = a.definition[i] + (b.definition[i] - a.definition[i]) * t;
94 }
95 out
96}
97
98#[allow(dead_code)]
99pub fn kk_is_neutral(state: &KnuckleState) -> bool {
100 state.prominence.iter().all(|&v| v < 1e-4)
101}
102
103#[allow(dead_code)]
104pub fn kk_to_weights(state: &KnuckleState) -> Vec<f32> {
105 let mut w = Vec::with_capacity(FINGER_COUNT * 2);
106 for &p in &state.prominence {
107 w.push(p);
108 }
109 for &d in &state.definition {
110 w.push(d);
111 }
112 w
113}
114
115#[allow(dead_code)]
116pub fn kk_to_json(state: &KnuckleState) -> String {
117 let p: Vec<String> = state
118 .prominence
119 .iter()
120 .map(|v| format!("{:.4}", v))
121 .collect();
122 let d: Vec<String> = state
123 .definition
124 .iter()
125 .map(|v| format!("{:.4}", v))
126 .collect();
127 format!(
128 "{{\"prominence\":[{}],\"definition\":[{}]}}",
129 p.join(","),
130 d.join(",")
131 )
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn default_neutral() {
140 assert!(kk_is_neutral(&new_knuckle_state()));
141 }
142
143 #[test]
144 fn set_prominence_clamps() {
145 let mut s = new_knuckle_state();
146 let cfg = default_knuckle_config();
147 kk_set_prominence(&mut s, &cfg, 0, 5.0);
148 assert!(s.prominence[0] <= cfg.max_prominence);
149 }
150
151 #[test]
152 fn out_of_range_finger_ignored() {
153 let mut s = new_knuckle_state();
154 let cfg = default_knuckle_config();
155 kk_set_prominence(&mut s, &cfg, 99, 1.0);
156 assert!(kk_is_neutral(&s));
157 }
158
159 #[test]
160 fn set_all_prominence() {
161 let mut s = new_knuckle_state();
162 let cfg = default_knuckle_config();
163 kk_set_all_prominence(&mut s, &cfg, 0.7);
164 assert!(s.prominence.iter().all(|&v| (v - 0.7).abs() < 1e-5));
165 }
166
167 #[test]
168 fn reset_neutral() {
169 let mut s = new_knuckle_state();
170 let cfg = default_knuckle_config();
171 kk_set_all_prominence(&mut s, &cfg, 1.0);
172 kk_reset(&mut s);
173 assert!(kk_is_neutral(&s));
174 }
175
176 #[test]
177 fn average_prominence_zero() {
178 assert!((kk_average_prominence(&new_knuckle_state())).abs() < 1e-5);
179 }
180
181 #[test]
182 fn blend_midpoint() {
183 let cfg = default_knuckle_config();
184 let mut a = new_knuckle_state();
185 let mut b = new_knuckle_state();
186 kk_set_prominence(&mut a, &cfg, 0, 0.0);
187 kk_set_prominence(&mut b, &cfg, 0, 1.0);
188 let m = kk_blend(&a, &b, 0.5);
189 assert!((m.prominence[0] - 0.5).abs() < 1e-4);
190 }
191
192 #[test]
193 fn weights_len() {
194 assert_eq!(kk_to_weights(&new_knuckle_state()).len(), FINGER_COUNT * 2);
195 }
196
197 #[test]
198 fn json_has_prominence() {
199 assert!(kk_to_json(&new_knuckle_state()).contains("prominence"));
200 }
201
202 #[test]
203 fn definition_clamped() {
204 let mut s = new_knuckle_state();
205 kk_set_definition(&mut s, 1, 5.0);
206 assert!(s.definition[1] <= 1.0);
207 }
208}