oxihuman_morph/
face_symmetry_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::TAU;
8
9#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum AsymmetryAxis {
13 Horizontal,
14 Vertical,
15 Depth,
16}
17
18#[allow(dead_code)]
20#[derive(Clone, Debug)]
21pub struct AsymmetryEntry {
22 pub axis: AsymmetryAxis,
23 pub deviation: f32,
25}
26
27#[allow(dead_code)]
29#[derive(Clone, Debug, Default)]
30pub struct FaceSymmetryState {
31 pub entries: Vec<AsymmetryEntry>,
32 pub enforce_weight: f32,
34}
35
36#[allow(dead_code)]
38#[derive(Clone, Debug)]
39pub struct FaceSymmetryConfig {
40 pub max_deviation: f32,
41}
42
43impl Default for FaceSymmetryConfig {
44 fn default() -> Self {
45 Self { max_deviation: 1.0 }
46 }
47}
48
49#[allow(dead_code)]
50pub fn new_face_symmetry_state() -> FaceSymmetryState {
51 FaceSymmetryState::default()
52}
53
54#[allow(dead_code)]
55pub fn default_face_symmetry_config() -> FaceSymmetryConfig {
56 FaceSymmetryConfig::default()
57}
58
59#[allow(dead_code)]
60pub fn fs_set_deviation(
61 state: &mut FaceSymmetryState,
62 cfg: &FaceSymmetryConfig,
63 axis: AsymmetryAxis,
64 v: f32,
65) {
66 let v = v.clamp(-cfg.max_deviation, cfg.max_deviation);
67 if let Some(e) = state.entries.iter_mut().find(|e| e.axis == axis) {
68 e.deviation = v;
69 } else {
70 state.entries.push(AsymmetryEntry { axis, deviation: v });
71 }
72}
73
74#[allow(dead_code)]
75pub fn fs_set_enforce(state: &mut FaceSymmetryState, w: f32) {
76 state.enforce_weight = w.clamp(0.0, 1.0);
77}
78
79#[allow(dead_code)]
80pub fn fs_get_deviation(state: &FaceSymmetryState, axis: AsymmetryAxis) -> f32 {
81 state
82 .entries
83 .iter()
84 .find(|e| e.axis == axis)
85 .map(|e| e.deviation)
86 .unwrap_or(0.0)
87}
88
89#[allow(dead_code)]
90pub fn fs_reset(state: &mut FaceSymmetryState) {
91 state.entries.clear();
92 state.enforce_weight = 0.0;
93}
94
95#[allow(dead_code)]
96pub fn fs_is_symmetric(state: &FaceSymmetryState) -> bool {
97 state.entries.iter().all(|e| e.deviation.abs() < 1e-4)
98}
99
100#[allow(dead_code)]
101pub fn fs_total_deviation(state: &FaceSymmetryState) -> f32 {
102 state.entries.iter().map(|e| e.deviation.abs()).sum()
103}
104
105#[allow(dead_code)]
106pub fn fs_blend(a: &FaceSymmetryState, b: &FaceSymmetryState, t: f32) -> FaceSymmetryState {
107 let t = t.clamp(0.0, 1.0);
108 let axes = [
109 AsymmetryAxis::Horizontal,
110 AsymmetryAxis::Vertical,
111 AsymmetryAxis::Depth,
112 ];
113 let entries = axes
114 .iter()
115 .map(|&ax| {
116 let da = a
117 .entries
118 .iter()
119 .find(|e| e.axis == ax)
120 .map(|e| e.deviation)
121 .unwrap_or(0.0);
122 let db = b
123 .entries
124 .iter()
125 .find(|e| e.axis == ax)
126 .map(|e| e.deviation)
127 .unwrap_or(0.0);
128 AsymmetryEntry {
129 axis: ax,
130 deviation: da + (db - da) * t,
131 }
132 })
133 .collect();
134 FaceSymmetryState {
135 entries,
136 enforce_weight: a.enforce_weight + (b.enforce_weight - a.enforce_weight) * t,
137 }
138}
139
140#[allow(dead_code)]
142pub fn fs_circular_noise(seed: f32) -> f32 {
143 (seed * TAU).sin() * 0.5
144}
145
146#[allow(dead_code)]
147pub fn fs_to_json(state: &FaceSymmetryState) -> String {
148 let e: Vec<String> = state
149 .entries
150 .iter()
151 .map(|en| {
152 let ax = match en.axis {
153 AsymmetryAxis::Horizontal => "horizontal",
154 AsymmetryAxis::Vertical => "vertical",
155 AsymmetryAxis::Depth => "depth",
156 };
157 format!("{{\"axis\":\"{}\",\"dev\":{:.4}}}", ax, en.deviation)
158 })
159 .collect();
160 format!(
161 "{{\"entries\":[{}],\"enforce\":{:.4}}}",
162 e.join(","),
163 state.enforce_weight
164 )
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn default_symmetric() {
173 assert!(fs_is_symmetric(&new_face_symmetry_state()));
174 }
175
176 #[test]
177 fn set_deviation_stores() {
178 let mut s = new_face_symmetry_state();
179 let cfg = default_face_symmetry_config();
180 fs_set_deviation(&mut s, &cfg, AsymmetryAxis::Horizontal, 0.3);
181 assert!((fs_get_deviation(&s, AsymmetryAxis::Horizontal) - 0.3).abs() < 1e-5);
182 }
183
184 #[test]
185 fn deviation_clamp() {
186 let mut s = new_face_symmetry_state();
187 let cfg = default_face_symmetry_config();
188 fs_set_deviation(&mut s, &cfg, AsymmetryAxis::Vertical, 5.0);
189 assert!(fs_get_deviation(&s, AsymmetryAxis::Vertical) <= cfg.max_deviation);
190 }
191
192 #[test]
193 fn unknown_axis_zero() {
194 let s = new_face_symmetry_state();
195 assert!((fs_get_deviation(&s, AsymmetryAxis::Depth)).abs() < 1e-5);
196 }
197
198 #[test]
199 fn reset_clears() {
200 let mut s = new_face_symmetry_state();
201 let cfg = default_face_symmetry_config();
202 fs_set_deviation(&mut s, &cfg, AsymmetryAxis::Horizontal, 0.5);
203 fs_reset(&mut s);
204 assert!(fs_is_symmetric(&s));
205 }
206
207 #[test]
208 fn total_deviation_sum() {
209 let mut s = new_face_symmetry_state();
210 let cfg = default_face_symmetry_config();
211 fs_set_deviation(&mut s, &cfg, AsymmetryAxis::Horizontal, 0.5);
212 fs_set_deviation(&mut s, &cfg, AsymmetryAxis::Vertical, 0.5);
213 assert!((fs_total_deviation(&s) - 1.0).abs() < 1e-4);
214 }
215
216 #[test]
217 fn blend_midpoint() {
218 let cfg = default_face_symmetry_config();
219 let mut a = new_face_symmetry_state();
220 let mut b = new_face_symmetry_state();
221 fs_set_deviation(&mut a, &cfg, AsymmetryAxis::Depth, 0.0);
222 fs_set_deviation(&mut b, &cfg, AsymmetryAxis::Depth, 1.0);
223 let m = fs_blend(&a, &b, 0.5);
224 let d = m
225 .entries
226 .iter()
227 .find(|e| e.axis == AsymmetryAxis::Depth)
228 .map(|e| e.deviation)
229 .unwrap_or(0.0);
230 assert!((d - 0.5).abs() < 1e-4);
231 }
232
233 #[test]
234 fn circular_noise_bounded() {
235 let v = fs_circular_noise(0.25);
236 assert!((-1.0..=1.0).contains(&v));
237 }
238
239 #[test]
240 fn json_contains_enforce() {
241 assert!(fs_to_json(&new_face_symmetry_state()).contains("enforce"));
242 }
243
244 #[test]
245 fn enforce_weight_clamped() {
246 let mut s = new_face_symmetry_state();
247 fs_set_enforce(&mut s, 5.0);
248 assert!(s.enforce_weight <= 1.0);
249 }
250}