Skip to main content

oxihuman_morph/
cheek_nasal_fold.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Cheek nasolabial fold depth control.
6
7use std::f32::consts::PI;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FoldSide {
12    Left,
13    Right,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct CheekNasalFoldConfig {
19    pub max_depth: f32,
20}
21
22impl Default for CheekNasalFoldConfig {
23    fn default() -> Self {
24        Self { max_depth: 1.0 }
25    }
26}
27
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct CheekNasalFoldState {
31    pub left: f32,
32    pub right: f32,
33    pub config: CheekNasalFoldConfig,
34}
35
36#[allow(dead_code)]
37pub fn default_cheek_nasal_fold_config() -> CheekNasalFoldConfig {
38    CheekNasalFoldConfig::default()
39}
40
41#[allow(dead_code)]
42pub fn new_cheek_nasal_fold_state(config: CheekNasalFoldConfig) -> CheekNasalFoldState {
43    CheekNasalFoldState {
44        left: 0.0,
45        right: 0.0,
46        config,
47    }
48}
49
50#[allow(dead_code)]
51pub fn cnf_set(state: &mut CheekNasalFoldState, side: FoldSide, v: f32) {
52    let v = v.clamp(0.0, state.config.max_depth);
53    match side {
54        FoldSide::Left => state.left = v,
55        FoldSide::Right => state.right = v,
56    }
57}
58
59#[allow(dead_code)]
60pub fn cnf_set_both(state: &mut CheekNasalFoldState, v: f32) {
61    let v = v.clamp(0.0, state.config.max_depth);
62    state.left = v;
63    state.right = v;
64}
65
66#[allow(dead_code)]
67pub fn cnf_reset(state: &mut CheekNasalFoldState) {
68    state.left = 0.0;
69    state.right = 0.0;
70}
71
72#[allow(dead_code)]
73pub fn cnf_is_neutral(state: &CheekNasalFoldState) -> bool {
74    state.left.abs() < 1e-6 && state.right.abs() < 1e-6
75}
76
77#[allow(dead_code)]
78pub fn cnf_average(state: &CheekNasalFoldState) -> f32 {
79    (state.left + state.right) * 0.5
80}
81
82#[allow(dead_code)]
83pub fn cnf_asymmetry(state: &CheekNasalFoldState) -> f32 {
84    (state.left - state.right).abs()
85}
86
87#[allow(dead_code)]
88pub fn cnf_fold_angle_rad(state: &CheekNasalFoldState) -> f32 {
89    cnf_average(state) * PI * 0.3
90}
91
92#[allow(dead_code)]
93pub fn cnf_to_weights(state: &CheekNasalFoldState) -> [f32; 2] {
94    let m = state.config.max_depth;
95    let n = |v: f32| if m > 1e-9 { v / m } else { 0.0 };
96    [n(state.left), n(state.right)]
97}
98
99#[allow(dead_code)]
100pub fn cnf_blend(a: &CheekNasalFoldState, b: &CheekNasalFoldState, t: f32) -> [f32; 2] {
101    let t = t.clamp(0.0, 1.0);
102    [
103        a.left * (1.0 - t) + b.left * t,
104        a.right * (1.0 - t) + b.right * t,
105    ]
106}
107
108#[allow(dead_code)]
109pub fn cnf_to_json(state: &CheekNasalFoldState) -> String {
110    format!(
111        "{{\"left\":{:.4},\"right\":{:.4}}}",
112        state.left, state.right
113    )
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn default_neutral() {
122        assert!(cnf_is_neutral(&new_cheek_nasal_fold_state(
123            default_cheek_nasal_fold_config()
124        )));
125    }
126    #[test]
127    fn set_clamps() {
128        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
129        cnf_set(&mut s, FoldSide::Left, 9.0);
130        assert!((0.0..=1.0).contains(&s.left));
131    }
132    #[test]
133    fn set_both_applies() {
134        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
135        cnf_set_both(&mut s, 0.5);
136        assert!((s.left - 0.5).abs() < 1e-5 && (s.right - 0.5).abs() < 1e-5);
137    }
138    #[test]
139    fn reset_zeroes() {
140        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
141        cnf_set_both(&mut s, 0.4);
142        cnf_reset(&mut s);
143        assert!(cnf_is_neutral(&s));
144    }
145    #[test]
146    fn average_mid() {
147        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
148        cnf_set(&mut s, FoldSide::Left, 0.2);
149        cnf_set(&mut s, FoldSide::Right, 0.6);
150        assert!((cnf_average(&s) - 0.4).abs() < 1e-5);
151    }
152    #[test]
153    fn asymmetry_abs_diff() {
154        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
155        cnf_set(&mut s, FoldSide::Left, 0.1);
156        cnf_set(&mut s, FoldSide::Right, 0.5);
157        assert!((cnf_asymmetry(&s) - 0.4).abs() < 1e-5);
158    }
159    #[test]
160    fn fold_angle_nonneg() {
161        let s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
162        assert!(cnf_fold_angle_rad(&s) >= 0.0);
163    }
164    #[test]
165    fn to_weights_one_at_max() {
166        let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
167        cnf_set(&mut s, FoldSide::Right, 1.0);
168        assert!((cnf_to_weights(&s)[1] - 1.0).abs() < 1e-5);
169    }
170    #[test]
171    fn blend_midpoint() {
172        let mut a = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
173        let b = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
174        cnf_set(&mut a, FoldSide::Left, 0.8);
175        let w = cnf_blend(&a, &b, 0.5);
176        assert!((w[0] - 0.4).abs() < 1e-5);
177    }
178    #[test]
179    fn to_json_has_right() {
180        let s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
181        assert!(cnf_to_json(&s).contains("\"right\""));
182    }
183}