Skip to main content

oxihuman_morph/
face_contour_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Face contour morph — controls the overall outline/silhouette of the face.
6
7/// Configuration for face contour control.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct FaceContourConfig {
11    pub max_scale: f32,
12}
13
14/// Runtime state.
15#[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}