oxihuman_morph/
cheek_sag_control.rs1#![allow(dead_code)]
3
4#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SagSide {
10 Left,
11 Right,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone, PartialEq)]
17pub struct CheekSagConfig {
18 pub max_sag_m: f32,
19}
20
21impl Default for CheekSagConfig {
22 fn default() -> Self {
23 Self { max_sag_m: 0.012 }
24 }
25}
26
27#[allow(dead_code)]
29#[derive(Debug, Clone, Default)]
30pub struct CheekSagState {
31 pub left: f32,
32 pub right: f32,
33}
34
35#[allow(dead_code)]
36pub fn new_cheek_sag_state() -> CheekSagState {
37 CheekSagState::default()
38}
39
40#[allow(dead_code)]
41pub fn default_cheek_sag_config() -> CheekSagConfig {
42 CheekSagConfig::default()
43}
44
45#[allow(dead_code)]
47pub fn csag_set(state: &mut CheekSagState, side: SagSide, v: f32) {
48 let v = v.clamp(0.0, 1.0);
49 match side {
50 SagSide::Left => state.left = v,
51 SagSide::Right => state.right = v,
52 }
53}
54
55#[allow(dead_code)]
57pub fn csag_set_both(state: &mut CheekSagState, v: f32) {
58 let v = v.clamp(0.0, 1.0);
59 state.left = v;
60 state.right = v;
61}
62
63#[allow(dead_code)]
65pub fn csag_reset(state: &mut CheekSagState) {
66 *state = CheekSagState::default();
67}
68
69#[allow(dead_code)]
71pub fn csag_is_neutral(state: &CheekSagState) -> bool {
72 state.left < 1e-4 && state.right < 1e-4
73}
74
75#[allow(dead_code)]
77pub fn csag_asymmetry(state: &CheekSagState) -> f32 {
78 (state.left - state.right).abs()
79}
80
81#[allow(dead_code)]
83pub fn csag_average(state: &CheekSagState) -> f32 {
84 (state.left + state.right) * 0.5
85}
86
87#[allow(dead_code)]
89pub fn csag_to_weights(state: &CheekSagState, cfg: &CheekSagConfig) -> [f32; 2] {
90 [state.left * cfg.max_sag_m, state.right * cfg.max_sag_m]
91}
92
93#[allow(dead_code)]
95pub fn csag_blend(a: &CheekSagState, b: &CheekSagState, t: f32) -> CheekSagState {
96 let t = t.clamp(0.0, 1.0);
97 let inv = 1.0 - t;
98 CheekSagState {
99 left: a.left * inv + b.left * t,
100 right: a.right * inv + b.right * t,
101 }
102}
103
104#[allow(dead_code)]
106pub fn csag_to_json(state: &CheekSagState) -> String {
107 format!(
108 "{{\"left\":{:.4},\"right\":{:.4}}}",
109 state.left, state.right
110 )
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn default_is_neutral() {
119 assert!(csag_is_neutral(&new_cheek_sag_state()));
120 }
121
122 #[test]
123 fn set_clamps_above_one() {
124 let mut s = new_cheek_sag_state();
125 csag_set(&mut s, SagSide::Left, 5.0);
126 assert!((s.left - 1.0).abs() < 1e-6);
127 }
128
129 #[test]
130 fn set_clamps_below_zero() {
131 let mut s = new_cheek_sag_state();
132 csag_set(&mut s, SagSide::Right, -1.0);
133 assert!(s.right < 1e-6);
134 }
135
136 #[test]
137 fn reset_clears() {
138 let mut s = new_cheek_sag_state();
139 csag_set_both(&mut s, 0.8);
140 csag_reset(&mut s);
141 assert!(csag_is_neutral(&s));
142 }
143
144 #[test]
145 fn asymmetry_when_unequal() {
146 let mut s = new_cheek_sag_state();
147 csag_set(&mut s, SagSide::Left, 1.0);
148 csag_set(&mut s, SagSide::Right, 0.0);
149 assert!(csag_asymmetry(&s) > 0.9);
150 }
151
152 #[test]
153 fn average_midpoint() {
154 let mut s = new_cheek_sag_state();
155 csag_set_both(&mut s, 0.4);
156 assert!((csag_average(&s) - 0.4).abs() < 1e-6);
157 }
158
159 #[test]
160 fn weights_proportional() {
161 let cfg = default_cheek_sag_config();
162 let mut s = new_cheek_sag_state();
163 csag_set_both(&mut s, 1.0);
164 let w = csag_to_weights(&s, &cfg);
165 assert!((w[0] - cfg.max_sag_m).abs() < 1e-6);
166 }
167
168 #[test]
169 fn blend_midpoint() {
170 let mut a = new_cheek_sag_state();
171 let mut b = new_cheek_sag_state();
172 csag_set_both(&mut a, 0.0);
173 csag_set_both(&mut b, 1.0);
174 let r = csag_blend(&a, &b, 0.5);
175 assert!((r.left - 0.5).abs() < 1e-5);
176 }
177
178 #[test]
179 fn json_has_keys() {
180 let j = csag_to_json(&new_cheek_sag_state());
181 assert!(j.contains("left") && j.contains("right"));
182 }
183
184 #[test]
185 fn set_both_equal() {
186 let mut s = new_cheek_sag_state();
187 csag_set_both(&mut s, 0.6);
188 assert!((s.left - s.right).abs() < 1e-6);
189 }
190}