Skip to main content

oxihuman_morph/
nasolabial_fold_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Nasolabial fold control — nasolabial fold depth morph (cheek-to-mouth crease).
5
6/// Which side of the face.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum NlSide {
10    Left,
11    Right,
12}
13
14/// Config.
15#[allow(dead_code)]
16#[derive(Debug, Clone, PartialEq)]
17pub struct NasolabialFoldConfig {
18    /// Maximum depth displacement in normalised units.
19    pub max_depth: f32,
20    /// Maximum length influence along the fold.
21    pub max_length: f32,
22}
23
24impl Default for NasolabialFoldConfig {
25    fn default() -> Self {
26        Self {
27            max_depth: 1.0,
28            max_length: 1.0,
29        }
30    }
31}
32
33/// Nasolabial fold state.
34#[allow(dead_code)]
35#[derive(Debug, Clone, Default)]
36pub struct NasolabialFoldState {
37    /// Fold depth: 0 (flat) .. 1 (deep crease).
38    pub depth_left: f32,
39    pub depth_right: f32,
40    /// Fold length emphasis: 0..=1.
41    pub length_left: f32,
42    pub length_right: f32,
43}
44
45#[allow(dead_code)]
46pub fn new_nasolabial_fold_state() -> NasolabialFoldState {
47    NasolabialFoldState::default()
48}
49
50#[allow(dead_code)]
51pub fn default_nasolabial_fold_config() -> NasolabialFoldConfig {
52    NasolabialFoldConfig::default()
53}
54
55#[allow(dead_code)]
56pub fn nlf_set_depth(state: &mut NasolabialFoldState, side: NlSide, v: f32) {
57    let v = v.clamp(0.0, 1.0);
58    match side {
59        NlSide::Left => state.depth_left = v,
60        NlSide::Right => state.depth_right = v,
61    }
62}
63
64#[allow(dead_code)]
65pub fn nlf_set_length(state: &mut NasolabialFoldState, side: NlSide, v: f32) {
66    let v = v.clamp(0.0, 1.0);
67    match side {
68        NlSide::Left => state.length_left = v,
69        NlSide::Right => state.length_right = v,
70    }
71}
72
73#[allow(dead_code)]
74pub fn nlf_set_both(state: &mut NasolabialFoldState, depth: f32) {
75    let depth = depth.clamp(0.0, 1.0);
76    state.depth_left = depth;
77    state.depth_right = depth;
78}
79
80#[allow(dead_code)]
81pub fn nlf_reset(state: &mut NasolabialFoldState) {
82    *state = NasolabialFoldState::default();
83}
84
85#[allow(dead_code)]
86pub fn nlf_is_neutral(state: &NasolabialFoldState) -> bool {
87    state.depth_left < 1e-4 && state.depth_right < 1e-4
88}
89
90/// Asymmetry between left and right fold depth.
91#[allow(dead_code)]
92pub fn nlf_asymmetry(state: &NasolabialFoldState) -> f32 {
93    (state.depth_left - state.depth_right).abs()
94}
95
96/// Depth in normalised units for a side.
97#[allow(dead_code)]
98pub fn nlf_depth(state: &NasolabialFoldState, side: NlSide, cfg: &NasolabialFoldConfig) -> f32 {
99    let v = match side {
100        NlSide::Left => state.depth_left,
101        NlSide::Right => state.depth_right,
102    };
103    v * cfg.max_depth
104}
105
106/// Returns morph weights \[depth_l, depth_r, length_l, length_r\].
107#[allow(dead_code)]
108pub fn nlf_to_weights(state: &NasolabialFoldState) -> [f32; 4] {
109    [
110        state.depth_left,
111        state.depth_right,
112        state.length_left,
113        state.length_right,
114    ]
115}
116
117#[allow(dead_code)]
118pub fn nlf_blend(a: &NasolabialFoldState, b: &NasolabialFoldState, t: f32) -> NasolabialFoldState {
119    let t = t.clamp(0.0, 1.0);
120    let inv = 1.0 - t;
121    NasolabialFoldState {
122        depth_left: a.depth_left * inv + b.depth_left * t,
123        depth_right: a.depth_right * inv + b.depth_right * t,
124        length_left: a.length_left * inv + b.length_left * t,
125        length_right: a.length_right * inv + b.length_right * t,
126    }
127}
128
129#[allow(dead_code)]
130pub fn nlf_to_json(state: &NasolabialFoldState) -> String {
131    format!(
132        "{{\"depth_l\":{:.4},\"depth_r\":{:.4}}}",
133        state.depth_left, state.depth_right
134    )
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn default_neutral() {
143        assert!(nlf_is_neutral(&new_nasolabial_fold_state()));
144    }
145
146    #[test]
147    fn depth_clamps_high() {
148        let mut s = new_nasolabial_fold_state();
149        nlf_set_depth(&mut s, NlSide::Left, 5.0);
150        assert!((s.depth_left - 1.0).abs() < 1e-6);
151    }
152
153    #[test]
154    fn depth_clamps_low() {
155        let mut s = new_nasolabial_fold_state();
156        nlf_set_depth(&mut s, NlSide::Right, -1.0);
157        assert!(s.depth_right < 1e-6);
158    }
159
160    #[test]
161    fn set_both_symmetric() {
162        let mut s = new_nasolabial_fold_state();
163        nlf_set_both(&mut s, 0.6);
164        assert!((s.depth_left - s.depth_right).abs() < 1e-6);
165    }
166
167    #[test]
168    fn reset_clears() {
169        let mut s = new_nasolabial_fold_state();
170        nlf_set_both(&mut s, 1.0);
171        nlf_reset(&mut s);
172        assert!(nlf_is_neutral(&s));
173    }
174
175    #[test]
176    fn asymmetry_nonzero_when_different() {
177        let mut s = new_nasolabial_fold_state();
178        nlf_set_depth(&mut s, NlSide::Left, 0.8);
179        nlf_set_depth(&mut s, NlSide::Right, 0.2);
180        assert!((nlf_asymmetry(&s) - 0.6).abs() < 1e-5);
181    }
182
183    #[test]
184    fn depth_scaled_by_config() {
185        let cfg = default_nasolabial_fold_config();
186        let mut s = new_nasolabial_fold_state();
187        nlf_set_depth(&mut s, NlSide::Left, 1.0);
188        assert!((nlf_depth(&s, NlSide::Left, &cfg) - 1.0).abs() < 1e-5);
189    }
190
191    #[test]
192    fn weights_four_elements() {
193        let w = nlf_to_weights(&new_nasolabial_fold_state());
194        assert_eq!(w.len(), 4);
195    }
196
197    #[test]
198    fn blend_midpoint() {
199        let mut b = new_nasolabial_fold_state();
200        nlf_set_both(&mut b, 1.0);
201        let r = nlf_blend(&new_nasolabial_fold_state(), &b, 0.5);
202        assert!((r.depth_left - 0.5).abs() < 1e-5);
203    }
204
205    #[test]
206    fn json_has_keys() {
207        let j = nlf_to_json(&new_nasolabial_fold_state());
208        assert!(j.contains("depth_l") && j.contains("depth_r"));
209    }
210}