Skip to main content

oxihuman_morph/
foot_toe_shape.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Foot toe shape — controls toe length taper and curvature.
6
7use std::f32::consts::FRAC_PI_6;
8
9/// Number of toes per foot.
10pub 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}