Skip to main content

oxihuman_morph/
philtrum_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Philtrum (groove between nose and upper lip) morph control.
5
6#![allow(dead_code)]
7
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct PhiltrumConfig {
11    pub max_depth: f32,
12    pub max_width: f32,
13}
14
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct PhiltrumState {
18    pub depth: f32,
19    pub width: f32,
20    pub length: f32,
21}
22
23#[allow(dead_code)]
24pub fn default_philtrum_config() -> PhiltrumConfig {
25    PhiltrumConfig {
26        max_depth: 1.0,
27        max_width: 1.0,
28    }
29}
30
31#[allow(dead_code)]
32pub fn new_philtrum_state() -> PhiltrumState {
33    PhiltrumState {
34        depth: 0.0,
35        width: 0.5,
36        length: 0.5,
37    }
38}
39
40#[allow(dead_code)]
41pub fn philtrum_set_depth(state: &mut PhiltrumState, cfg: &PhiltrumConfig, value: f32) {
42    state.depth = value.clamp(0.0, cfg.max_depth);
43}
44
45#[allow(dead_code)]
46pub fn philtrum_set_width(state: &mut PhiltrumState, cfg: &PhiltrumConfig, value: f32) {
47    state.width = value.clamp(0.0, cfg.max_width);
48}
49
50#[allow(dead_code)]
51pub fn philtrum_set_length(state: &mut PhiltrumState, value: f32) {
52    state.length = value.clamp(0.0, 1.0);
53}
54
55#[allow(dead_code)]
56pub fn philtrum_reset(state: &mut PhiltrumState) {
57    *state = new_philtrum_state();
58}
59
60#[allow(dead_code)]
61pub fn philtrum_to_weights(state: &PhiltrumState) -> Vec<(String, f32)> {
62    vec![
63        ("philtrum_depth".to_string(), state.depth),
64        ("philtrum_width".to_string(), state.width),
65        ("philtrum_length".to_string(), state.length),
66    ]
67}
68
69#[allow(dead_code)]
70pub fn philtrum_to_json(state: &PhiltrumState) -> String {
71    format!(
72        r#"{{"depth":{:.4},"width":{:.4},"length":{:.4}}}"#,
73        state.depth, state.width, state.length
74    )
75}
76
77#[allow(dead_code)]
78pub fn philtrum_clamp(state: &mut PhiltrumState, cfg: &PhiltrumConfig) {
79    state.depth = state.depth.clamp(0.0, cfg.max_depth);
80    state.width = state.width.clamp(0.0, cfg.max_width);
81    state.length = state.length.clamp(0.0, 1.0);
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_default_config() {
90        let cfg = default_philtrum_config();
91        assert_eq!(cfg.max_depth, 1.0);
92        assert_eq!(cfg.max_width, 1.0);
93    }
94
95    #[test]
96    fn test_new_state_defaults() {
97        let s = new_philtrum_state();
98        assert_eq!(s.depth, 0.0);
99        assert!((s.width - 0.5).abs() < 1e-6);
100        assert!((s.length - 0.5).abs() < 1e-6);
101    }
102
103    #[test]
104    fn test_set_depth_clamps() {
105        let cfg = default_philtrum_config();
106        let mut s = new_philtrum_state();
107        philtrum_set_depth(&mut s, &cfg, 2.0);
108        assert_eq!(s.depth, 1.0);
109        philtrum_set_depth(&mut s, &cfg, -0.5);
110        assert_eq!(s.depth, 0.0);
111    }
112
113    #[test]
114    fn test_set_width_clamps() {
115        let cfg = default_philtrum_config();
116        let mut s = new_philtrum_state();
117        philtrum_set_width(&mut s, &cfg, 0.6);
118        assert!((s.width - 0.6).abs() < 1e-6);
119    }
120
121    #[test]
122    fn test_set_length_clamps() {
123        let mut s = new_philtrum_state();
124        philtrum_set_length(&mut s, 5.0);
125        assert_eq!(s.length, 1.0);
126        philtrum_set_length(&mut s, -1.0);
127        assert_eq!(s.length, 0.0);
128    }
129
130    #[test]
131    fn test_reset() {
132        let cfg = default_philtrum_config();
133        let mut s = new_philtrum_state();
134        philtrum_set_depth(&mut s, &cfg, 0.9);
135        philtrum_reset(&mut s);
136        assert_eq!(s.depth, 0.0);
137    }
138
139    #[test]
140    fn test_to_weights_count() {
141        let s = new_philtrum_state();
142        let w = philtrum_to_weights(&s);
143        assert_eq!(w.len(), 3);
144    }
145
146    #[test]
147    fn test_to_json_contains_keys() {
148        let s = new_philtrum_state();
149        let j = philtrum_to_json(&s);
150        assert!(j.contains("depth"));
151        assert!(j.contains("length"));
152    }
153
154    #[test]
155    fn test_clamp_enforces_bounds() {
156        let cfg = default_philtrum_config();
157        let mut s = PhiltrumState {
158            depth: 3.0,
159            width: -1.0,
160            length: 2.0,
161        };
162        philtrum_clamp(&mut s, &cfg);
163        assert_eq!(s.depth, 1.0);
164        assert_eq!(s.width, 0.0);
165        assert_eq!(s.length, 1.0);
166    }
167}
168
169// ── PhiltrumControl (simple blend API) ────────────────────────────────────────
170
171/// Simple philtrum morph parameters (blend API).
172#[allow(dead_code)]
173#[derive(Debug, Clone, PartialEq)]
174pub struct PhiltrumControl {
175    /// Groove depth, normalised 0..1.
176    pub depth: f32,
177    /// Groove width, normalised 0..1.
178    pub width: f32,
179    /// Groove length, normalised 0..1.
180    pub length: f32,
181}
182
183/// Return a default philtrum control.
184#[allow(dead_code)]
185pub fn default_philtrum_control() -> PhiltrumControl {
186    PhiltrumControl {
187        depth: 0.5,
188        width: 0.5,
189        length: 0.5,
190    }
191}
192
193/// Apply philtrum control to a morph-weight slice.
194#[allow(dead_code)]
195pub fn apply_philtrum_control(weights: &mut [f32], pc: &PhiltrumControl) {
196    if !weights.is_empty() {
197        weights[0] = pc.depth;
198    }
199    if weights.len() > 1 {
200        weights[1] = pc.width;
201    }
202    if weights.len() > 2 {
203        weights[2] = pc.length;
204    }
205}
206
207/// Linear blend between two philtrum controls.
208#[allow(dead_code)]
209pub fn philtrum_blend(a: &PhiltrumControl, b: &PhiltrumControl, t: f32) -> PhiltrumControl {
210    let t = t.clamp(0.0, 1.0);
211    PhiltrumControl {
212        depth: a.depth + (b.depth - a.depth) * t,
213        width: a.width + (b.width - a.width) * t,
214        length: a.length + (b.length - a.length) * t,
215    }
216}