Skip to main content

oxihuman_morph/
hand_width_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hand width control — transverse palm width scaling.
6
7/// Configuration.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct HandWidthConfig {
11    pub max_width: f32,
12}
13
14impl Default for HandWidthConfig {
15    fn default() -> Self {
16        HandWidthConfig { max_width: 1.0 }
17    }
18}
19
20/// Runtime state.
21#[allow(dead_code)]
22#[derive(Debug, Clone)]
23pub struct HandWidthState {
24    left: f32,
25    right: f32,
26    /// Finger-spread contribution in `[0.0, 1.0]`.
27    finger_spread: f32,
28    config: HandWidthConfig,
29}
30
31/// Default config.
32pub fn default_hand_width_config() -> HandWidthConfig {
33    HandWidthConfig::default()
34}
35
36/// New neutral state.
37pub fn new_hand_width_state(config: HandWidthConfig) -> HandWidthState {
38    HandWidthState {
39        left: 0.5,
40        right: 0.5,
41        finger_spread: 0.0,
42        config,
43    }
44}
45
46/// Set width for one hand.
47pub fn hwc_set_left(state: &mut HandWidthState, v: f32) {
48    state.left = v.clamp(0.0, 1.0);
49}
50
51/// Set width for right hand.
52pub fn hwc_set_right(state: &mut HandWidthState, v: f32) {
53    state.right = v.clamp(0.0, 1.0);
54}
55
56/// Set both hands.
57pub fn hwc_set_both(state: &mut HandWidthState, v: f32) {
58    let v = v.clamp(0.0, 1.0);
59    state.left = v;
60    state.right = v;
61}
62
63/// Set finger spread.
64pub fn hwc_set_finger_spread(state: &mut HandWidthState, v: f32) {
65    state.finger_spread = v.clamp(0.0, 1.0);
66}
67
68/// Reset to neutral (0.5).
69pub fn hwc_reset(state: &mut HandWidthState) {
70    state.left = 0.5;
71    state.right = 0.5;
72    state.finger_spread = 0.0;
73}
74
75/// True when neutral.
76pub fn hwc_is_neutral(state: &HandWidthState) -> bool {
77    (state.left - 0.5).abs() < 1e-5
78        && (state.right - 0.5).abs() < 1e-5
79        && state.finger_spread < 1e-5
80}
81
82/// Asymmetry between hands.
83pub fn hwc_asymmetry(state: &HandWidthState) -> f32 {
84    (state.left - state.right).abs()
85}
86
87/// Average width including finger spread contribution.
88pub fn hwc_effective_width(state: &HandWidthState) -> f32 {
89    let base = (state.left + state.right) * 0.5;
90    (base + state.finger_spread * 0.2).clamp(0.0, 1.0)
91}
92
93/// Morph weights: `[left, right, finger_spread]`.
94pub fn hwc_to_weights(state: &HandWidthState) -> [f32; 3] {
95    [state.left, state.right, state.finger_spread]
96}
97
98/// Blend.
99pub fn hwc_blend(a: &HandWidthState, b: &HandWidthState, t: f32) -> HandWidthState {
100    let t = t.clamp(0.0, 1.0);
101    HandWidthState {
102        left: a.left + (b.left - a.left) * t,
103        right: a.right + (b.right - a.right) * t,
104        finger_spread: a.finger_spread + (b.finger_spread - a.finger_spread) * t,
105        config: a.config.clone(),
106    }
107}
108
109/// Serialise.
110pub fn hwc_to_json(state: &HandWidthState) -> String {
111    format!(
112        r#"{{"left":{:.4},"right":{:.4},"finger_spread":{:.4}}}"#,
113        state.left, state.right, state.finger_spread
114    )
115}
116
117// ---------------------------------------------------------------------------
118// Tests
119// ---------------------------------------------------------------------------
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn make() -> HandWidthState {
125        new_hand_width_state(default_hand_width_config())
126    }
127
128    #[test]
129    fn neutral_on_creation() {
130        assert!(hwc_is_neutral(&make()));
131    }
132
133    #[test]
134    fn set_both_equal() {
135        let mut s = make();
136        hwc_set_both(&mut s, 0.8);
137        assert!((s.left - s.right).abs() < 1e-5);
138    }
139
140    #[test]
141    fn reset_restores_neutral() {
142        let mut s = make();
143        hwc_set_both(&mut s, 0.1);
144        hwc_reset(&mut s);
145        assert!(hwc_is_neutral(&s));
146    }
147
148    #[test]
149    fn asymmetry_zero_equal() {
150        let mut s = make();
151        hwc_set_both(&mut s, 0.7);
152        assert!(hwc_asymmetry(&s) < 1e-5);
153    }
154
155    #[test]
156    fn effective_width_in_range() {
157        let s = make();
158        assert!((0.0..=1.0).contains(&hwc_effective_width(&s)));
159    }
160
161    #[test]
162    fn weights_in_range() {
163        let s = make();
164        for v in hwc_to_weights(&s) {
165            assert!((0.0..=1.0).contains(&v));
166        }
167    }
168
169    #[test]
170    fn blend_midpoint() {
171        let mut a = make();
172        let mut b = make();
173        hwc_set_both(&mut a, 0.0);
174        hwc_set_both(&mut b, 1.0);
175        let m = hwc_blend(&a, &b, 0.5);
176        assert!((m.left - 0.5).abs() < 1e-5);
177    }
178
179    #[test]
180    fn json_has_left() {
181        assert!(hwc_to_json(&make()).contains("left"));
182    }
183
184    #[test]
185    fn finger_spread_clamped() {
186        let mut s = make();
187        hwc_set_finger_spread(&mut s, 2.0);
188        assert!((s.finger_spread - 1.0).abs() < 1e-5);
189    }
190}