Skip to main content

oxihuman_morph/
foot_width_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Foot-width (forefoot / heel) proportioning control.
6
7/// Side.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum FootSide {
11    Left,
12    Right,
13    Both,
14}
15
16/// State.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct FootWidthState {
20    pub forefoot_left: f32,
21    pub forefoot_right: f32,
22    pub heel_left: f32,
23    pub heel_right: f32,
24    pub arch_height: f32,
25}
26
27/// Config.
28#[allow(dead_code)]
29#[derive(Clone, Debug)]
30pub struct FootWidthConfig {
31    pub max_width: f32,
32    pub max_arch: f32,
33}
34
35impl Default for FootWidthConfig {
36    fn default() -> Self {
37        Self {
38            max_width: 1.0,
39            max_arch: 1.0,
40        }
41    }
42}
43impl Default for FootWidthState {
44    fn default() -> Self {
45        Self {
46            forefoot_left: 0.5,
47            forefoot_right: 0.5,
48            heel_left: 0.5,
49            heel_right: 0.5,
50            arch_height: 0.0,
51        }
52    }
53}
54
55#[allow(dead_code)]
56pub fn new_foot_width_state() -> FootWidthState {
57    FootWidthState::default()
58}
59
60#[allow(dead_code)]
61pub fn default_foot_width_config() -> FootWidthConfig {
62    FootWidthConfig::default()
63}
64
65#[allow(dead_code)]
66pub fn fw_set_forefoot(state: &mut FootWidthState, cfg: &FootWidthConfig, side: FootSide, v: f32) {
67    let v = v.clamp(0.0, cfg.max_width);
68    match side {
69        FootSide::Left => state.forefoot_left = v,
70        FootSide::Right => state.forefoot_right = v,
71        FootSide::Both => {
72            state.forefoot_left = v;
73            state.forefoot_right = v;
74        }
75    }
76}
77
78#[allow(dead_code)]
79pub fn fw_set_heel(state: &mut FootWidthState, cfg: &FootWidthConfig, side: FootSide, v: f32) {
80    let v = v.clamp(0.0, cfg.max_width);
81    match side {
82        FootSide::Left => state.heel_left = v,
83        FootSide::Right => state.heel_right = v,
84        FootSide::Both => {
85            state.heel_left = v;
86            state.heel_right = v;
87        }
88    }
89}
90
91#[allow(dead_code)]
92pub fn fw_set_arch(state: &mut FootWidthState, cfg: &FootWidthConfig, v: f32) {
93    state.arch_height = v.clamp(0.0, cfg.max_arch);
94}
95
96#[allow(dead_code)]
97pub fn fw_reset(state: &mut FootWidthState) {
98    *state = FootWidthState::default();
99}
100
101#[allow(dead_code)]
102pub fn fw_blend(a: &FootWidthState, b: &FootWidthState, t: f32) -> FootWidthState {
103    let t = t.clamp(0.0, 1.0);
104    FootWidthState {
105        forefoot_left: a.forefoot_left + (b.forefoot_left - a.forefoot_left) * t,
106        forefoot_right: a.forefoot_right + (b.forefoot_right - a.forefoot_right) * t,
107        heel_left: a.heel_left + (b.heel_left - a.heel_left) * t,
108        heel_right: a.heel_right + (b.heel_right - a.heel_right) * t,
109        arch_height: a.arch_height + (b.arch_height - a.arch_height) * t,
110    }
111}
112
113#[allow(dead_code)]
114pub fn fw_symmetry(state: &FootWidthState) -> f32 {
115    1.0 - (state.forefoot_left - state.forefoot_right).abs().min(1.0)
116}
117
118#[allow(dead_code)]
119pub fn fw_to_weights(state: &FootWidthState) -> [f32; 5] {
120    [
121        state.forefoot_left,
122        state.forefoot_right,
123        state.heel_left,
124        state.heel_right,
125        state.arch_height,
126    ]
127}
128
129#[allow(dead_code)]
130pub fn fw_to_json(state: &FootWidthState) -> String {
131    format!(
132        "{{\"ff_l\":{:.4},\"ff_r\":{:.4},\"heel_l\":{:.4},\"heel_r\":{:.4},\"arch\":{:.4}}}",
133        state.forefoot_left,
134        state.forefoot_right,
135        state.heel_left,
136        state.heel_right,
137        state.arch_height
138    )
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn default_half_width() {
147        let s = new_foot_width_state();
148        assert!((s.forefoot_left - 0.5).abs() < 1e-5);
149    }
150
151    #[test]
152    fn clamp_max() {
153        let mut s = new_foot_width_state();
154        let cfg = default_foot_width_config();
155        fw_set_forefoot(&mut s, &cfg, FootSide::Left, 5.0);
156        assert!(s.forefoot_left <= cfg.max_width);
157    }
158
159    #[test]
160    fn clamp_min() {
161        let mut s = new_foot_width_state();
162        let cfg = default_foot_width_config();
163        fw_set_heel(&mut s, &cfg, FootSide::Right, -1.0);
164        assert!(s.heel_right >= 0.0);
165    }
166
167    #[test]
168    fn both_sides_set() {
169        let mut s = new_foot_width_state();
170        let cfg = default_foot_width_config();
171        fw_set_forefoot(&mut s, &cfg, FootSide::Both, 0.8);
172        assert!((s.forefoot_left - s.forefoot_right).abs() < 1e-5);
173    }
174
175    #[test]
176    fn reset_default() {
177        let mut s = new_foot_width_state();
178        let cfg = default_foot_width_config();
179        fw_set_arch(&mut s, &cfg, 0.9);
180        fw_reset(&mut s);
181        assert!((s.arch_height).abs() < 1e-5);
182    }
183
184    #[test]
185    fn blend_half() {
186        let cfg = default_foot_width_config();
187        let mut a = new_foot_width_state();
188        let mut b = new_foot_width_state();
189        fw_set_forefoot(&mut a, &cfg, FootSide::Left, 0.0);
190        fw_set_forefoot(&mut b, &cfg, FootSide::Left, 1.0);
191        let m = fw_blend(&a, &b, 0.5);
192        assert!((m.forefoot_left - 0.5).abs() < 1e-4);
193    }
194
195    #[test]
196    fn symmetry_equal() {
197        let s = new_foot_width_state();
198        assert!((fw_symmetry(&s) - 1.0).abs() < 1e-5);
199    }
200
201    #[test]
202    fn weights_len() {
203        assert_eq!(fw_to_weights(&new_foot_width_state()).len(), 5);
204    }
205
206    #[test]
207    fn json_has_arch() {
208        assert!(fw_to_json(&new_foot_width_state()).contains("arch"));
209    }
210
211    #[test]
212    fn arch_clamp() {
213        let mut s = new_foot_width_state();
214        let cfg = default_foot_width_config();
215        fw_set_arch(&mut s, &cfg, -5.0);
216        assert!(s.arch_height >= 0.0);
217    }
218}