oxihuman_morph/
foot_toe_spread.rs1#![allow(dead_code)]
4
5pub const TOE_COUNT: usize = 5;
9
10#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct FootToeSpreadConfig {
14 pub max_spread: f32,
15}
16
17#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct FootToeSpreadState {
21 pub left_spread: [f32; TOE_COUNT],
22 pub right_spread: [f32; TOE_COUNT],
23 pub left_curl: f32,
24 pub right_curl: f32,
25}
26
27#[allow(dead_code)]
28pub fn default_foot_toe_spread_config() -> FootToeSpreadConfig {
29 FootToeSpreadConfig { max_spread: 1.0 }
30}
31
32#[allow(dead_code)]
33pub fn new_foot_toe_spread_state() -> FootToeSpreadState {
34 FootToeSpreadState {
35 left_spread: [0.0; TOE_COUNT],
36 right_spread: [0.0; TOE_COUNT],
37 left_curl: 0.0,
38 right_curl: 0.0,
39 }
40}
41
42#[allow(dead_code)]
43pub fn fts_set_left_all(state: &mut FootToeSpreadState, cfg: &FootToeSpreadConfig, v: f32) {
44 let clamped = v.clamp(0.0, cfg.max_spread);
45 #[allow(clippy::needless_range_loop)]
46 for i in 0..TOE_COUNT {
47 state.left_spread[i] = clamped;
48 }
49}
50
51#[allow(dead_code)]
52pub fn fts_set_right_all(state: &mut FootToeSpreadState, cfg: &FootToeSpreadConfig, v: f32) {
53 let clamped = v.clamp(0.0, cfg.max_spread);
54 #[allow(clippy::needless_range_loop)]
55 for i in 0..TOE_COUNT {
56 state.right_spread[i] = clamped;
57 }
58}
59
60#[allow(dead_code)]
61pub fn fts_set_toe(
62 state: &mut FootToeSpreadState,
63 cfg: &FootToeSpreadConfig,
64 left: bool,
65 toe: usize,
66 v: f32,
67) {
68 if toe >= TOE_COUNT {
69 return;
70 }
71 let clamped = v.clamp(0.0, cfg.max_spread);
72 if left {
73 state.left_spread[toe] = clamped;
74 } else {
75 state.right_spread[toe] = clamped;
76 }
77}
78
79#[allow(dead_code)]
80pub fn fts_set_curl(state: &mut FootToeSpreadState, left_curl: f32, right_curl: f32) {
81 state.left_curl = left_curl.clamp(0.0, 1.0);
82 state.right_curl = right_curl.clamp(0.0, 1.0);
83}
84
85#[allow(dead_code)]
86pub fn fts_reset(state: &mut FootToeSpreadState) {
87 *state = new_foot_toe_spread_state();
88}
89
90#[allow(dead_code)]
91pub fn fts_is_neutral(state: &FootToeSpreadState) -> bool {
92 let left_zero =
93 !state.left_spread.is_empty() && state.left_spread.iter().all(|v| v.abs() < 1e-6);
94 let right_zero =
95 !state.right_spread.is_empty() && state.right_spread.iter().all(|v| v.abs() < 1e-6);
96 left_zero && right_zero && state.left_curl.abs() < 1e-6 && state.right_curl.abs() < 1e-6
97}
98
99#[allow(dead_code)]
100pub fn fts_average_spread(state: &FootToeSpreadState) -> f32 {
101 let total: f32 = state.left_spread.iter().sum::<f32>() + state.right_spread.iter().sum::<f32>();
102 total / (2 * TOE_COUNT) as f32
103}
104
105#[allow(dead_code)]
106pub fn fts_blend(a: &FootToeSpreadState, b: &FootToeSpreadState, t: f32) -> FootToeSpreadState {
107 let t = t.clamp(0.0, 1.0);
108 let mut ls = [0.0f32; TOE_COUNT];
109 let mut rs = [0.0f32; TOE_COUNT];
110 #[allow(clippy::needless_range_loop)]
111 for i in 0..TOE_COUNT {
112 ls[i] = a.left_spread[i] + (b.left_spread[i] - a.left_spread[i]) * t;
113 rs[i] = a.right_spread[i] + (b.right_spread[i] - a.right_spread[i]) * t;
114 }
115 FootToeSpreadState {
116 left_spread: ls,
117 right_spread: rs,
118 left_curl: a.left_curl + (b.left_curl - a.left_curl) * t,
119 right_curl: a.right_curl + (b.right_curl - a.right_curl) * t,
120 }
121}
122
123#[allow(dead_code)]
124pub fn fts_to_weights(state: &FootToeSpreadState) -> Vec<(String, f32)> {
125 let mut out = Vec::with_capacity(TOE_COUNT * 2 + 2);
126 #[allow(clippy::needless_range_loop)]
127 for i in 0..TOE_COUNT {
128 out.push((format!("toe_spread_l_{i}"), state.left_spread[i]));
129 out.push((format!("toe_spread_r_{i}"), state.right_spread[i]));
130 }
131 out.push(("toe_curl_l".to_string(), state.left_curl));
132 out.push(("toe_curl_r".to_string(), state.right_curl));
133 out
134}
135
136#[allow(dead_code)]
137pub fn fts_to_json(state: &FootToeSpreadState) -> String {
138 format!(
139 r#"{{"left_avg":{:.4},"right_avg":{:.4},"left_curl":{:.4},"right_curl":{:.4}}}"#,
140 state.left_spread.iter().sum::<f32>() / TOE_COUNT as f32,
141 state.right_spread.iter().sum::<f32>() / TOE_COUNT as f32,
142 state.left_curl,
143 state.right_curl
144 )
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn default_config() {
153 let cfg = default_foot_toe_spread_config();
154 assert!((cfg.max_spread - 1.0).abs() < 1e-6);
155 }
156
157 #[test]
158 fn new_state_neutral() {
159 let s = new_foot_toe_spread_state();
160 assert!(fts_is_neutral(&s));
161 }
162
163 #[test]
164 fn set_left_all_clamps() {
165 let cfg = default_foot_toe_spread_config();
166 let mut s = new_foot_toe_spread_state();
167 fts_set_left_all(&mut s, &cfg, 5.0);
168 assert!(s.left_spread.iter().all(|&v| (v - 1.0).abs() < 1e-6));
169 }
170
171 #[test]
172 fn set_right_all() {
173 let cfg = default_foot_toe_spread_config();
174 let mut s = new_foot_toe_spread_state();
175 fts_set_right_all(&mut s, &cfg, 0.5);
176 assert!(s.right_spread.iter().all(|&v| (v - 0.5).abs() < 1e-6));
177 }
178
179 #[test]
180 fn set_single_toe() {
181 let cfg = default_foot_toe_spread_config();
182 let mut s = new_foot_toe_spread_state();
183 fts_set_toe(&mut s, &cfg, true, 2, 0.7);
184 assert!((s.left_spread[2] - 0.7).abs() < 1e-6);
185 }
186
187 #[test]
188 fn set_curl() {
189 let mut s = new_foot_toe_spread_state();
190 fts_set_curl(&mut s, 0.3, 0.6);
191 assert!((s.left_curl - 0.3).abs() < 1e-6);
192 assert!((s.right_curl - 0.6).abs() < 1e-6);
193 }
194
195 #[test]
196 fn average_spread() {
197 let cfg = default_foot_toe_spread_config();
198 let mut s = new_foot_toe_spread_state();
199 fts_set_left_all(&mut s, &cfg, 1.0);
200 fts_set_right_all(&mut s, &cfg, 1.0);
201 assert!((fts_average_spread(&s) - 1.0).abs() < 1e-6);
202 }
203
204 #[test]
205 fn reset_clears() {
206 let cfg = default_foot_toe_spread_config();
207 let mut s = new_foot_toe_spread_state();
208 fts_set_left_all(&mut s, &cfg, 0.8);
209 fts_reset(&mut s);
210 assert!(fts_is_neutral(&s));
211 }
212
213 #[test]
214 fn blend_midpoint() {
215 let a = new_foot_toe_spread_state();
216 let cfg = default_foot_toe_spread_config();
217 let mut b = new_foot_toe_spread_state();
218 fts_set_left_all(&mut b, &cfg, 1.0);
219 let mid = fts_blend(&a, &b, 0.5);
220 assert!((mid.left_spread[0] - 0.5).abs() < 1e-6);
221 }
222
223 #[test]
224 fn to_weights_count() {
225 let s = new_foot_toe_spread_state();
226 assert_eq!(fts_to_weights(&s).len(), TOE_COUNT * 2 + 2);
227 }
228
229 #[test]
230 fn to_json_fields() {
231 let s = new_foot_toe_spread_state();
232 let j = fts_to_json(&s);
233 assert!(j.contains("left_avg"));
234 assert!(j.contains("right_curl"));
235 }
236}