1#![allow(dead_code)]
3
4use std::f32::consts::PI;
9
10#[allow(dead_code)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Finger {
14 Thumb,
15 Index,
16 Middle,
17 Ring,
18 Pinky,
19}
20
21#[allow(dead_code)]
23#[derive(Debug, Clone, PartialEq)]
24pub struct FingerSpreadParams {
25 pub spreads: [f32; 5],
27 pub global_scale: f32,
29 pub web_stretch: f32,
31}
32
33impl Default for FingerSpreadParams {
34 fn default() -> Self {
35 Self {
36 spreads: [0.0; 5],
37 global_scale: 1.0,
38 web_stretch: 0.3,
39 }
40 }
41}
42
43#[allow(dead_code)]
45#[derive(Debug, Clone)]
46pub struct FingerSpreadResult {
47 pub effective_spreads: [f32; 5],
49 pub width_change: f32,
51 pub web_weights: [f32; 4],
53}
54
55#[allow(dead_code)]
57pub fn max_spread(finger: Finger) -> f32 {
58 match finger {
59 Finger::Thumb => PI / 4.0,
60 Finger::Index => PI / 8.0,
61 Finger::Middle => PI / 12.0,
62 Finger::Ring => PI / 8.0,
63 Finger::Pinky => PI / 6.0,
64 }
65}
66
67#[allow(dead_code)]
69pub fn clamp_spread(angle: f32, finger: Finger) -> f32 {
70 let max = max_spread(finger);
71 angle.clamp(-max * 0.5, max)
72}
73
74#[allow(dead_code)]
76pub fn effective_spread(raw_angle: f32, finger: Finger, global_scale: f32) -> f32 {
77 clamp_spread(raw_angle * global_scale, finger)
78}
79
80#[allow(dead_code)]
84pub fn web_stretch_weight(spread_a: f32, spread_b: f32, max_a: f32, max_b: f32) -> f32 {
85 let diff = (spread_a - spread_b).abs();
86 let max_diff = (max_a + max_b) * 0.5;
87 if max_diff < 1e-6 {
88 return 0.0;
89 }
90 (diff / max_diff).clamp(0.0, 1.0)
91}
92
93#[allow(dead_code)]
95pub fn finger_from_index(idx: usize) -> Option<Finger> {
96 match idx {
97 0 => Some(Finger::Thumb),
98 1 => Some(Finger::Index),
99 2 => Some(Finger::Middle),
100 3 => Some(Finger::Ring),
101 4 => Some(Finger::Pinky),
102 _ => None,
103 }
104}
105
106#[allow(dead_code)]
108pub fn evaluate_finger_spread(params: &FingerSpreadParams) -> FingerSpreadResult {
109 let fingers = [
110 Finger::Thumb,
111 Finger::Index,
112 Finger::Middle,
113 Finger::Ring,
114 Finger::Pinky,
115 ];
116 let mut effective_spreads = [0.0_f32; 5];
117
118 for (i, &finger) in fingers.iter().enumerate() {
119 effective_spreads[i] = effective_spread(params.spreads[i], finger, params.global_scale);
120 }
121
122 let mut web_weights = [0.0_f32; 4];
123 for i in 0..4 {
124 let max_a = max_spread(fingers[i]);
125 let max_b = max_spread(fingers[i + 1]);
126 web_weights[i] =
127 web_stretch_weight(effective_spreads[i], effective_spreads[i + 1], max_a, max_b)
128 * params.web_stretch;
129 }
130
131 let width_change: f32 = effective_spreads.iter().map(|s| s.sin()).sum::<f32>() * 0.01;
132
133 FingerSpreadResult {
134 effective_spreads,
135 width_change,
136 web_weights,
137 }
138}
139
140#[allow(dead_code)]
142pub fn preset_relaxed() -> FingerSpreadParams {
143 FingerSpreadParams {
144 spreads: [0.1, 0.03, 0.0, -0.02, -0.05],
145 global_scale: 1.0,
146 web_stretch: 0.3,
147 }
148}
149
150#[allow(dead_code)]
152pub fn preset_wide() -> FingerSpreadParams {
153 FingerSpreadParams {
154 spreads: [PI / 5.0, PI / 10.0, PI / 14.0, PI / 10.0, PI / 8.0],
155 global_scale: 1.0,
156 web_stretch: 0.8,
157 }
158}
159
160#[allow(dead_code)]
162#[allow(clippy::needless_range_loop)]
163pub fn blend_finger_spread(
164 a: &FingerSpreadParams,
165 b: &FingerSpreadParams,
166 t: f32,
167) -> FingerSpreadParams {
168 let t = t.clamp(0.0, 1.0);
169 let inv = 1.0 - t;
170 let mut spreads = [0.0; 5];
171 for i in 0..5 {
172 spreads[i] = a.spreads[i] * inv + b.spreads[i] * t;
173 }
174 FingerSpreadParams {
175 spreads,
176 global_scale: a.global_scale * inv + b.global_scale * t,
177 web_stretch: a.web_stretch * inv + b.web_stretch * t,
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use std::f32::consts::PI;
185
186 #[test]
187 fn test_default_params() {
188 let p = FingerSpreadParams::default();
189 assert_eq!(p.spreads, [0.0; 5]);
190 }
191
192 #[test]
193 fn test_max_spread_thumb_largest() {
194 let thumb = max_spread(Finger::Thumb);
195 let middle = max_spread(Finger::Middle);
196 assert!(thumb > middle);
197 }
198
199 #[test]
200 fn test_clamp_spread() {
201 let clamped = clamp_spread(PI, Finger::Index);
202 assert!(clamped <= max_spread(Finger::Index));
203 }
204
205 #[test]
206 fn test_effective_spread_zero_scale() {
207 let e = effective_spread(0.5, Finger::Index, 0.0);
208 assert!(e.abs() < 1e-6);
209 }
210
211 #[test]
212 fn test_web_stretch_weight_same() {
213 let w = web_stretch_weight(0.1, 0.1, 0.5, 0.5);
214 assert!(w.abs() < 1e-6);
215 }
216
217 #[test]
218 fn test_finger_from_index_valid() {
219 assert_eq!(finger_from_index(0), Some(Finger::Thumb));
220 assert_eq!(finger_from_index(4), Some(Finger::Pinky));
221 }
222
223 #[test]
224 fn test_finger_from_index_invalid() {
225 assert_eq!(finger_from_index(5), None);
226 }
227
228 #[test]
229 fn test_evaluate_default() {
230 let r = evaluate_finger_spread(&FingerSpreadParams::default());
231 assert_eq!(r.effective_spreads, [0.0; 5]);
232 assert!(r.width_change.abs() < 1e-6);
233 }
234
235 #[test]
236 fn test_preset_wide_nonzero() {
237 let p = preset_wide();
238 let r = evaluate_finger_spread(&p);
239 assert!(r.width_change > 0.0);
240 }
241
242 #[test]
243 fn test_blend_finger_spread() {
244 let a = FingerSpreadParams::default();
245 let b = preset_wide();
246 let r = blend_finger_spread(&a, &b, 0.5);
247 assert!(r.spreads[0] > 0.0);
248 }
249}