Skip to main content

oxihuman_morph/
cheek_tighten_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Cheek tightening control — pulls the cheek skin inward / upward.
6
7/// Configuration for cheek tightening.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct CheekTightenConfig {
11    /// Scale factor applied to the tighten output.
12    pub scale: f32,
13}
14
15impl Default for CheekTightenConfig {
16    fn default() -> Self {
17        CheekTightenConfig { scale: 1.0 }
18    }
19}
20
21/// Runtime state for cheek tightening.
22#[allow(dead_code)]
23#[derive(Debug, Clone)]
24pub struct CheekTightenState {
25    left: f32,
26    right: f32,
27    /// Vertical bias in `[-1.0, 1.0]` (positive = pull upward).
28    vertical_bias: f32,
29    config: CheekTightenConfig,
30}
31
32/// Return a default config.
33pub fn default_cheek_tighten_config() -> CheekTightenConfig {
34    CheekTightenConfig::default()
35}
36
37/// Create a new neutral state.
38pub fn new_cheek_tighten_state(config: CheekTightenConfig) -> CheekTightenState {
39    CheekTightenState {
40        left: 0.0,
41        right: 0.0,
42        vertical_bias: 0.0,
43        config,
44    }
45}
46
47/// Set the left cheek tighten amount.
48pub fn ct_set_left(state: &mut CheekTightenState, v: f32) {
49    state.left = v.clamp(0.0, 1.0);
50}
51
52/// Set the right cheek tighten amount.
53pub fn ct_set_right(state: &mut CheekTightenState, v: f32) {
54    state.right = v.clamp(0.0, 1.0);
55}
56
57/// Set both sides to the same value.
58pub fn ct_set_both(state: &mut CheekTightenState, v: f32) {
59    let v = v.clamp(0.0, 1.0);
60    state.left = v;
61    state.right = v;
62}
63
64/// Set the vertical bias in `[-1.0, 1.0]`.
65pub fn ct_set_vertical_bias(state: &mut CheekTightenState, v: f32) {
66    state.vertical_bias = v.clamp(-1.0, 1.0);
67}
68
69/// Reset all parameters to zero.
70pub fn ct_reset(state: &mut CheekTightenState) {
71    state.left = 0.0;
72    state.right = 0.0;
73    state.vertical_bias = 0.0;
74}
75
76/// Return true when all parameters are effectively zero.
77pub fn ct_is_neutral(state: &CheekTightenState) -> bool {
78    state.left < 1e-5 && state.right < 1e-5 && state.vertical_bias.abs() < 1e-5
79}
80
81/// Asymmetry between left and right.
82pub fn ct_asymmetry(state: &CheekTightenState) -> f32 {
83    (state.left - state.right).abs()
84}
85
86/// Average tighten value across both sides.
87pub fn ct_average(state: &CheekTightenState) -> f32 {
88    (state.left + state.right) * 0.5
89}
90
91/// Evaluate morph weights: `[left, right, vertical_component]`.
92pub fn ct_to_weights(state: &CheekTightenState) -> [f32; 3] {
93    let s = state.config.scale;
94    [
95        (state.left * s).clamp(0.0, 1.0),
96        (state.right * s).clamp(0.0, 1.0),
97        ((state.vertical_bias * 0.5 + 0.5) * s).clamp(0.0, 1.0),
98    ]
99}
100
101/// Blend between two states.
102pub fn ct_blend(a: &CheekTightenState, b: &CheekTightenState, t: f32) -> CheekTightenState {
103    let t = t.clamp(0.0, 1.0);
104    CheekTightenState {
105        left: a.left + (b.left - a.left) * t,
106        right: a.right + (b.right - a.right) * t,
107        vertical_bias: a.vertical_bias + (b.vertical_bias - a.vertical_bias) * t,
108        config: a.config.clone(),
109    }
110}
111
112/// Serialise to JSON-like string.
113pub fn ct_to_json(state: &CheekTightenState) -> String {
114    format!(
115        r#"{{"left":{:.4},"right":{:.4},"vertical_bias":{:.4}}}"#,
116        state.left, state.right, state.vertical_bias
117    )
118}
119
120// ---------------------------------------------------------------------------
121// Tests
122// ---------------------------------------------------------------------------
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    fn make() -> CheekTightenState {
128        new_cheek_tighten_state(default_cheek_tighten_config())
129    }
130
131    #[test]
132    fn neutral_on_creation() {
133        assert!(ct_is_neutral(&make()));
134    }
135
136    #[test]
137    fn set_left_clamps() {
138        let mut s = make();
139        ct_set_left(&mut s, 5.0);
140        assert!((s.left - 1.0).abs() < 1e-5);
141    }
142
143    #[test]
144    fn set_both_equal() {
145        let mut s = make();
146        ct_set_both(&mut s, 0.6);
147        assert!((s.left - s.right).abs() < 1e-5);
148    }
149
150    #[test]
151    fn reset_clears_all() {
152        let mut s = make();
153        ct_set_both(&mut s, 0.9);
154        ct_reset(&mut s);
155        assert!(ct_is_neutral(&s));
156    }
157
158    #[test]
159    fn asymmetry_zero_when_equal() {
160        let mut s = make();
161        ct_set_both(&mut s, 0.4);
162        assert!(ct_asymmetry(&s) < 1e-5);
163    }
164
165    #[test]
166    fn weights_in_unit_range() {
167        let mut s = make();
168        ct_set_both(&mut s, 0.7);
169        for v in ct_to_weights(&s) {
170            assert!((0.0..=1.0).contains(&v));
171        }
172    }
173
174    #[test]
175    fn blend_at_one_is_b() {
176        let mut b = make();
177        ct_set_both(&mut b, 0.8);
178        let r = ct_blend(&make(), &b, 1.0);
179        assert!((r.left - 0.8).abs() < 1e-5);
180    }
181
182    #[test]
183    fn json_contains_left_key() {
184        assert!(ct_to_json(&make()).contains("left"));
185    }
186
187    #[test]
188    fn vertical_bias_clamped() {
189        let mut s = make();
190        ct_set_vertical_bias(&mut s, 10.0);
191        assert!((s.vertical_bias - 1.0).abs() < 1e-5);
192    }
193
194    #[test]
195    fn average_is_mean_of_sides() {
196        let mut s = make();
197        ct_set_left(&mut s, 0.2);
198        ct_set_right(&mut s, 0.8);
199        assert!((ct_average(&s) - 0.5).abs() < 1e-5);
200    }
201}