Skip to main content

oxihuman_morph/
lip_line_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Lip line (vermillion border) definition and curvature control.
5
6use std::f32::consts::FRAC_PI_8;
7
8/// Config.
9#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct LipLineConfig {
12    pub max_curvature_m: f32,
13}
14
15impl Default for LipLineConfig {
16    fn default() -> Self {
17        Self {
18            max_curvature_m: 0.006,
19        }
20    }
21}
22
23/// State.
24#[allow(dead_code)]
25#[derive(Debug, Clone, Default)]
26pub struct LipLineState {
27    /// Upper bow curvature, −1..=1 (positive = peaked, negative = flat).
28    pub upper_bow: f32,
29    /// Lower lip definition, 0..=1.
30    pub lower_def: f32,
31    /// Lip line width scale, −1..=1.
32    pub width_scale: f32,
33}
34
35#[allow(dead_code)]
36pub fn new_lip_line_state() -> LipLineState {
37    LipLineState::default()
38}
39
40#[allow(dead_code)]
41pub fn default_lip_line_config() -> LipLineConfig {
42    LipLineConfig::default()
43}
44
45#[allow(dead_code)]
46pub fn ll_set_upper_bow(state: &mut LipLineState, v: f32) {
47    state.upper_bow = v.clamp(-1.0, 1.0);
48}
49
50#[allow(dead_code)]
51pub fn ll_set_lower_def(state: &mut LipLineState, v: f32) {
52    state.lower_def = v.clamp(0.0, 1.0);
53}
54
55#[allow(dead_code)]
56pub fn ll_set_width_scale(state: &mut LipLineState, v: f32) {
57    state.width_scale = v.clamp(-1.0, 1.0);
58}
59
60#[allow(dead_code)]
61pub fn ll_reset(state: &mut LipLineState) {
62    *state = LipLineState::default();
63}
64
65#[allow(dead_code)]
66pub fn ll_is_neutral(state: &LipLineState) -> bool {
67    state.upper_bow.abs() < 1e-4 && state.lower_def < 1e-4 && state.width_scale.abs() < 1e-4
68}
69
70/// Bow angle in radians.
71#[allow(dead_code)]
72pub fn ll_bow_angle_rad(state: &LipLineState) -> f32 {
73    state.upper_bow * FRAC_PI_8
74}
75
76#[allow(dead_code)]
77pub fn ll_to_weights(state: &LipLineState, cfg: &LipLineConfig) -> [f32; 3] {
78    [
79        state.upper_bow * cfg.max_curvature_m,
80        state.lower_def * cfg.max_curvature_m,
81        state.width_scale * cfg.max_curvature_m * 0.5,
82    ]
83}
84
85#[allow(dead_code)]
86pub fn ll_blend(a: &LipLineState, b: &LipLineState, t: f32) -> LipLineState {
87    let t = t.clamp(0.0, 1.0);
88    let inv = 1.0 - t;
89    LipLineState {
90        upper_bow: a.upper_bow * inv + b.upper_bow * t,
91        lower_def: a.lower_def * inv + b.lower_def * t,
92        width_scale: a.width_scale * inv + b.width_scale * t,
93    }
94}
95
96#[allow(dead_code)]
97pub fn ll_to_json(state: &LipLineState) -> String {
98    format!(
99        "{{\"upper_bow\":{:.4},\"lower_def\":{:.4},\"width_scale\":{:.4}}}",
100        state.upper_bow, state.lower_def, state.width_scale
101    )
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_neutral() {
110        assert!(ll_is_neutral(&new_lip_line_state()));
111    }
112
113    #[test]
114    fn upper_bow_clamps() {
115        let mut s = new_lip_line_state();
116        ll_set_upper_bow(&mut s, 5.0);
117        assert!((s.upper_bow - 1.0).abs() < 1e-6);
118        ll_set_upper_bow(&mut s, -5.0);
119        assert!((s.upper_bow + 1.0).abs() < 1e-6);
120    }
121
122    #[test]
123    fn lower_def_clamps() {
124        let mut s = new_lip_line_state();
125        ll_set_lower_def(&mut s, -1.0);
126        assert!(s.lower_def < 1e-6);
127    }
128
129    #[test]
130    fn width_scale_clamps() {
131        let mut s = new_lip_line_state();
132        ll_set_width_scale(&mut s, 3.0);
133        assert!((s.width_scale - 1.0).abs() < 1e-6);
134    }
135
136    #[test]
137    fn reset_clears() {
138        let mut s = new_lip_line_state();
139        ll_set_upper_bow(&mut s, 0.7);
140        ll_reset(&mut s);
141        assert!(ll_is_neutral(&s));
142    }
143
144    #[test]
145    fn bow_angle_positive_peaked() {
146        let mut s = new_lip_line_state();
147        ll_set_upper_bow(&mut s, 1.0);
148        assert!(ll_bow_angle_rad(&s) > 0.0);
149    }
150
151    #[test]
152    fn weights_three_values() {
153        let w = ll_to_weights(&new_lip_line_state(), &default_lip_line_config());
154        assert_eq!(w.len(), 3);
155    }
156
157    #[test]
158    fn blend_midpoint() {
159        let b = LipLineState {
160            upper_bow: 1.0,
161            lower_def: 0.0,
162            width_scale: 0.0,
163        };
164        let r = ll_blend(&new_lip_line_state(), &b, 0.5);
165        assert!((r.upper_bow - 0.5).abs() < 1e-5);
166    }
167
168    #[test]
169    fn json_has_keys() {
170        let j = ll_to_json(&new_lip_line_state());
171        assert!(j.contains("upper_bow") && j.contains("lower_def"));
172    }
173
174    #[test]
175    fn bow_angle_negative_when_flat() {
176        let mut s = new_lip_line_state();
177        ll_set_upper_bow(&mut s, -1.0);
178        assert!(ll_bow_angle_rad(&s) < 0.0);
179    }
180}