Skip to main content

oxihuman_morph/
face_symmetry_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Face symmetry / asymmetry injection control.
6
7use std::f32::consts::TAU;
8
9/// Axes of asymmetry.
10#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum AsymmetryAxis {
13    Horizontal,
14    Vertical,
15    Depth,
16}
17
18/// A single asymmetry override.
19#[allow(dead_code)]
20#[derive(Clone, Debug)]
21pub struct AsymmetryEntry {
22    pub axis: AsymmetryAxis,
23    /// Deviation amount (-1..1).
24    pub deviation: f32,
25}
26
27/// Face symmetry state.
28#[allow(dead_code)]
29#[derive(Clone, Debug, Default)]
30pub struct FaceSymmetryState {
31    pub entries: Vec<AsymmetryEntry>,
32    /// Overall symmetry enforcement (1.0 = fully symmetric, 0.0 = unmodified).
33    pub enforce_weight: f32,
34}
35
36/// Config.
37#[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/// Circular noise for organic-feeling asymmetry (uses TAU).
141#[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}