Skip to main content

oxihuman_morph/
lip_retract_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Lip retraction control — pulling the lips back toward the teeth.
6
7use std::f32::consts::FRAC_PI_6;
8
9/// Which lip.
10#[allow(dead_code)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LipSide {
13    Upper,
14    Lower,
15}
16
17/// Configuration.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct LipRetractConfig {
21    /// Maximum retraction angle in radians (informational).
22    pub max_angle_rad: f32,
23}
24
25impl Default for LipRetractConfig {
26    fn default() -> Self {
27        LipRetractConfig {
28            max_angle_rad: FRAC_PI_6,
29        }
30    }
31}
32
33/// Runtime state.
34#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct LipRetractState {
37    upper: f32,
38    lower: f32,
39    /// Corner pull in `[0.0, 1.0]`.
40    corners: f32,
41    config: LipRetractConfig,
42}
43
44/// Default config.
45pub fn default_lip_retract_config() -> LipRetractConfig {
46    LipRetractConfig::default()
47}
48
49/// New neutral state.
50pub fn new_lip_retract_state(config: LipRetractConfig) -> LipRetractState {
51    LipRetractState {
52        upper: 0.0,
53        lower: 0.0,
54        corners: 0.0,
55        config,
56    }
57}
58
59/// Set retraction for a lip.
60pub fn lrc_set_retract(state: &mut LipRetractState, side: LipSide, v: f32) {
61    let v = v.clamp(0.0, 1.0);
62    match side {
63        LipSide::Upper => state.upper = v,
64        LipSide::Lower => state.lower = v,
65    }
66}
67
68/// Set both lips.
69pub fn lrc_set_both(state: &mut LipRetractState, v: f32) {
70    let v = v.clamp(0.0, 1.0);
71    state.upper = v;
72    state.lower = v;
73}
74
75/// Set corner pull.
76pub fn lrc_set_corners(state: &mut LipRetractState, v: f32) {
77    state.corners = v.clamp(0.0, 1.0);
78}
79
80/// Reset.
81pub fn lrc_reset(state: &mut LipRetractState) {
82    state.upper = 0.0;
83    state.lower = 0.0;
84    state.corners = 0.0;
85}
86
87/// True when neutral.
88pub fn lrc_is_neutral(state: &LipRetractState) -> bool {
89    state.upper < 1e-5 && state.lower < 1e-5 && state.corners < 1e-5
90}
91
92/// Average retraction.
93pub fn lrc_average(state: &LipRetractState) -> f32 {
94    (state.upper + state.lower) * 0.5
95}
96
97/// Retraction angle in radians.
98pub fn lrc_angle_rad(state: &LipRetractState) -> f32 {
99    lrc_average(state) * state.config.max_angle_rad
100}
101
102/// Morph weights: `[upper, lower, corners]`.
103pub fn lrc_to_weights(state: &LipRetractState) -> [f32; 3] {
104    [state.upper, state.lower, state.corners]
105}
106
107/// Blend.
108pub fn lrc_blend(a: &LipRetractState, b: &LipRetractState, t: f32) -> LipRetractState {
109    let t = t.clamp(0.0, 1.0);
110    LipRetractState {
111        upper: a.upper + (b.upper - a.upper) * t,
112        lower: a.lower + (b.lower - a.lower) * t,
113        corners: a.corners + (b.corners - a.corners) * t,
114        config: a.config.clone(),
115    }
116}
117
118/// Serialise.
119pub fn lrc_to_json(state: &LipRetractState) -> String {
120    format!(
121        r#"{{"upper":{:.4},"lower":{:.4},"corners":{:.4}}}"#,
122        state.upper, state.lower, state.corners
123    )
124}
125
126// ---------------------------------------------------------------------------
127// Tests
128// ---------------------------------------------------------------------------
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn make() -> LipRetractState {
134        new_lip_retract_state(default_lip_retract_config())
135    }
136
137    #[test]
138    fn neutral_on_creation() {
139        assert!(lrc_is_neutral(&make()));
140    }
141
142    #[test]
143    fn set_upper() {
144        let mut s = make();
145        lrc_set_retract(&mut s, LipSide::Upper, 0.5);
146        assert!((s.upper - 0.5).abs() < 1e-5);
147    }
148
149    #[test]
150    fn set_both_syncs() {
151        let mut s = make();
152        lrc_set_both(&mut s, 0.7);
153        assert!((s.upper - s.lower).abs() < 1e-5);
154    }
155
156    #[test]
157    fn reset_clears() {
158        let mut s = make();
159        lrc_set_both(&mut s, 1.0);
160        lrc_reset(&mut s);
161        assert!(lrc_is_neutral(&s));
162    }
163
164    #[test]
165    fn angle_positive_when_retracted() {
166        let mut s = make();
167        lrc_set_both(&mut s, 0.5);
168        assert!(lrc_angle_rad(&s) > 0.0);
169    }
170
171    #[test]
172    fn weights_in_range() {
173        let mut s = make();
174        lrc_set_both(&mut s, 0.6);
175        for v in lrc_to_weights(&s) {
176            assert!((0.0..=1.0).contains(&v));
177        }
178    }
179
180    #[test]
181    fn blend_midpoint() {
182        let mut b = make();
183        lrc_set_both(&mut b, 1.0);
184        let m = lrc_blend(&make(), &b, 0.5);
185        assert!((m.upper - 0.5).abs() < 1e-5);
186    }
187
188    #[test]
189    fn json_has_upper() {
190        assert!(lrc_to_json(&make()).contains("upper"));
191    }
192
193    #[test]
194    fn corners_clamped_high() {
195        let mut s = make();
196        lrc_set_corners(&mut s, 10.0);
197        assert!((s.corners - 1.0).abs() < 1e-5);
198    }
199}