oxihuman_morph/
hand_finger_splay.rs1#![allow(dead_code)]
3
4use std::f32::consts::FRAC_PI_6;
7
8pub const FINGER_COUNT: usize = 5;
10
11#[allow(dead_code)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HandSide {
15 Left,
16 Right,
17}
18
19#[allow(dead_code)]
21#[derive(Debug, Clone, PartialEq)]
22pub struct FingerSplayConfig {
23 pub max_angle_rad: f32,
24}
25
26impl Default for FingerSplayConfig {
27 fn default() -> Self {
28 Self {
29 max_angle_rad: FRAC_PI_6,
30 }
31 }
32}
33
34#[allow(dead_code)]
36#[derive(Debug, Clone)]
37pub struct FingerSplayState {
38 pub left: [f32; FINGER_COUNT],
39 pub right: [f32; FINGER_COUNT],
40}
41
42impl Default for FingerSplayState {
43 fn default() -> Self {
44 Self {
45 left: [0.0; FINGER_COUNT],
46 right: [0.0; FINGER_COUNT],
47 }
48 }
49}
50
51#[allow(dead_code)]
52pub fn new_finger_splay_state() -> FingerSplayState {
53 FingerSplayState::default()
54}
55
56#[allow(dead_code)]
57pub fn default_finger_splay_config() -> FingerSplayConfig {
58 FingerSplayConfig::default()
59}
60
61#[allow(dead_code)]
62pub fn hfs_set_finger(state: &mut FingerSplayState, side: HandSide, finger: usize, v: f32) {
63 if finger >= FINGER_COUNT {
64 return;
65 }
66 let v = v.clamp(0.0, 1.0);
67 match side {
68 HandSide::Left => state.left[finger] = v,
69 HandSide::Right => state.right[finger] = v,
70 }
71}
72
73#[allow(dead_code)]
74pub fn hfs_set_all(state: &mut FingerSplayState, side: HandSide, v: f32) {
75 let v = v.clamp(0.0, 1.0);
76 let arr = match side {
77 HandSide::Left => &mut state.left,
78 HandSide::Right => &mut state.right,
79 };
80 #[allow(clippy::needless_range_loop)]
81 for i in 0..FINGER_COUNT {
82 arr[i] = v;
83 }
84}
85
86#[allow(dead_code)]
87pub fn hfs_reset(state: &mut FingerSplayState) {
88 *state = FingerSplayState::default();
89}
90
91#[allow(dead_code)]
92pub fn hfs_is_neutral(state: &FingerSplayState) -> bool {
93 state.left.iter().all(|&v| v < 1e-4) && state.right.iter().all(|&v| v < 1e-4)
94}
95
96#[allow(dead_code)]
98pub fn hfs_average_angle_rad(
99 state: &FingerSplayState,
100 side: HandSide,
101 cfg: &FingerSplayConfig,
102) -> f32 {
103 let arr = match side {
104 HandSide::Left => &state.left,
105 HandSide::Right => &state.right,
106 };
107 let sum: f32 = arr.iter().sum();
108 (sum / FINGER_COUNT as f32) * cfg.max_angle_rad
109}
110
111#[allow(dead_code)]
113pub fn hfs_finger_angle_rad(
114 state: &FingerSplayState,
115 side: HandSide,
116 finger: usize,
117 cfg: &FingerSplayConfig,
118) -> f32 {
119 if finger >= FINGER_COUNT {
120 return 0.0;
121 }
122 let v = match side {
123 HandSide::Left => state.left[finger],
124 HandSide::Right => state.right[finger],
125 };
126 v * cfg.max_angle_rad
127}
128
129#[allow(dead_code)]
130pub fn hfs_blend(a: &FingerSplayState, b: &FingerSplayState, t: f32) -> FingerSplayState {
131 let t = t.clamp(0.0, 1.0);
132 let inv = 1.0 - t;
133 let mut left = [0.0f32; FINGER_COUNT];
134 let mut right = [0.0f32; FINGER_COUNT];
135 #[allow(clippy::needless_range_loop)]
136 for i in 0..FINGER_COUNT {
137 left[i] = a.left[i] * inv + b.left[i] * t;
138 right[i] = a.right[i] * inv + b.right[i] * t;
139 }
140 FingerSplayState { left, right }
141}
142
143#[allow(dead_code)]
144pub fn hfs_to_json(state: &FingerSplayState) -> String {
145 format!("{{\"left\":{:?},\"right\":{:?}}}", state.left, state.right)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn default_neutral() {
154 assert!(hfs_is_neutral(&new_finger_splay_state()));
155 }
156
157 #[test]
158 fn set_finger_clamps() {
159 let mut s = new_finger_splay_state();
160 hfs_set_finger(&mut s, HandSide::Left, 0, 5.0);
161 assert!((s.left[0] - 1.0).abs() < 1e-6);
162 }
163
164 #[test]
165 fn set_all_fills_array() {
166 let mut s = new_finger_splay_state();
167 hfs_set_all(&mut s, HandSide::Right, 0.7);
168 assert!(s.right.iter().all(|&v| (v - 0.7).abs() < 1e-6));
169 }
170
171 #[test]
172 fn out_of_range_finger_ignored() {
173 let mut s = new_finger_splay_state();
174 hfs_set_finger(&mut s, HandSide::Left, 99, 1.0);
175 assert!(hfs_is_neutral(&s));
176 }
177
178 #[test]
179 fn reset_clears() {
180 let mut s = new_finger_splay_state();
181 hfs_set_all(&mut s, HandSide::Left, 0.9);
182 hfs_reset(&mut s);
183 assert!(hfs_is_neutral(&s));
184 }
185
186 #[test]
187 fn average_angle_zero_at_neutral() {
188 let cfg = default_finger_splay_config();
189 let s = new_finger_splay_state();
190 assert!(hfs_average_angle_rad(&s, HandSide::Left, &cfg) < 1e-6);
191 }
192
193 #[test]
194 fn finger_angle_positive() {
195 let cfg = default_finger_splay_config();
196 let mut s = new_finger_splay_state();
197 hfs_set_finger(&mut s, HandSide::Left, 2, 1.0);
198 assert!(hfs_finger_angle_rad(&s, HandSide::Left, 2, &cfg) > 0.0);
199 }
200
201 #[test]
202 fn blend_midpoint() {
203 let mut b = new_finger_splay_state();
204 hfs_set_all(&mut b, HandSide::Left, 1.0);
205 let r = hfs_blend(&new_finger_splay_state(), &b, 0.5);
206 assert!((r.left[0] - 0.5).abs() < 1e-5);
207 }
208
209 #[test]
210 fn json_has_left_right() {
211 let j = hfs_to_json(&new_finger_splay_state());
212 assert!(j.contains("left") && j.contains("right"));
213 }
214
215 #[test]
216 fn finger_count_constant() {
217 assert_eq!(FINGER_COUNT, 5);
218 }
219}