oxihuman_morph/
nasolabial_fold_control.rs1#![allow(dead_code)]
3
4#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum NlSide {
10 Left,
11 Right,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone, PartialEq)]
17pub struct NasolabialFoldConfig {
18 pub max_depth: f32,
20 pub max_length: f32,
22}
23
24impl Default for NasolabialFoldConfig {
25 fn default() -> Self {
26 Self {
27 max_depth: 1.0,
28 max_length: 1.0,
29 }
30 }
31}
32
33#[allow(dead_code)]
35#[derive(Debug, Clone, Default)]
36pub struct NasolabialFoldState {
37 pub depth_left: f32,
39 pub depth_right: f32,
40 pub length_left: f32,
42 pub length_right: f32,
43}
44
45#[allow(dead_code)]
46pub fn new_nasolabial_fold_state() -> NasolabialFoldState {
47 NasolabialFoldState::default()
48}
49
50#[allow(dead_code)]
51pub fn default_nasolabial_fold_config() -> NasolabialFoldConfig {
52 NasolabialFoldConfig::default()
53}
54
55#[allow(dead_code)]
56pub fn nlf_set_depth(state: &mut NasolabialFoldState, side: NlSide, v: f32) {
57 let v = v.clamp(0.0, 1.0);
58 match side {
59 NlSide::Left => state.depth_left = v,
60 NlSide::Right => state.depth_right = v,
61 }
62}
63
64#[allow(dead_code)]
65pub fn nlf_set_length(state: &mut NasolabialFoldState, side: NlSide, v: f32) {
66 let v = v.clamp(0.0, 1.0);
67 match side {
68 NlSide::Left => state.length_left = v,
69 NlSide::Right => state.length_right = v,
70 }
71}
72
73#[allow(dead_code)]
74pub fn nlf_set_both(state: &mut NasolabialFoldState, depth: f32) {
75 let depth = depth.clamp(0.0, 1.0);
76 state.depth_left = depth;
77 state.depth_right = depth;
78}
79
80#[allow(dead_code)]
81pub fn nlf_reset(state: &mut NasolabialFoldState) {
82 *state = NasolabialFoldState::default();
83}
84
85#[allow(dead_code)]
86pub fn nlf_is_neutral(state: &NasolabialFoldState) -> bool {
87 state.depth_left < 1e-4 && state.depth_right < 1e-4
88}
89
90#[allow(dead_code)]
92pub fn nlf_asymmetry(state: &NasolabialFoldState) -> f32 {
93 (state.depth_left - state.depth_right).abs()
94}
95
96#[allow(dead_code)]
98pub fn nlf_depth(state: &NasolabialFoldState, side: NlSide, cfg: &NasolabialFoldConfig) -> f32 {
99 let v = match side {
100 NlSide::Left => state.depth_left,
101 NlSide::Right => state.depth_right,
102 };
103 v * cfg.max_depth
104}
105
106#[allow(dead_code)]
108pub fn nlf_to_weights(state: &NasolabialFoldState) -> [f32; 4] {
109 [
110 state.depth_left,
111 state.depth_right,
112 state.length_left,
113 state.length_right,
114 ]
115}
116
117#[allow(dead_code)]
118pub fn nlf_blend(a: &NasolabialFoldState, b: &NasolabialFoldState, t: f32) -> NasolabialFoldState {
119 let t = t.clamp(0.0, 1.0);
120 let inv = 1.0 - t;
121 NasolabialFoldState {
122 depth_left: a.depth_left * inv + b.depth_left * t,
123 depth_right: a.depth_right * inv + b.depth_right * t,
124 length_left: a.length_left * inv + b.length_left * t,
125 length_right: a.length_right * inv + b.length_right * t,
126 }
127}
128
129#[allow(dead_code)]
130pub fn nlf_to_json(state: &NasolabialFoldState) -> String {
131 format!(
132 "{{\"depth_l\":{:.4},\"depth_r\":{:.4}}}",
133 state.depth_left, state.depth_right
134 )
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn default_neutral() {
143 assert!(nlf_is_neutral(&new_nasolabial_fold_state()));
144 }
145
146 #[test]
147 fn depth_clamps_high() {
148 let mut s = new_nasolabial_fold_state();
149 nlf_set_depth(&mut s, NlSide::Left, 5.0);
150 assert!((s.depth_left - 1.0).abs() < 1e-6);
151 }
152
153 #[test]
154 fn depth_clamps_low() {
155 let mut s = new_nasolabial_fold_state();
156 nlf_set_depth(&mut s, NlSide::Right, -1.0);
157 assert!(s.depth_right < 1e-6);
158 }
159
160 #[test]
161 fn set_both_symmetric() {
162 let mut s = new_nasolabial_fold_state();
163 nlf_set_both(&mut s, 0.6);
164 assert!((s.depth_left - s.depth_right).abs() < 1e-6);
165 }
166
167 #[test]
168 fn reset_clears() {
169 let mut s = new_nasolabial_fold_state();
170 nlf_set_both(&mut s, 1.0);
171 nlf_reset(&mut s);
172 assert!(nlf_is_neutral(&s));
173 }
174
175 #[test]
176 fn asymmetry_nonzero_when_different() {
177 let mut s = new_nasolabial_fold_state();
178 nlf_set_depth(&mut s, NlSide::Left, 0.8);
179 nlf_set_depth(&mut s, NlSide::Right, 0.2);
180 assert!((nlf_asymmetry(&s) - 0.6).abs() < 1e-5);
181 }
182
183 #[test]
184 fn depth_scaled_by_config() {
185 let cfg = default_nasolabial_fold_config();
186 let mut s = new_nasolabial_fold_state();
187 nlf_set_depth(&mut s, NlSide::Left, 1.0);
188 assert!((nlf_depth(&s, NlSide::Left, &cfg) - 1.0).abs() < 1e-5);
189 }
190
191 #[test]
192 fn weights_four_elements() {
193 let w = nlf_to_weights(&new_nasolabial_fold_state());
194 assert_eq!(w.len(), 4);
195 }
196
197 #[test]
198 fn blend_midpoint() {
199 let mut b = new_nasolabial_fold_state();
200 nlf_set_both(&mut b, 1.0);
201 let r = nlf_blend(&new_nasolabial_fold_state(), &b, 0.5);
202 assert!((r.depth_left - 0.5).abs() < 1e-5);
203 }
204
205 #[test]
206 fn json_has_keys() {
207 let j = nlf_to_json(&new_nasolabial_fold_state());
208 assert!(j.contains("depth_l") && j.contains("depth_r"));
209 }
210}