Skip to main content

oxihuman_morph/
face_width_v2_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Face width v2 control — bizygomatic and bigonial width scaling.
6
7use std::f32::consts::FRAC_1_SQRT_2;
8
9/// Configuration.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct FaceWidthV2Config {
13    /// Reference diagonal factor (informational).
14    pub diagonal_factor: f32,
15    /// Whether to link bizygomatic and bigonial together.
16    pub link_regions: bool,
17}
18
19impl Default for FaceWidthV2Config {
20    fn default() -> Self {
21        FaceWidthV2Config {
22            diagonal_factor: FRAC_1_SQRT_2,
23            link_regions: false,
24        }
25    }
26}
27
28/// Runtime state.
29#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct FaceWidthV2State {
32    /// Bizygomatic (cheekbone) width in `[0.0, 1.0]`.
33    bizygomatic: f32,
34    /// Bigonial (jaw) width in `[0.0, 1.0]`.
35    bigonial: f32,
36    /// Temporal width in `[0.0, 1.0]`.
37    temporal: f32,
38    config: FaceWidthV2Config,
39}
40
41/// Default config.
42pub fn default_face_width_v2_config() -> FaceWidthV2Config {
43    FaceWidthV2Config::default()
44}
45
46/// New neutral state.
47pub fn new_face_width_v2_state(config: FaceWidthV2Config) -> FaceWidthV2State {
48    FaceWidthV2State {
49        bizygomatic: 0.5,
50        bigonial: 0.5,
51        temporal: 0.5,
52        config,
53    }
54}
55
56/// Set bizygomatic width.
57pub fn fw2_set_bizygomatic(state: &mut FaceWidthV2State, v: f32) {
58    state.bizygomatic = v.clamp(0.0, 1.0);
59    if state.config.link_regions {
60        state.bigonial = state.bizygomatic;
61    }
62}
63
64/// Set bigonial (jaw) width.
65pub fn fw2_set_bigonial(state: &mut FaceWidthV2State, v: f32) {
66    state.bigonial = v.clamp(0.0, 1.0);
67}
68
69/// Set temporal width.
70pub fn fw2_set_temporal(state: &mut FaceWidthV2State, v: f32) {
71    state.temporal = v.clamp(0.0, 1.0);
72}
73
74/// Reset to neutral (0.5).
75pub fn fw2_reset(state: &mut FaceWidthV2State) {
76    state.bizygomatic = 0.5;
77    state.bigonial = 0.5;
78    state.temporal = 0.5;
79}
80
81/// True when all at 0.5 (neutral).
82pub fn fw2_is_neutral(state: &FaceWidthV2State) -> bool {
83    (state.bizygomatic - 0.5).abs() < 1e-5
84        && (state.bigonial - 0.5).abs() < 1e-5
85        && (state.temporal - 0.5).abs() < 1e-5
86}
87
88/// Weighted average width across regions.
89pub fn fw2_average_width(state: &FaceWidthV2State) -> f32 {
90    (state.bizygomatic * 0.5 + state.bigonial * 0.3 + state.temporal * 0.2).clamp(0.0, 1.0)
91}
92
93/// Morph weights: `[bizygomatic, bigonial, temporal]`.
94pub fn fw2_to_weights(state: &FaceWidthV2State) -> [f32; 3] {
95    [state.bizygomatic, state.bigonial, state.temporal]
96}
97
98/// Blend.
99pub fn fw2_blend(a: &FaceWidthV2State, b: &FaceWidthV2State, t: f32) -> FaceWidthV2State {
100    let t = t.clamp(0.0, 1.0);
101    FaceWidthV2State {
102        bizygomatic: a.bizygomatic + (b.bizygomatic - a.bizygomatic) * t,
103        bigonial: a.bigonial + (b.bigonial - a.bigonial) * t,
104        temporal: a.temporal + (b.temporal - a.temporal) * t,
105        config: a.config.clone(),
106    }
107}
108
109/// Serialise.
110pub fn fw2_to_json(state: &FaceWidthV2State) -> String {
111    format!(
112        r#"{{"bizygomatic":{:.4},"bigonial":{:.4},"temporal":{:.4}}}"#,
113        state.bizygomatic, state.bigonial, state.temporal
114    )
115}
116
117// ---------------------------------------------------------------------------
118// Tests
119// ---------------------------------------------------------------------------
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn make() -> FaceWidthV2State {
125        new_face_width_v2_state(default_face_width_v2_config())
126    }
127
128    #[test]
129    fn neutral_on_creation() {
130        assert!(fw2_is_neutral(&make()));
131    }
132
133    #[test]
134    fn set_bizygomatic_clamps() {
135        let mut s = make();
136        fw2_set_bizygomatic(&mut s, 5.0);
137        assert!((s.bizygomatic - 1.0).abs() < 1e-5);
138    }
139
140    #[test]
141    fn reset_restores_neutral() {
142        let mut s = make();
143        fw2_set_bizygomatic(&mut s, 0.1);
144        fw2_reset(&mut s);
145        assert!(fw2_is_neutral(&s));
146    }
147
148    #[test]
149    fn average_in_range() {
150        let s = make();
151        assert!((0.0..=1.0).contains(&fw2_average_width(&s)));
152    }
153
154    #[test]
155    fn weights_in_range() {
156        let s = make();
157        for v in fw2_to_weights(&s) {
158            assert!((0.0..=1.0).contains(&v));
159        }
160    }
161
162    #[test]
163    fn blend_at_one_is_b() {
164        let mut b = make();
165        fw2_set_bizygomatic(&mut b, 0.9);
166        let r = fw2_blend(&make(), &b, 1.0);
167        assert!((r.bizygomatic - 0.9).abs() < 1e-5);
168    }
169
170    #[test]
171    fn blend_midpoint() {
172        let mut a = make();
173        let mut b = make();
174        fw2_set_bizygomatic(&mut a, 0.0);
175        fw2_set_bizygomatic(&mut b, 1.0);
176        let m = fw2_blend(&a, &b, 0.5);
177        assert!((m.bizygomatic - 0.5).abs() < 1e-5);
178    }
179
180    #[test]
181    fn json_has_bizygomatic() {
182        assert!(fw2_to_json(&make()).contains("bizygomatic"));
183    }
184
185    #[test]
186    fn link_regions_propagates() {
187        let mut cfg = default_face_width_v2_config();
188        cfg.link_regions = true;
189        let mut s = new_face_width_v2_state(cfg);
190        fw2_set_bizygomatic(&mut s, 0.3);
191        assert!((s.bigonial - 0.3).abs() < 1e-5);
192    }
193}