Skip to main content

oxihuman_morph/
hand_knuckle_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hand knuckle prominence and definition control.
6
7/// Finger index (0 = index, 1 = middle, 2 = ring, 3 = pinky).
8pub const FINGER_COUNT: usize = 4;
9
10/// State per hand (one hand, use two for bilateral).
11#[allow(dead_code)]
12#[derive(Clone, Debug)]
13pub struct KnuckleState {
14    /// Knuckle prominence for each finger [index, middle, ring, pinky] (0..1).
15    pub prominence: [f32; FINGER_COUNT],
16    /// Knuckle definition (sharpness) per finger (0..1).
17    pub definition: [f32; FINGER_COUNT],
18}
19
20/// Config.
21#[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}