Skip to main content

oxihuman_morph/
chin_flat_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Chin flatness control — flattening of the mental protuberance inferior surface.
5
6use std::f32::consts::FRAC_PI_6;
7
8/// Config.
9#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct ChinFlatConfig {
12    pub max_flatten_m: f32,
13}
14
15impl Default for ChinFlatConfig {
16    fn default() -> Self {
17        Self {
18            max_flatten_m: 0.008,
19        }
20    }
21}
22
23/// State.
24#[allow(dead_code)]
25#[derive(Debug, Clone, Default)]
26pub struct ChinFlatState {
27    /// Flatten amount, 0..=1 (0 = round, 1 = flat).
28    pub flatten: f32,
29    /// Vertical bias of flattening, −1..=1.
30    pub v_bias: f32,
31}
32
33#[allow(dead_code)]
34pub fn new_chin_flat_state() -> ChinFlatState {
35    ChinFlatState::default()
36}
37
38#[allow(dead_code)]
39pub fn default_chin_flat_config() -> ChinFlatConfig {
40    ChinFlatConfig::default()
41}
42
43#[allow(dead_code)]
44pub fn cf_set_flatten(state: &mut ChinFlatState, v: f32) {
45    state.flatten = v.clamp(0.0, 1.0);
46}
47
48#[allow(dead_code)]
49pub fn cf_set_v_bias(state: &mut ChinFlatState, v: f32) {
50    state.v_bias = v.clamp(-1.0, 1.0);
51}
52
53#[allow(dead_code)]
54pub fn cf_reset(state: &mut ChinFlatState) {
55    *state = ChinFlatState::default();
56}
57
58#[allow(dead_code)]
59pub fn cf_is_neutral(state: &ChinFlatState) -> bool {
60    state.flatten < 1e-4 && state.v_bias.abs() < 1e-4
61}
62
63/// Inferior surface angle change in radians.
64#[allow(dead_code)]
65pub fn cf_angle_rad(state: &ChinFlatState) -> f32 {
66    state.flatten * FRAC_PI_6
67}
68
69/// Morph weight for the chin-flatten target.
70#[allow(dead_code)]
71pub fn cf_to_weights(state: &ChinFlatState, cfg: &ChinFlatConfig) -> f32 {
72    state.flatten * cfg.max_flatten_m
73}
74
75/// Blend two states.
76#[allow(dead_code)]
77pub fn cf_blend(a: &ChinFlatState, b: &ChinFlatState, t: f32) -> ChinFlatState {
78    let t = t.clamp(0.0, 1.0);
79    let inv = 1.0 - t;
80    ChinFlatState {
81        flatten: a.flatten * inv + b.flatten * t,
82        v_bias: a.v_bias * inv + b.v_bias * t,
83    }
84}
85
86/// JSON.
87#[allow(dead_code)]
88pub fn cf_to_json(state: &ChinFlatState) -> String {
89    format!(
90        "{{\"flatten\":{:.4},\"v_bias\":{:.4}}}",
91        state.flatten, state.v_bias
92    )
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn default_is_neutral() {
101        assert!(cf_is_neutral(&new_chin_flat_state()));
102    }
103
104    #[test]
105    fn flatten_clamps_high() {
106        let mut s = new_chin_flat_state();
107        cf_set_flatten(&mut s, 2.0);
108        assert!((s.flatten - 1.0).abs() < 1e-6);
109    }
110
111    #[test]
112    fn flatten_clamps_low() {
113        let mut s = new_chin_flat_state();
114        cf_set_flatten(&mut s, -1.0);
115        assert!(s.flatten < 1e-6);
116    }
117
118    #[test]
119    fn v_bias_clamps() {
120        let mut s = new_chin_flat_state();
121        cf_set_v_bias(&mut s, 5.0);
122        assert!((s.v_bias - 1.0).abs() < 1e-6);
123    }
124
125    #[test]
126    fn reset_clears() {
127        let mut s = new_chin_flat_state();
128        cf_set_flatten(&mut s, 0.9);
129        cf_reset(&mut s);
130        assert!(cf_is_neutral(&s));
131    }
132
133    #[test]
134    fn angle_zero_at_neutral() {
135        let s = new_chin_flat_state();
136        assert!(cf_angle_rad(&s).abs() < 1e-6);
137    }
138
139    #[test]
140    fn angle_positive_when_flat() {
141        let mut s = new_chin_flat_state();
142        cf_set_flatten(&mut s, 1.0);
143        assert!(cf_angle_rad(&s) > 0.0);
144    }
145
146    #[test]
147    fn weight_scales_with_config() {
148        let cfg = default_chin_flat_config();
149        let mut s = new_chin_flat_state();
150        cf_set_flatten(&mut s, 1.0);
151        assert!((cf_to_weights(&s, &cfg) - cfg.max_flatten_m).abs() < 1e-6);
152    }
153
154    #[test]
155    fn blend_midpoint() {
156        let mut a = new_chin_flat_state();
157        let b = ChinFlatState {
158            flatten: 1.0,
159            v_bias: 0.0,
160        };
161        cf_set_flatten(&mut a, 0.0);
162        let r = cf_blend(&a, &b, 0.5);
163        assert!((r.flatten - 0.5).abs() < 1e-5);
164    }
165
166    #[test]
167    fn json_contains_flatten() {
168        let j = cf_to_json(&new_chin_flat_state());
169        assert!(j.contains("flatten"));
170    }
171}