Skip to main content

oxihuman_morph/
hand_finger_splay.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Hand finger splay control — abduction spread of digits.
5
6use std::f32::consts::FRAC_PI_6;
7
8/// Number of fingers (index..pinky = 4, plus thumb).
9pub const FINGER_COUNT: usize = 5;
10
11/// Which hand.
12#[allow(dead_code)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HandSide {
15    Left,
16    Right,
17}
18
19/// Config.
20#[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/// Per-hand state: splay per finger, 0..=1.
35#[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/// Average splay angle in radians for one side.
97#[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/// Per-finger angle in radians.
112#[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}