Skip to main content

oxihuman_morph/
hand_grip_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hand grip control — finger curl and palm compression for grip poses.
6
7/// Configuration for hand grip.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct HandGripConfig {
11    pub max_curl: f32,
12}
13
14/// Side selector.
15#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum HandGripSide {
18    Left,
19    Right,
20}
21
22/// Runtime state.
23#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct HandGripState {
26    pub left_curl: f32,
27    pub right_curl: f32,
28    pub palm_compression: f32,
29}
30
31#[allow(dead_code)]
32pub fn default_hand_grip_config() -> HandGripConfig {
33    HandGripConfig { max_curl: 1.0 }
34}
35
36#[allow(dead_code)]
37pub fn new_hand_grip_state() -> HandGripState {
38    HandGripState {
39        left_curl: 0.0,
40        right_curl: 0.0,
41        palm_compression: 0.0,
42    }
43}
44
45#[allow(dead_code)]
46pub fn hg_set_curl(state: &mut HandGripState, cfg: &HandGripConfig, side: HandGripSide, v: f32) {
47    let clamped = v.clamp(0.0, cfg.max_curl);
48    match side {
49        HandGripSide::Left => state.left_curl = clamped,
50        HandGripSide::Right => state.right_curl = clamped,
51    }
52}
53
54#[allow(dead_code)]
55pub fn hg_set_both(state: &mut HandGripState, cfg: &HandGripConfig, v: f32) {
56    let clamped = v.clamp(0.0, cfg.max_curl);
57    state.left_curl = clamped;
58    state.right_curl = clamped;
59}
60
61#[allow(dead_code)]
62pub fn hg_set_palm_compression(state: &mut HandGripState, v: f32) {
63    state.palm_compression = v.clamp(0.0, 1.0);
64}
65
66#[allow(dead_code)]
67pub fn hg_reset(state: &mut HandGripState) {
68    *state = new_hand_grip_state();
69}
70
71#[allow(dead_code)]
72pub fn hg_is_neutral(state: &HandGripState) -> bool {
73    state.left_curl.abs() < 1e-6
74        && state.right_curl.abs() < 1e-6
75        && state.palm_compression.abs() < 1e-6
76}
77
78#[allow(dead_code)]
79pub fn hg_average_curl(state: &HandGripState) -> f32 {
80    (state.left_curl + state.right_curl) * 0.5
81}
82
83#[allow(dead_code)]
84pub fn hg_symmetry(state: &HandGripState) -> f32 {
85    (state.left_curl - state.right_curl).abs()
86}
87
88#[allow(dead_code)]
89pub fn hg_blend(a: &HandGripState, b: &HandGripState, t: f32) -> HandGripState {
90    let t = t.clamp(0.0, 1.0);
91    HandGripState {
92        left_curl: a.left_curl + (b.left_curl - a.left_curl) * t,
93        right_curl: a.right_curl + (b.right_curl - a.right_curl) * t,
94        palm_compression: a.palm_compression + (b.palm_compression - a.palm_compression) * t,
95    }
96}
97
98#[allow(dead_code)]
99pub fn hg_to_weights(state: &HandGripState) -> Vec<(String, f32)> {
100    vec![
101        ("hand_grip_curl_l".to_string(), state.left_curl),
102        ("hand_grip_curl_r".to_string(), state.right_curl),
103        ("palm_compression".to_string(), state.palm_compression),
104    ]
105}
106
107#[allow(dead_code)]
108pub fn hg_to_json(state: &HandGripState) -> String {
109    format!(
110        r#"{{"left_curl":{:.4},"right_curl":{:.4},"palm_compression":{:.4}}}"#,
111        state.left_curl, state.right_curl, state.palm_compression
112    )
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn default_config() {
121        let cfg = default_hand_grip_config();
122        assert!((cfg.max_curl - 1.0).abs() < 1e-6);
123    }
124
125    #[test]
126    fn new_state_neutral() {
127        let s = new_hand_grip_state();
128        assert!(hg_is_neutral(&s));
129    }
130
131    #[test]
132    fn set_curl_left() {
133        let cfg = default_hand_grip_config();
134        let mut s = new_hand_grip_state();
135        hg_set_curl(&mut s, &cfg, HandGripSide::Left, 0.7);
136        assert!((s.left_curl - 0.7).abs() < 1e-6);
137    }
138
139    #[test]
140    fn set_curl_clamps() {
141        let cfg = default_hand_grip_config();
142        let mut s = new_hand_grip_state();
143        hg_set_curl(&mut s, &cfg, HandGripSide::Right, 5.0);
144        assert!((s.right_curl - 1.0).abs() < 1e-6);
145    }
146
147    #[test]
148    fn set_both_symmetric() {
149        let cfg = default_hand_grip_config();
150        let mut s = new_hand_grip_state();
151        hg_set_both(&mut s, &cfg, 0.5);
152        assert!(hg_symmetry(&s) < 1e-6);
153    }
154
155    #[test]
156    fn set_palm_compression() {
157        let mut s = new_hand_grip_state();
158        hg_set_palm_compression(&mut s, 0.6);
159        assert!((s.palm_compression - 0.6).abs() < 1e-6);
160    }
161
162    #[test]
163    fn reset_clears() {
164        let cfg = default_hand_grip_config();
165        let mut s = new_hand_grip_state();
166        hg_set_both(&mut s, &cfg, 0.8);
167        hg_reset(&mut s);
168        assert!(hg_is_neutral(&s));
169    }
170
171    #[test]
172    fn blend_midpoint() {
173        let a = new_hand_grip_state();
174        let cfg = default_hand_grip_config();
175        let mut b = new_hand_grip_state();
176        hg_set_both(&mut b, &cfg, 1.0);
177        let m = hg_blend(&a, &b, 0.5);
178        assert!((m.left_curl - 0.5).abs() < 1e-6);
179    }
180
181    #[test]
182    fn to_weights_count() {
183        let s = new_hand_grip_state();
184        assert_eq!(hg_to_weights(&s).len(), 3);
185    }
186
187    #[test]
188    fn to_json_fields() {
189        let s = new_hand_grip_state();
190        let j = hg_to_json(&s);
191        assert!(j.contains("left_curl"));
192    }
193}