oxihuman_morph/
face_contour_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct FaceContourConfig {
11 pub max_scale: f32,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct FaceContourState {
18 pub temporal_width: f32,
19 pub zygomatic_projection: f32,
20 pub mandible_flare: f32,
21 pub overall_taper: f32,
22}
23
24#[allow(dead_code)]
25pub fn default_face_contour_config() -> FaceContourConfig {
26 FaceContourConfig { max_scale: 1.0 }
27}
28
29#[allow(dead_code)]
30pub fn new_face_contour_state() -> FaceContourState {
31 FaceContourState {
32 temporal_width: 0.0,
33 zygomatic_projection: 0.0,
34 mandible_flare: 0.0,
35 overall_taper: 0.0,
36 }
37}
38
39#[allow(dead_code)]
40pub fn fc_set_temporal(state: &mut FaceContourState, cfg: &FaceContourConfig, v: f32) {
41 state.temporal_width = v.clamp(0.0, cfg.max_scale);
42}
43
44#[allow(dead_code)]
45pub fn fc_set_zygomatic(state: &mut FaceContourState, cfg: &FaceContourConfig, v: f32) {
46 state.zygomatic_projection = v.clamp(0.0, cfg.max_scale);
47}
48
49#[allow(dead_code)]
50pub fn fc_set_mandible(state: &mut FaceContourState, cfg: &FaceContourConfig, v: f32) {
51 state.mandible_flare = v.clamp(0.0, cfg.max_scale);
52}
53
54#[allow(dead_code)]
55pub fn fc_set_taper(state: &mut FaceContourState, cfg: &FaceContourConfig, v: f32) {
56 state.overall_taper = v.clamp(0.0, cfg.max_scale);
57}
58
59#[allow(dead_code)]
60pub fn fc_reset(state: &mut FaceContourState) {
61 *state = new_face_contour_state();
62}
63
64#[allow(dead_code)]
65pub fn fc_is_neutral(state: &FaceContourState) -> bool {
66 let vals = [
67 state.temporal_width,
68 state.zygomatic_projection,
69 state.mandible_flare,
70 state.overall_taper,
71 ];
72 !vals.is_empty() && vals.iter().all(|v| v.abs() < 1e-6)
73}
74
75#[allow(dead_code)]
76pub fn fc_contour_intensity(state: &FaceContourState) -> f32 {
77 let vals = [
78 state.temporal_width,
79 state.zygomatic_projection,
80 state.mandible_flare,
81 state.overall_taper,
82 ];
83 vals.iter().cloned().fold(0.0_f32, f32::max)
84}
85
86#[allow(dead_code)]
87pub fn fc_blend(a: &FaceContourState, b: &FaceContourState, t: f32) -> FaceContourState {
88 let t = t.clamp(0.0, 1.0);
89 FaceContourState {
90 temporal_width: a.temporal_width + (b.temporal_width - a.temporal_width) * t,
91 zygomatic_projection: a.zygomatic_projection
92 + (b.zygomatic_projection - a.zygomatic_projection) * t,
93 mandible_flare: a.mandible_flare + (b.mandible_flare - a.mandible_flare) * t,
94 overall_taper: a.overall_taper + (b.overall_taper - a.overall_taper) * t,
95 }
96}
97
98#[allow(dead_code)]
99pub fn fc_to_weights(state: &FaceContourState) -> Vec<(String, f32)> {
100 vec![
101 ("face_temporal_width".to_string(), state.temporal_width),
102 (
103 "face_zygomatic_proj".to_string(),
104 state.zygomatic_projection,
105 ),
106 ("face_mandible_flare".to_string(), state.mandible_flare),
107 ("face_overall_taper".to_string(), state.overall_taper),
108 ]
109}
110
111#[allow(dead_code)]
112pub fn fc_to_json(state: &FaceContourState) -> String {
113 format!(
114 r#"{{"temporal_width":{:.4},"zygomatic_projection":{:.4},"mandible_flare":{:.4},"overall_taper":{:.4}}}"#,
115 state.temporal_width, state.zygomatic_projection, state.mandible_flare, state.overall_taper
116 )
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn default_config() {
125 let cfg = default_face_contour_config();
126 assert!((cfg.max_scale - 1.0).abs() < 1e-6);
127 }
128
129 #[test]
130 fn new_state_neutral() {
131 let s = new_face_contour_state();
132 assert!(fc_is_neutral(&s));
133 }
134
135 #[test]
136 fn set_temporal_clamps() {
137 let cfg = default_face_contour_config();
138 let mut s = new_face_contour_state();
139 fc_set_temporal(&mut s, &cfg, 5.0);
140 assert!((s.temporal_width - 1.0).abs() < 1e-6);
141 }
142
143 #[test]
144 fn set_zygomatic() {
145 let cfg = default_face_contour_config();
146 let mut s = new_face_contour_state();
147 fc_set_zygomatic(&mut s, &cfg, 0.6);
148 assert!((s.zygomatic_projection - 0.6).abs() < 1e-6);
149 }
150
151 #[test]
152 fn set_mandible() {
153 let cfg = default_face_contour_config();
154 let mut s = new_face_contour_state();
155 fc_set_mandible(&mut s, &cfg, 0.4);
156 assert!((s.mandible_flare - 0.4).abs() < 1e-6);
157 }
158
159 #[test]
160 fn contour_intensity_max() {
161 let cfg = default_face_contour_config();
162 let mut s = new_face_contour_state();
163 fc_set_zygomatic(&mut s, &cfg, 0.9);
164 fc_set_temporal(&mut s, &cfg, 0.3);
165 assert!((fc_contour_intensity(&s) - 0.9).abs() < 1e-6);
166 }
167
168 #[test]
169 fn reset_clears() {
170 let cfg = default_face_contour_config();
171 let mut s = new_face_contour_state();
172 fc_set_taper(&mut s, &cfg, 0.5);
173 fc_reset(&mut s);
174 assert!(fc_is_neutral(&s));
175 }
176
177 #[test]
178 fn blend_midpoint() {
179 let a = new_face_contour_state();
180 let cfg = default_face_contour_config();
181 let mut b = new_face_contour_state();
182 fc_set_temporal(&mut b, &cfg, 1.0);
183 let mid = fc_blend(&a, &b, 0.5);
184 assert!((mid.temporal_width - 0.5).abs() < 1e-6);
185 }
186
187 #[test]
188 fn to_weights_count() {
189 let s = new_face_contour_state();
190 assert_eq!(fc_to_weights(&s).len(), 4);
191 }
192
193 #[test]
194 fn to_json_fields() {
195 let s = new_face_contour_state();
196 let j = fc_to_json(&s);
197 assert!(j.contains("temporal_width"));
198 assert!(j.contains("overall_taper"));
199 }
200}