oxihuman_morph/
cheek_jowl_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum JowlSide {
11 Left,
12 Right,
13 Both,
14}
15
16#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct CheekJowlState {
20 pub sag_left: f32,
21 pub sag_right: f32,
22 pub volume_left: f32,
23 pub volume_right: f32,
24}
25
26#[allow(dead_code)]
28#[derive(Clone, Debug)]
29pub struct CheekJowlConfig {
30 pub max_sag: f32,
31 pub max_volume: f32,
32}
33
34impl Default for CheekJowlConfig {
35 fn default() -> Self {
36 Self {
37 max_sag: 1.0,
38 max_volume: 1.0,
39 }
40 }
41}
42
43impl Default for CheekJowlState {
44 fn default() -> Self {
45 Self {
46 sag_left: 0.0,
47 sag_right: 0.0,
48 volume_left: 0.0,
49 volume_right: 0.0,
50 }
51 }
52}
53
54#[allow(dead_code)]
55pub fn new_cheek_jowl_state() -> CheekJowlState {
56 CheekJowlState::default()
57}
58
59#[allow(dead_code)]
60pub fn default_cheek_jowl_config() -> CheekJowlConfig {
61 CheekJowlConfig::default()
62}
63
64#[allow(dead_code)]
65pub fn cj_set_sag(state: &mut CheekJowlState, cfg: &CheekJowlConfig, side: JowlSide, v: f32) {
66 let v = v.clamp(0.0, cfg.max_sag);
67 match side {
68 JowlSide::Left => state.sag_left = v,
69 JowlSide::Right => state.sag_right = v,
70 JowlSide::Both => {
71 state.sag_left = v;
72 state.sag_right = v;
73 }
74 }
75}
76
77#[allow(dead_code)]
78pub fn cj_set_volume(state: &mut CheekJowlState, cfg: &CheekJowlConfig, side: JowlSide, v: f32) {
79 let v = v.clamp(0.0, cfg.max_volume);
80 match side {
81 JowlSide::Left => state.volume_left = v,
82 JowlSide::Right => state.volume_right = v,
83 JowlSide::Both => {
84 state.volume_left = v;
85 state.volume_right = v;
86 }
87 }
88}
89
90#[allow(dead_code)]
91pub fn cj_reset(state: &mut CheekJowlState) {
92 *state = CheekJowlState::default();
93}
94
95#[allow(dead_code)]
96pub fn cj_is_neutral(state: &CheekJowlState) -> bool {
97 state.sag_left < 1e-4
98 && state.sag_right < 1e-4
99 && state.volume_left < 1e-4
100 && state.volume_right < 1e-4
101}
102
103#[allow(dead_code)]
104pub fn cj_blend(a: &CheekJowlState, b: &CheekJowlState, t: f32) -> CheekJowlState {
105 let t = t.clamp(0.0, 1.0);
106 CheekJowlState {
107 sag_left: a.sag_left + (b.sag_left - a.sag_left) * t,
108 sag_right: a.sag_right + (b.sag_right - a.sag_right) * t,
109 volume_left: a.volume_left + (b.volume_left - a.volume_left) * t,
110 volume_right: a.volume_right + (b.volume_right - a.volume_right) * t,
111 }
112}
113
114#[allow(dead_code)]
115pub fn cj_symmetry(state: &CheekJowlState) -> f32 {
116 1.0 - (state.sag_left - state.sag_right).abs().min(1.0)
117}
118
119#[allow(dead_code)]
120pub fn cj_total_volume(state: &CheekJowlState) -> f32 {
121 state.volume_left + state.volume_right
122}
123
124#[allow(dead_code)]
125pub fn cj_to_weights(state: &CheekJowlState) -> [f32; 4] {
126 [
127 state.sag_left,
128 state.sag_right,
129 state.volume_left,
130 state.volume_right,
131 ]
132}
133
134#[allow(dead_code)]
135pub fn cj_to_json(state: &CheekJowlState) -> String {
136 format!(
137 "{{\"sag_left\":{:.4},\"sag_right\":{:.4},\"vol_left\":{:.4},\"vol_right\":{:.4}}}",
138 state.sag_left, state.sag_right, state.volume_left, state.volume_right
139 )
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn default_neutral() {
148 assert!(cj_is_neutral(&new_cheek_jowl_state()));
149 }
150
151 #[test]
152 fn set_sag_clamps() {
153 let mut s = new_cheek_jowl_state();
154 let cfg = default_cheek_jowl_config();
155 cj_set_sag(&mut s, &cfg, JowlSide::Left, 5.0);
156 assert!(s.sag_left <= cfg.max_sag);
157 }
158
159 #[test]
160 fn set_both_sides() {
161 let mut s = new_cheek_jowl_state();
162 let cfg = default_cheek_jowl_config();
163 cj_set_sag(&mut s, &cfg, JowlSide::Both, 0.7);
164 assert!((s.sag_left - 0.7).abs() < 1e-5);
165 assert!((s.sag_right - 0.7).abs() < 1e-5);
166 }
167
168 #[test]
169 fn reset_neutral() {
170 let mut s = new_cheek_jowl_state();
171 let cfg = default_cheek_jowl_config();
172 cj_set_sag(&mut s, &cfg, JowlSide::Both, 0.5);
173 cj_reset(&mut s);
174 assert!(cj_is_neutral(&s));
175 }
176
177 #[test]
178 fn blend_t0_is_a() {
179 let mut a = new_cheek_jowl_state();
180 let b = new_cheek_jowl_state();
181 let cfg = default_cheek_jowl_config();
182 cj_set_sag(&mut a, &cfg, JowlSide::Left, 0.6);
183 let r = cj_blend(&a, &b, 0.0);
184 assert!((r.sag_left - 0.6).abs() < 1e-5);
185 }
186
187 #[test]
188 fn symmetry_symmetric() {
189 let s = new_cheek_jowl_state();
190 assert!((cj_symmetry(&s) - 1.0).abs() < 1e-5);
191 }
192
193 #[test]
194 fn total_volume_zero_by_default() {
195 assert!((cj_total_volume(&new_cheek_jowl_state())).abs() < 1e-5);
196 }
197
198 #[test]
199 fn weights_len() {
200 assert_eq!(cj_to_weights(&new_cheek_jowl_state()).len(), 4);
201 }
202
203 #[test]
204 fn json_contains_sag() {
205 let s = new_cheek_jowl_state();
206 assert!(cj_to_json(&s).contains("sag"));
207 }
208
209 #[test]
210 fn volume_not_negative() {
211 let mut s = new_cheek_jowl_state();
212 let cfg = default_cheek_jowl_config();
213 cj_set_volume(&mut s, &cfg, JowlSide::Right, -1.0);
214 assert!(s.volume_right >= 0.0);
215 }
216}