Skip to main content

oxihuman_morph/
lip_cupid_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Lip Cupid's bow peak shape control.
6
7use std::f32::consts::FRAC_PI_4;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct LipCupidConfig {
12    pub max_peak: f32,
13    pub max_depth: f32,
14}
15
16impl Default for LipCupidConfig {
17    fn default() -> Self {
18        Self {
19            max_peak: 1.0,
20            max_depth: 1.0,
21        }
22    }
23}
24
25#[allow(dead_code)]
26#[derive(Debug, Clone)]
27pub struct LipCupidState {
28    pub peak: f32,
29    pub depth: f32,
30    pub config: LipCupidConfig,
31}
32
33#[allow(dead_code)]
34pub fn default_lip_cupid_config() -> LipCupidConfig {
35    LipCupidConfig::default()
36}
37
38#[allow(dead_code)]
39pub fn new_lip_cupid_state(config: LipCupidConfig) -> LipCupidState {
40    LipCupidState {
41        peak: 0.0,
42        depth: 0.0,
43        config,
44    }
45}
46
47#[allow(dead_code)]
48pub fn lc_set_peak(state: &mut LipCupidState, v: f32) {
49    state.peak = v.clamp(0.0, state.config.max_peak);
50}
51
52#[allow(dead_code)]
53pub fn lc_set_depth(state: &mut LipCupidState, v: f32) {
54    state.depth = v.clamp(0.0, state.config.max_depth);
55}
56
57#[allow(dead_code)]
58pub fn lc_reset(state: &mut LipCupidState) {
59    state.peak = 0.0;
60    state.depth = 0.0;
61}
62
63#[allow(dead_code)]
64pub fn lc_is_neutral(state: &LipCupidState) -> bool {
65    state.peak.abs() < 1e-6 && state.depth.abs() < 1e-6
66}
67
68#[allow(dead_code)]
69pub fn lc_bow_acuity(state: &LipCupidState) -> f32 {
70    if state.depth > 1e-9 {
71        state.peak / state.depth
72    } else {
73        0.0
74    }
75}
76
77#[allow(dead_code)]
78pub fn lc_peak_angle_rad(state: &LipCupidState) -> f32 {
79    state.peak * FRAC_PI_4
80}
81
82#[allow(dead_code)]
83pub fn lc_to_weights(state: &LipCupidState) -> [f32; 2] {
84    let np = if state.config.max_peak > 1e-9 {
85        state.peak / state.config.max_peak
86    } else {
87        0.0
88    };
89    let nd = if state.config.max_depth > 1e-9 {
90        state.depth / state.config.max_depth
91    } else {
92        0.0
93    };
94    [np, nd]
95}
96
97#[allow(dead_code)]
98pub fn lc_blend(a: &LipCupidState, b: &LipCupidState, t: f32) -> [f32; 2] {
99    let t = t.clamp(0.0, 1.0);
100    [
101        a.peak * (1.0 - t) + b.peak * t,
102        a.depth * (1.0 - t) + b.depth * t,
103    ]
104}
105
106#[allow(dead_code)]
107pub fn lc_to_json(state: &LipCupidState) -> String {
108    format!(
109        "{{\"peak\":{:.4},\"depth\":{:.4}}}",
110        state.peak, state.depth
111    )
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    #[test]
118    fn default_neutral() {
119        assert!(lc_is_neutral(&new_lip_cupid_state(
120            default_lip_cupid_config()
121        )));
122    }
123    #[test]
124    fn set_peak_clamps() {
125        let mut s = new_lip_cupid_state(default_lip_cupid_config());
126        lc_set_peak(&mut s, 5.0);
127        assert!((0.0..=1.0).contains(&s.peak));
128    }
129    #[test]
130    fn set_depth_clamps() {
131        let mut s = new_lip_cupid_state(default_lip_cupid_config());
132        lc_set_depth(&mut s, -0.5);
133        assert!(s.depth.abs() < 1e-6);
134    }
135    #[test]
136    fn reset_zeroes() {
137        let mut s = new_lip_cupid_state(default_lip_cupid_config());
138        lc_set_peak(&mut s, 0.5);
139        lc_reset(&mut s);
140        assert!(lc_is_neutral(&s));
141    }
142    #[test]
143    fn bow_acuity_zero_when_depth_zero() {
144        let s = new_lip_cupid_state(default_lip_cupid_config());
145        assert!(lc_bow_acuity(&s).abs() < 1e-6);
146    }
147    #[test]
148    fn peak_angle_nonneg() {
149        let s = new_lip_cupid_state(default_lip_cupid_config());
150        assert!(lc_peak_angle_rad(&s) >= 0.0);
151    }
152    #[test]
153    fn to_weights_max() {
154        let mut s = new_lip_cupid_state(default_lip_cupid_config());
155        lc_set_peak(&mut s, 1.0);
156        assert!((lc_to_weights(&s)[0] - 1.0).abs() < 1e-5);
157    }
158    #[test]
159    fn blend_mid() {
160        let mut a = new_lip_cupid_state(default_lip_cupid_config());
161        let b = new_lip_cupid_state(default_lip_cupid_config());
162        lc_set_peak(&mut a, 0.8);
163        let w = lc_blend(&a, &b, 0.5);
164        assert!((w[0] - 0.4).abs() < 1e-5);
165    }
166    #[test]
167    fn to_json_has_peak() {
168        assert!(lc_to_json(&new_lip_cupid_state(default_lip_cupid_config())).contains("\"peak\""));
169    }
170    #[test]
171    fn bow_acuity_ratio() {
172        let mut s = new_lip_cupid_state(default_lip_cupid_config());
173        lc_set_peak(&mut s, 0.6);
174        lc_set_depth(&mut s, 0.3);
175        assert!((lc_bow_acuity(&s) - 2.0).abs() < 1e-4);
176    }
177}