oxihuman_morph/
foot_toe_shape.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_6;
8
9pub const TOE_COUNT: usize = 5;
11
12#[allow(dead_code)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum FootSide {
15 Left,
16 Right,
17}
18
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct FootToeShapeConfig {
22 pub max_length: f32,
23}
24
25impl Default for FootToeShapeConfig {
26 fn default() -> Self {
27 Self { max_length: 1.0 }
28 }
29}
30
31#[allow(dead_code)]
32#[derive(Debug, Clone)]
33pub struct FootToeShapeState {
34 pub left_lengths: [f32; TOE_COUNT],
35 pub right_lengths: [f32; TOE_COUNT],
36 pub curl: f32,
37 pub config: FootToeShapeConfig,
38}
39
40#[allow(dead_code)]
41pub fn default_foot_toe_shape_config() -> FootToeShapeConfig {
42 FootToeShapeConfig::default()
43}
44
45#[allow(dead_code)]
46pub fn new_foot_toe_shape_state(config: FootToeShapeConfig) -> FootToeShapeState {
47 FootToeShapeState {
48 left_lengths: [0.0; TOE_COUNT],
49 right_lengths: [0.0; TOE_COUNT],
50 curl: 0.0,
51 config,
52 }
53}
54
55#[allow(dead_code)]
56pub fn fts_set_toe(state: &mut FootToeShapeState, side: FootSide, toe: usize, v: f32) {
57 if toe < TOE_COUNT {
58 let v = v.clamp(0.0, state.config.max_length);
59 match side {
60 FootSide::Left => state.left_lengths[toe] = v,
61 FootSide::Right => state.right_lengths[toe] = v,
62 }
63 }
64}
65
66#[allow(dead_code)]
67pub fn fts_set_all(state: &mut FootToeShapeState, v: f32) {
68 let v = v.clamp(0.0, state.config.max_length);
69 #[allow(clippy::needless_range_loop)]
70 for i in 0..TOE_COUNT {
71 state.left_lengths[i] = v;
72 state.right_lengths[i] = v;
73 }
74}
75
76#[allow(dead_code)]
77pub fn fts_set_curl(state: &mut FootToeShapeState, v: f32) {
78 state.curl = v.clamp(0.0, 1.0);
79}
80
81#[allow(dead_code)]
82pub fn fts_reset(state: &mut FootToeShapeState) {
83 state.left_lengths = [0.0; TOE_COUNT];
84 state.right_lengths = [0.0; TOE_COUNT];
85 state.curl = 0.0;
86}
87
88#[allow(dead_code)]
89pub fn fts_is_neutral(state: &FootToeShapeState) -> bool {
90 state.left_lengths.iter().all(|v| v.abs() < 1e-6)
91 && state.right_lengths.iter().all(|v| v.abs() < 1e-6)
92 && state.curl.abs() < 1e-6
93}
94
95#[allow(dead_code)]
96pub fn fts_average_length(state: &FootToeShapeState, side: FootSide) -> f32 {
97 let arr = match side {
98 FootSide::Left => &state.left_lengths,
99 FootSide::Right => &state.right_lengths,
100 };
101 arr.iter().sum::<f32>() / TOE_COUNT as f32
102}
103
104#[allow(dead_code)]
105pub fn fts_curl_angle_rad(state: &FootToeShapeState) -> f32 {
106 state.curl * FRAC_PI_6
107}
108
109#[allow(dead_code)]
110pub fn fts_to_weights(state: &FootToeShapeState) -> [f32; TOE_COUNT] {
111 let m = state.config.max_length;
112 let mut w = [0.0f32; TOE_COUNT];
113 #[allow(clippy::needless_range_loop)]
114 for i in 0..TOE_COUNT {
115 w[i] = if m > 1e-9 {
116 (state.left_lengths[i] + state.right_lengths[i]) * 0.5 / m
117 } else {
118 0.0
119 };
120 }
121 w
122}
123
124#[allow(dead_code)]
125pub fn fts_to_json(state: &FootToeShapeState) -> String {
126 format!("{{\"curl\":{:.4}}}", state.curl)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 #[test]
133 fn default_neutral() {
134 assert!(fts_is_neutral(&new_foot_toe_shape_state(
135 default_foot_toe_shape_config()
136 )));
137 }
138 #[test]
139 fn set_toe_clamps() {
140 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
141 fts_set_toe(&mut s, FootSide::Left, 0, 5.0);
142 assert!((0.0..=1.0).contains(&s.left_lengths[0]));
143 }
144 #[test]
145 fn set_all_applies() {
146 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
147 fts_set_all(&mut s, 0.4);
148 assert!(!s.left_lengths.is_empty() && (s.left_lengths[0] - 0.4).abs() < 1e-5);
149 }
150 #[test]
151 fn curl_clamps() {
152 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
153 fts_set_curl(&mut s, 2.0);
154 assert!((0.0..=1.0).contains(&s.curl));
155 }
156 #[test]
157 fn reset_zeroes() {
158 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
159 fts_set_all(&mut s, 0.5);
160 fts_reset(&mut s);
161 assert!(fts_is_neutral(&s));
162 }
163 #[test]
164 fn average_length_zero_by_default() {
165 let s = new_foot_toe_shape_state(default_foot_toe_shape_config());
166 assert!(fts_average_length(&s, FootSide::Left).abs() < 1e-6);
167 }
168 #[test]
169 fn curl_angle_nonneg() {
170 let s = new_foot_toe_shape_state(default_foot_toe_shape_config());
171 assert!(fts_curl_angle_rad(&s) >= 0.0);
172 }
173 #[test]
174 fn to_weights_max() {
175 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
176 fts_set_all(&mut s, 1.0);
177 let w = fts_to_weights(&s);
178 assert!((w[0] - 1.0).abs() < 1e-5);
179 }
180 #[test]
181 fn out_of_range_toe_ignored() {
182 let mut s = new_foot_toe_shape_state(default_foot_toe_shape_config());
183 fts_set_toe(&mut s, FootSide::Left, 99, 1.0);
184 assert!(fts_is_neutral(&s));
185 }
186 #[test]
187 fn to_json_has_curl() {
188 assert!(
189 fts_to_json(&new_foot_toe_shape_state(default_foot_toe_shape_config()))
190 .contains("\"curl\"")
191 );
192 }
193}