Skip to main content

oxihuman_morph/
foot_instep_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Foot instep (dorsal arch height) control.
5
6use std::f32::consts::FRAC_PI_4;
7
8/// Foot side.
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FootInstepSide {
12    Left,
13    Right,
14}
15
16/// Config.
17#[allow(dead_code)]
18#[derive(Debug, Clone, PartialEq)]
19pub struct FootInstepConfig {
20    pub max_arch_m: f32,
21}
22
23impl Default for FootInstepConfig {
24    fn default() -> Self {
25        Self { max_arch_m: 0.014 }
26    }
27}
28
29/// State.
30#[allow(dead_code)]
31#[derive(Debug, Clone, Default)]
32pub struct FootInstepState {
33    pub left_arch: f32,
34    pub right_arch: f32,
35}
36
37#[allow(dead_code)]
38pub fn new_foot_instep_state() -> FootInstepState {
39    FootInstepState::default()
40}
41
42#[allow(dead_code)]
43pub fn default_foot_instep_config() -> FootInstepConfig {
44    FootInstepConfig::default()
45}
46
47#[allow(dead_code)]
48pub fn fi_set_arch(state: &mut FootInstepState, side: FootInstepSide, v: f32) {
49    let v = v.clamp(0.0, 1.0);
50    match side {
51        FootInstepSide::Left => state.left_arch = v,
52        FootInstepSide::Right => state.right_arch = v,
53    }
54}
55
56#[allow(dead_code)]
57pub fn fi_set_both(state: &mut FootInstepState, v: f32) {
58    let v = v.clamp(0.0, 1.0);
59    state.left_arch = v;
60    state.right_arch = v;
61}
62
63#[allow(dead_code)]
64pub fn fi_reset(state: &mut FootInstepState) {
65    *state = FootInstepState::default();
66}
67
68#[allow(dead_code)]
69pub fn fi_is_neutral(state: &FootInstepState) -> bool {
70    state.left_arch < 1e-4 && state.right_arch < 1e-4
71}
72
73#[allow(dead_code)]
74pub fn fi_average_arch(state: &FootInstepState) -> f32 {
75    (state.left_arch + state.right_arch) * 0.5
76}
77
78/// Arch angle in radians.
79#[allow(dead_code)]
80pub fn fi_arch_angle_rad(state: &FootInstepState, side: FootInstepSide) -> f32 {
81    let v = match side {
82        FootInstepSide::Left => state.left_arch,
83        FootInstepSide::Right => state.right_arch,
84    };
85    v * FRAC_PI_4 * 0.5
86}
87
88#[allow(dead_code)]
89pub fn fi_to_weights(state: &FootInstepState, cfg: &FootInstepConfig) -> [f32; 2] {
90    [
91        state.left_arch * cfg.max_arch_m,
92        state.right_arch * cfg.max_arch_m,
93    ]
94}
95
96#[allow(dead_code)]
97pub fn fi_blend(a: &FootInstepState, b: &FootInstepState, t: f32) -> FootInstepState {
98    let t = t.clamp(0.0, 1.0);
99    let inv = 1.0 - t;
100    FootInstepState {
101        left_arch: a.left_arch * inv + b.left_arch * t,
102        right_arch: a.right_arch * inv + b.right_arch * t,
103    }
104}
105
106#[allow(dead_code)]
107pub fn fi_to_json(state: &FootInstepState) -> String {
108    format!(
109        "{{\"left_arch\":{:.4},\"right_arch\":{:.4}}}",
110        state.left_arch, state.right_arch
111    )
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn default_neutral() {
120        assert!(fi_is_neutral(&new_foot_instep_state()));
121    }
122
123    #[test]
124    fn set_clamps_high() {
125        let mut s = new_foot_instep_state();
126        fi_set_arch(&mut s, FootInstepSide::Left, 10.0);
127        assert!((s.left_arch - 1.0).abs() < 1e-6);
128    }
129
130    #[test]
131    fn set_clamps_low() {
132        let mut s = new_foot_instep_state();
133        fi_set_arch(&mut s, FootInstepSide::Right, -5.0);
134        assert!(s.right_arch < 1e-6);
135    }
136
137    #[test]
138    fn reset_clears() {
139        let mut s = new_foot_instep_state();
140        fi_set_both(&mut s, 0.7);
141        fi_reset(&mut s);
142        assert!(fi_is_neutral(&s));
143    }
144
145    #[test]
146    fn average_arch_correct() {
147        let mut s = new_foot_instep_state();
148        fi_set_arch(&mut s, FootInstepSide::Left, 0.4);
149        fi_set_arch(&mut s, FootInstepSide::Right, 0.6);
150        assert!((fi_average_arch(&s) - 0.5).abs() < 1e-5);
151    }
152
153    #[test]
154    fn arch_angle_positive() {
155        let mut s = new_foot_instep_state();
156        fi_set_arch(&mut s, FootInstepSide::Left, 1.0);
157        assert!(fi_arch_angle_rad(&s, FootInstepSide::Left) > 0.0);
158    }
159
160    #[test]
161    fn weights_scale_correctly() {
162        let cfg = default_foot_instep_config();
163        let mut s = new_foot_instep_state();
164        fi_set_both(&mut s, 1.0);
165        let w = fi_to_weights(&s, &cfg);
166        assert!((w[0] - cfg.max_arch_m).abs() < 1e-6);
167    }
168
169    #[test]
170    fn blend_midpoint() {
171        let mut b = new_foot_instep_state();
172        fi_set_both(&mut b, 1.0);
173        let r = fi_blend(&new_foot_instep_state(), &b, 0.5);
174        assert!((r.left_arch - 0.5).abs() < 1e-5);
175    }
176
177    #[test]
178    fn json_has_keys() {
179        let j = fi_to_json(&new_foot_instep_state());
180        assert!(j.contains("left_arch") && j.contains("right_arch"));
181    }
182
183    #[test]
184    fn set_both_equal() {
185        let mut s = new_foot_instep_state();
186        fi_set_both(&mut s, 0.3);
187        assert!((s.left_arch - s.right_arch).abs() < 1e-6);
188    }
189}