Skip to main content

oxihuman_morph/
face_depth_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Face anterior-posterior depth (protrusion) control.
6
7use std::f32::consts::FRAC_PI_4;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct FaceDepthConfig {
12    pub max_depth: f32,
13    pub min_depth: f32,
14}
15
16impl Default for FaceDepthConfig {
17    fn default() -> Self {
18        Self {
19            max_depth: 1.0,
20            min_depth: -1.0,
21        }
22    }
23}
24
25#[allow(dead_code)]
26#[derive(Debug, Clone)]
27pub struct FaceDepthState {
28    pub upper: f32,
29    pub middle: f32,
30    pub lower: f32,
31    pub config: FaceDepthConfig,
32}
33
34#[allow(dead_code)]
35pub fn default_face_depth_config() -> FaceDepthConfig {
36    FaceDepthConfig::default()
37}
38
39#[allow(dead_code)]
40pub fn new_face_depth_state(config: FaceDepthConfig) -> FaceDepthState {
41    FaceDepthState {
42        upper: 0.0,
43        middle: 0.0,
44        lower: 0.0,
45        config,
46    }
47}
48
49#[allow(dead_code)]
50pub fn fdc_set_upper(state: &mut FaceDepthState, v: f32) {
51    state.upper = v.clamp(state.config.min_depth, state.config.max_depth);
52}
53
54#[allow(dead_code)]
55pub fn fdc_set_middle(state: &mut FaceDepthState, v: f32) {
56    state.middle = v.clamp(state.config.min_depth, state.config.max_depth);
57}
58
59#[allow(dead_code)]
60pub fn fdc_set_lower(state: &mut FaceDepthState, v: f32) {
61    state.lower = v.clamp(state.config.min_depth, state.config.max_depth);
62}
63
64#[allow(dead_code)]
65pub fn fdc_set_all(state: &mut FaceDepthState, v: f32) {
66    let v = v.clamp(state.config.min_depth, state.config.max_depth);
67    state.upper = v;
68    state.middle = v;
69    state.lower = v;
70}
71
72#[allow(dead_code)]
73pub fn fdc_reset(state: &mut FaceDepthState) {
74    state.upper = 0.0;
75    state.middle = 0.0;
76    state.lower = 0.0;
77}
78
79#[allow(dead_code)]
80pub fn fdc_is_neutral(state: &FaceDepthState) -> bool {
81    state.upper.abs() < 1e-6 && state.middle.abs() < 1e-6 && state.lower.abs() < 1e-6
82}
83
84#[allow(dead_code)]
85pub fn fdc_average(state: &FaceDepthState) -> f32 {
86    (state.upper + state.middle + state.lower) / 3.0
87}
88
89#[allow(dead_code)]
90pub fn fdc_range(state: &FaceDepthState) -> f32 {
91    let max = state.upper.max(state.middle).max(state.lower);
92    let min = state.upper.min(state.middle).min(state.lower);
93    max - min
94}
95
96#[allow(dead_code)]
97pub fn fdc_profile_angle_rad(state: &FaceDepthState) -> f32 {
98    fdc_average(state) * FRAC_PI_4
99}
100
101#[allow(dead_code)]
102pub fn fdc_to_weights(state: &FaceDepthState) -> [f32; 3] {
103    let m = state.config.max_depth;
104    let n = |v: f32| {
105        if m > 1e-9 {
106            (v / m).clamp(-1.0, 1.0)
107        } else {
108            0.0
109        }
110    };
111    [n(state.upper), n(state.middle), n(state.lower)]
112}
113
114#[allow(dead_code)]
115pub fn fdc_blend(a: &FaceDepthState, b: &FaceDepthState, t: f32) -> [f32; 3] {
116    let t = t.clamp(0.0, 1.0);
117    [
118        a.upper * (1.0 - t) + b.upper * t,
119        a.middle * (1.0 - t) + b.middle * t,
120        a.lower * (1.0 - t) + b.lower * t,
121    ]
122}
123
124#[allow(dead_code)]
125pub fn fdc_to_json(state: &FaceDepthState) -> String {
126    format!(
127        "{{\"upper\":{:.4},\"middle\":{:.4},\"lower\":{:.4}}}",
128        state.upper, state.middle, state.lower
129    )
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    #[test]
136    fn default_neutral() {
137        assert!(fdc_is_neutral(&new_face_depth_state(
138            default_face_depth_config()
139        )));
140    }
141    #[test]
142    fn set_upper_clamps() {
143        let mut s = new_face_depth_state(default_face_depth_config());
144        fdc_set_upper(&mut s, 5.0);
145        assert!((0.0..=1.0).contains(&s.upper));
146    }
147    #[test]
148    fn set_all_applies() {
149        let mut s = new_face_depth_state(default_face_depth_config());
150        fdc_set_all(&mut s, 0.5);
151        assert!((s.upper - 0.5).abs() < 1e-5 && (s.lower - 0.5).abs() < 1e-5);
152    }
153    #[test]
154    fn reset_zeroes() {
155        let mut s = new_face_depth_state(default_face_depth_config());
156        fdc_set_all(&mut s, 0.5);
157        fdc_reset(&mut s);
158        assert!(fdc_is_neutral(&s));
159    }
160    #[test]
161    fn average_three_vals() {
162        let mut s = new_face_depth_state(default_face_depth_config());
163        fdc_set_upper(&mut s, 0.3);
164        fdc_set_middle(&mut s, 0.6);
165        fdc_set_lower(&mut s, 0.9);
166        assert!((fdc_average(&s) - 0.6).abs() < 1e-5);
167    }
168    #[test]
169    fn range_max_minus_min() {
170        let mut s = new_face_depth_state(default_face_depth_config());
171        fdc_set_upper(&mut s, 0.2);
172        fdc_set_middle(&mut s, 0.2);
173        fdc_set_lower(&mut s, 0.8);
174        assert!((fdc_range(&s) - 0.6).abs() < 1e-5);
175    }
176    #[test]
177    fn profile_angle_nonneg() {
178        let s = new_face_depth_state(default_face_depth_config());
179        assert!(fdc_profile_angle_rad(&s) >= -1.0);
180    }
181    #[test]
182    fn to_weights_at_max() {
183        let mut s = new_face_depth_state(default_face_depth_config());
184        fdc_set_upper(&mut s, 1.0);
185        assert!((fdc_to_weights(&s)[0] - 1.0).abs() < 1e-5);
186    }
187    #[test]
188    fn blend_mid() {
189        let mut a = new_face_depth_state(default_face_depth_config());
190        let b = new_face_depth_state(default_face_depth_config());
191        fdc_set_upper(&mut a, 0.4);
192        let w = fdc_blend(&a, &b, 0.5);
193        assert!((w[0] - 0.2).abs() < 1e-5);
194    }
195    #[test]
196    fn to_json_has_middle() {
197        assert!(
198            fdc_to_json(&new_face_depth_state(default_face_depth_config())).contains("\"middle\"")
199        );
200    }
201}