Skip to main content

oxihuman_morph/
foot_heel_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Foot heel control — heel pad thickness and calcaneus prominence.
6
7/// Which foot.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FootSide {
11    Left,
12    Right,
13}
14
15/// Configuration.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct FootHeelConfig {
19    pub max_pad: f32,
20}
21
22impl Default for FootHeelConfig {
23    fn default() -> Self {
24        FootHeelConfig { max_pad: 1.0 }
25    }
26}
27
28/// State per foot.
29#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct FootHeelEntry {
32    pub pad: f32,
33    pub calcaneus: f32,
34}
35
36/// Runtime state.
37#[allow(dead_code)]
38#[derive(Debug, Clone)]
39pub struct FootHeelState {
40    left: FootHeelEntry,
41    right: FootHeelEntry,
42    config: FootHeelConfig,
43}
44
45/// Default config.
46pub fn default_foot_heel_config() -> FootHeelConfig {
47    FootHeelConfig::default()
48}
49
50/// New neutral state.
51pub fn new_foot_heel_state(config: FootHeelConfig) -> FootHeelState {
52    FootHeelState {
53        left: FootHeelEntry {
54            pad: 0.5,
55            calcaneus: 0.0,
56        },
57        right: FootHeelEntry {
58            pad: 0.5,
59            calcaneus: 0.0,
60        },
61        config,
62    }
63}
64
65fn entry_mut(state: &mut FootHeelState, side: FootSide) -> &mut FootHeelEntry {
66    match side {
67        FootSide::Left => &mut state.left,
68        FootSide::Right => &mut state.right,
69    }
70}
71
72fn entry_ref(state: &FootHeelState, side: FootSide) -> &FootHeelEntry {
73    match side {
74        FootSide::Left => &state.left,
75        FootSide::Right => &state.right,
76    }
77}
78
79/// Set heel pad thickness for a side.
80pub fn fhc_set_pad(state: &mut FootHeelState, side: FootSide, v: f32) {
81    entry_mut(state, side).pad = v.clamp(0.0, 1.0);
82}
83
84/// Set calcaneus prominence for a side.
85pub fn fhc_set_calcaneus(state: &mut FootHeelState, side: FootSide, v: f32) {
86    entry_mut(state, side).calcaneus = v.clamp(0.0, 1.0);
87}
88
89/// Set both sides to the same pad value.
90pub fn fhc_set_both_pad(state: &mut FootHeelState, v: f32) {
91    let v = v.clamp(0.0, 1.0);
92    state.left.pad = v;
93    state.right.pad = v;
94}
95
96/// Reset.
97pub fn fhc_reset(state: &mut FootHeelState) {
98    state.left = FootHeelEntry {
99        pad: 0.5,
100        calcaneus: 0.0,
101    };
102    state.right = FootHeelEntry {
103        pad: 0.5,
104        calcaneus: 0.0,
105    };
106}
107
108/// True if both heels are at neutral.
109pub fn fhc_is_neutral(state: &FootHeelState) -> bool {
110    (state.left.pad - 0.5).abs() < 1e-5
111        && (state.right.pad - 0.5).abs() < 1e-5
112        && state.left.calcaneus < 1e-5
113        && state.right.calcaneus < 1e-5
114}
115
116/// Pad value for one side.
117pub fn fhc_pad(state: &FootHeelState, side: FootSide) -> f32 {
118    entry_ref(state, side).pad
119}
120
121/// Asymmetry of pad thickness.
122pub fn fhc_pad_asymmetry(state: &FootHeelState) -> f32 {
123    (state.left.pad - state.right.pad).abs()
124}
125
126/// Morph weights as flat array: `[left_pad, right_pad, left_cal, right_cal]`.
127pub fn fhc_to_weights(state: &FootHeelState) -> [f32; 4] {
128    [
129        state.left.pad,
130        state.right.pad,
131        state.left.calcaneus,
132        state.right.calcaneus,
133    ]
134}
135
136/// Serialise.
137pub fn fhc_to_json(state: &FootHeelState) -> String {
138    format!(
139        r#"{{"left_pad":{:.4},"right_pad":{:.4},"left_cal":{:.4},"right_cal":{:.4}}}"#,
140        state.left.pad, state.right.pad, state.left.calcaneus, state.right.calcaneus
141    )
142}
143
144// ---------------------------------------------------------------------------
145// Tests
146// ---------------------------------------------------------------------------
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    fn make() -> FootHeelState {
152        new_foot_heel_state(default_foot_heel_config())
153    }
154
155    #[test]
156    fn neutral_on_creation() {
157        assert!(fhc_is_neutral(&make()));
158    }
159
160    #[test]
161    fn set_pad_clamps_high() {
162        let mut s = make();
163        fhc_set_pad(&mut s, FootSide::Left, 10.0);
164        assert!((s.left.pad - 1.0).abs() < 1e-5);
165    }
166
167    #[test]
168    fn set_both_pads_equal() {
169        let mut s = make();
170        fhc_set_both_pad(&mut s, 0.7);
171        assert!((s.left.pad - s.right.pad).abs() < 1e-5);
172    }
173
174    #[test]
175    fn reset_restores_neutral() {
176        let mut s = make();
177        fhc_set_pad(&mut s, FootSide::Left, 1.0);
178        fhc_reset(&mut s);
179        assert!(fhc_is_neutral(&s));
180    }
181
182    #[test]
183    fn pad_asymmetry_zero_when_equal() {
184        let mut s = make();
185        fhc_set_both_pad(&mut s, 0.6);
186        assert!(fhc_pad_asymmetry(&s) < 1e-5);
187    }
188
189    #[test]
190    fn weights_in_range() {
191        let s = make();
192        for v in fhc_to_weights(&s) {
193            assert!((0.0..=1.0).contains(&v));
194        }
195    }
196
197    #[test]
198    fn json_has_left_pad() {
199        assert!(fhc_to_json(&make()).contains("left_pad"));
200    }
201
202    #[test]
203    fn calcaneus_clamped() {
204        let mut s = make();
205        fhc_set_calcaneus(&mut s, FootSide::Right, -1.0);
206        assert!(s.right.calcaneus >= 0.0);
207    }
208
209    #[test]
210    fn pad_returns_correct_side() {
211        let mut s = make();
212        fhc_set_pad(&mut s, FootSide::Right, 0.3);
213        assert!((fhc_pad(&s, FootSide::Right) - 0.3).abs() < 1e-5);
214    }
215}