oxihuman_morph/
ear_rim_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct EarRimConfig {
11 pub max_roll: f32,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum EarRimSide {
18 Left,
19 Right,
20}
21
22#[allow(dead_code)]
24#[derive(Debug, Clone)]
25pub struct EarRimState {
26 pub left_roll: f32,
27 pub right_roll: f32,
28 pub left_sharpness: f32,
29 pub right_sharpness: f32,
30}
31
32#[allow(dead_code)]
33pub fn default_ear_rim_config() -> EarRimConfig {
34 EarRimConfig { max_roll: 1.0 }
35}
36
37#[allow(dead_code)]
38pub fn new_ear_rim_state() -> EarRimState {
39 EarRimState {
40 left_roll: 0.0,
41 right_roll: 0.0,
42 left_sharpness: 0.0,
43 right_sharpness: 0.0,
44 }
45}
46
47#[allow(dead_code)]
48pub fn er_set_roll(state: &mut EarRimState, cfg: &EarRimConfig, side: EarRimSide, v: f32) {
49 let clamped = v.clamp(0.0, cfg.max_roll);
50 match side {
51 EarRimSide::Left => state.left_roll = clamped,
52 EarRimSide::Right => state.right_roll = clamped,
53 }
54}
55
56#[allow(dead_code)]
57pub fn er_set_sharpness(state: &mut EarRimState, side: EarRimSide, v: f32) {
58 let clamped = v.clamp(0.0, 1.0);
59 match side {
60 EarRimSide::Left => state.left_sharpness = clamped,
61 EarRimSide::Right => state.right_sharpness = clamped,
62 }
63}
64
65#[allow(dead_code)]
66pub fn er_set_both_roll(state: &mut EarRimState, cfg: &EarRimConfig, v: f32) {
67 let clamped = v.clamp(0.0, cfg.max_roll);
68 state.left_roll = clamped;
69 state.right_roll = clamped;
70}
71
72#[allow(dead_code)]
73pub fn er_reset(state: &mut EarRimState) {
74 *state = new_ear_rim_state();
75}
76
77#[allow(dead_code)]
78pub fn er_is_neutral(state: &EarRimState) -> bool {
79 let vals = [
80 state.left_roll,
81 state.right_roll,
82 state.left_sharpness,
83 state.right_sharpness,
84 ];
85 !vals.is_empty() && vals.iter().all(|v| v.abs() < 1e-6)
86}
87
88#[allow(dead_code)]
89pub fn er_average_roll(state: &EarRimState) -> f32 {
90 (state.left_roll + state.right_roll) * 0.5
91}
92
93#[allow(dead_code)]
94pub fn er_symmetry(state: &EarRimState) -> f32 {
95 (state.left_roll - state.right_roll).abs()
96}
97
98#[allow(dead_code)]
99pub fn er_blend(a: &EarRimState, b: &EarRimState, t: f32) -> EarRimState {
100 let t = t.clamp(0.0, 1.0);
101 EarRimState {
102 left_roll: a.left_roll + (b.left_roll - a.left_roll) * t,
103 right_roll: a.right_roll + (b.right_roll - a.right_roll) * t,
104 left_sharpness: a.left_sharpness + (b.left_sharpness - a.left_sharpness) * t,
105 right_sharpness: a.right_sharpness + (b.right_sharpness - a.right_sharpness) * t,
106 }
107}
108
109#[allow(dead_code)]
110pub fn er_to_weights(state: &EarRimState) -> Vec<(String, f32)> {
111 vec![
112 ("ear_rim_roll_l".to_string(), state.left_roll),
113 ("ear_rim_roll_r".to_string(), state.right_roll),
114 ("ear_rim_sharp_l".to_string(), state.left_sharpness),
115 ("ear_rim_sharp_r".to_string(), state.right_sharpness),
116 ]
117}
118
119#[allow(dead_code)]
120pub fn er_to_json(state: &EarRimState) -> String {
121 format!(
122 r#"{{"left_roll":{:.4},"right_roll":{:.4},"left_sharpness":{:.4},"right_sharpness":{:.4}}}"#,
123 state.left_roll, state.right_roll, state.left_sharpness, state.right_sharpness
124 )
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn default_config() {
133 let cfg = default_ear_rim_config();
134 assert!((cfg.max_roll - 1.0).abs() < 1e-6);
135 }
136
137 #[test]
138 fn new_state_neutral() {
139 let s = new_ear_rim_state();
140 assert!(er_is_neutral(&s));
141 }
142
143 #[test]
144 fn set_roll_left() {
145 let cfg = default_ear_rim_config();
146 let mut s = new_ear_rim_state();
147 er_set_roll(&mut s, &cfg, EarRimSide::Left, 0.5);
148 assert!((s.left_roll - 0.5).abs() < 1e-6);
149 assert_eq!(s.right_roll, 0.0);
150 }
151
152 #[test]
153 fn set_roll_clamps() {
154 let cfg = default_ear_rim_config();
155 let mut s = new_ear_rim_state();
156 er_set_roll(&mut s, &cfg, EarRimSide::Right, 5.0);
157 assert!((s.right_roll - 1.0).abs() < 1e-6);
158 }
159
160 #[test]
161 fn set_sharpness() {
162 let mut s = new_ear_rim_state();
163 er_set_sharpness(&mut s, EarRimSide::Left, 0.8);
164 assert!((s.left_sharpness - 0.8).abs() < 1e-6);
165 }
166
167 #[test]
168 fn set_both_roll_equal() {
169 let cfg = default_ear_rim_config();
170 let mut s = new_ear_rim_state();
171 er_set_both_roll(&mut s, &cfg, 0.6);
172 assert!((s.left_roll - 0.6).abs() < 1e-6);
173 assert!((s.right_roll - 0.6).abs() < 1e-6);
174 }
175
176 #[test]
177 fn symmetry_zero_when_equal() {
178 let cfg = default_ear_rim_config();
179 let mut s = new_ear_rim_state();
180 er_set_both_roll(&mut s, &cfg, 0.5);
181 assert!(er_symmetry(&s) < 1e-6);
182 }
183
184 #[test]
185 fn reset_clears() {
186 let cfg = default_ear_rim_config();
187 let mut s = new_ear_rim_state();
188 er_set_both_roll(&mut s, &cfg, 0.7);
189 er_reset(&mut s);
190 assert!(er_is_neutral(&s));
191 }
192
193 #[test]
194 fn blend_midpoint() {
195 let a = new_ear_rim_state();
196 let cfg = default_ear_rim_config();
197 let mut b = new_ear_rim_state();
198 er_set_both_roll(&mut b, &cfg, 1.0);
199 let mid = er_blend(&a, &b, 0.5);
200 assert!((mid.left_roll - 0.5).abs() < 1e-6);
201 }
202
203 #[test]
204 fn to_weights_count() {
205 let s = new_ear_rim_state();
206 assert_eq!(er_to_weights(&s).len(), 4);
207 }
208}