Skip to main content

oxihuman_morph/
finger_joint_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6//! Finger joint morph control: adjusts finger bend and spread.
7
8use std::f32::consts::PI;
9
10/// Which finger.
11#[allow(dead_code)]
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub enum Finger {
14    Thumb,
15    Index,
16    Middle,
17    Ring,
18    Pinky,
19}
20
21/// Runtime state for finger joints.
22#[allow(dead_code)]
23#[derive(Debug, Clone)]
24pub struct FingerJointState {
25    pub curl: [f32; 5],
26    pub spread: [f32; 5],
27    pub stiffness: f32,
28}
29
30#[allow(dead_code)]
31pub fn new_finger_joint_state() -> FingerJointState {
32    FingerJointState {
33        curl: [0.0; 5],
34        spread: [0.0; 5],
35        stiffness: 0.5,
36    }
37}
38
39#[allow(dead_code)]
40pub fn fj_finger_index(finger: Finger) -> usize {
41    match finger {
42        Finger::Thumb => 0,
43        Finger::Index => 1,
44        Finger::Middle => 2,
45        Finger::Ring => 3,
46        Finger::Pinky => 4,
47    }
48}
49
50#[allow(dead_code)]
51pub fn fj_set_curl(state: &mut FingerJointState, finger: Finger, v: f32) {
52    let idx = fj_finger_index(finger);
53    state.curl[idx] = v.clamp(0.0, 1.0);
54}
55
56#[allow(dead_code)]
57pub fn fj_set_spread(state: &mut FingerJointState, finger: Finger, v: f32) {
58    let idx = fj_finger_index(finger);
59    state.spread[idx] = v.clamp(-1.0, 1.0);
60}
61
62#[allow(dead_code)]
63pub fn fj_set_all_curl(state: &mut FingerJointState, v: f32) {
64    let clamped = v.clamp(0.0, 1.0);
65    #[allow(clippy::needless_range_loop)]
66    for i in 0..5 {
67        state.curl[i] = clamped;
68    }
69}
70
71#[allow(dead_code)]
72pub fn fj_set_stiffness(state: &mut FingerJointState, v: f32) {
73    state.stiffness = v.clamp(0.0, 1.0);
74}
75
76#[allow(dead_code)]
77pub fn fj_reset(state: &mut FingerJointState) {
78    *state = new_finger_joint_state();
79}
80
81#[allow(dead_code)]
82pub fn fj_curl_angle(state: &FingerJointState, finger: Finger) -> f32 {
83    let idx = fj_finger_index(finger);
84    state.curl[idx] * PI * 0.5
85}
86
87#[allow(dead_code)]
88pub fn fj_to_json(state: &FingerJointState) -> String {
89    format!(
90        r#"{{"curl":[{:.4},{:.4},{:.4},{:.4},{:.4}],"stiffness":{:.4}}}"#,
91        state.curl[0], state.curl[1], state.curl[2], state.curl[3], state.curl[4], state.stiffness
92    )
93}
94
95#[allow(dead_code)]
96pub fn fj_blend(a: &FingerJointState, b: &FingerJointState, t: f32) -> FingerJointState {
97    let t = t.clamp(0.0, 1.0);
98    let mut result = new_finger_joint_state();
99    #[allow(clippy::needless_range_loop)]
100    for i in 0..5 {
101        result.curl[i] = a.curl[i] + (b.curl[i] - a.curl[i]) * t;
102        result.spread[i] = a.spread[i] + (b.spread[i] - a.spread[i]) * t;
103    }
104    result.stiffness = a.stiffness + (b.stiffness - a.stiffness) * t;
105    result
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_new_state() {
114        let s = new_finger_joint_state();
115        assert!(s.curl[0].abs() < 1e-6);
116        assert!((s.stiffness - 0.5).abs() < 1e-6);
117    }
118
119    #[test]
120    fn test_finger_index() {
121        assert_eq!(fj_finger_index(Finger::Thumb), 0);
122        assert_eq!(fj_finger_index(Finger::Pinky), 4);
123    }
124
125    #[test]
126    fn test_set_curl() {
127        let mut s = new_finger_joint_state();
128        fj_set_curl(&mut s, Finger::Index, 0.8);
129        assert!((s.curl[1] - 0.8).abs() < 1e-6);
130    }
131
132    #[test]
133    fn test_set_curl_clamps() {
134        let mut s = new_finger_joint_state();
135        fj_set_curl(&mut s, Finger::Middle, 5.0);
136        assert!((s.curl[2] - 1.0).abs() < 1e-6);
137    }
138
139    #[test]
140    fn test_set_spread() {
141        let mut s = new_finger_joint_state();
142        fj_set_spread(&mut s, Finger::Ring, -0.5);
143        assert!((s.spread[3] + 0.5).abs() < 1e-6);
144    }
145
146    #[test]
147    fn test_set_all_curl() {
148        let mut s = new_finger_joint_state();
149        fj_set_all_curl(&mut s, 0.7);
150        for c in &s.curl {
151            assert!((*c - 0.7).abs() < 1e-6);
152        }
153    }
154
155    #[test]
156    fn test_reset() {
157        let mut s = new_finger_joint_state();
158        fj_set_curl(&mut s, Finger::Thumb, 0.9);
159        fj_reset(&mut s);
160        assert!(s.curl[0].abs() < 1e-6);
161    }
162
163    #[test]
164    fn test_curl_angle() {
165        let mut s = new_finger_joint_state();
166        fj_set_curl(&mut s, Finger::Index, 1.0);
167        let angle = fj_curl_angle(&s, Finger::Index);
168        assert!((angle - PI * 0.5).abs() < 1e-6);
169    }
170
171    #[test]
172    fn test_to_json() {
173        let s = new_finger_joint_state();
174        let j = fj_to_json(&s);
175        assert!(j.contains("curl"));
176        assert!(j.contains("stiffness"));
177    }
178
179    #[test]
180    fn test_blend() {
181        let a = new_finger_joint_state();
182        let mut b = new_finger_joint_state();
183        b.curl[0] = 1.0;
184        let mid = fj_blend(&a, &b, 0.5);
185        assert!((mid.curl[0] - 0.5).abs() < 1e-6);
186    }
187}