Skip to main content

oxihuman_morph/
foot_toe_spread.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Foot toe spread morph — controls how spread out the toes are.
6
7/// Number of toes on each foot.
8pub const TOE_COUNT: usize = 5;
9
10/// Configuration for foot toe spread.
11#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct FootToeSpreadConfig {
14    pub max_spread: f32,
15}
16
17/// Runtime state.
18#[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}