Skip to main content

oxihuman_morph/
chin_recession_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Chin recession control — posterior chin displacement and retrogenia shaping.
6
7/// Configuration for chin recession.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct ChinRecessionConfig {
11    pub max_recession: f32,
12    pub max_protrusion: f32,
13}
14
15/// Runtime state.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct ChinRecessionState {
19    pub recession: f32,
20    pub vertical_shift: f32,
21    pub lateral_tilt: f32,
22}
23
24#[allow(dead_code)]
25pub fn default_chin_recession_config() -> ChinRecessionConfig {
26    ChinRecessionConfig {
27        max_recession: 1.0,
28        max_protrusion: 1.0,
29    }
30}
31
32#[allow(dead_code)]
33pub fn new_chin_recession_state() -> ChinRecessionState {
34    ChinRecessionState {
35        recession: 0.0,
36        vertical_shift: 0.0,
37        lateral_tilt: 0.0,
38    }
39}
40
41#[allow(dead_code)]
42pub fn chin_rec_set_recession(state: &mut ChinRecessionState, cfg: &ChinRecessionConfig, v: f32) {
43    state.recession = v.clamp(-cfg.max_protrusion, cfg.max_recession);
44}
45
46#[allow(dead_code)]
47pub fn chin_rec_set_vertical(state: &mut ChinRecessionState, v: f32) {
48    state.vertical_shift = v.clamp(-1.0, 1.0);
49}
50
51#[allow(dead_code)]
52pub fn chin_rec_set_tilt(state: &mut ChinRecessionState, v: f32) {
53    state.lateral_tilt = v.clamp(-1.0, 1.0);
54}
55
56#[allow(dead_code)]
57pub fn chin_rec_reset(state: &mut ChinRecessionState) {
58    *state = new_chin_recession_state();
59}
60
61#[allow(dead_code)]
62pub fn chin_rec_is_neutral(state: &ChinRecessionState) -> bool {
63    state.recession.abs() < 1e-6
64        && state.vertical_shift.abs() < 1e-6
65        && state.lateral_tilt.abs() < 1e-6
66}
67
68#[allow(dead_code)]
69pub fn chin_rec_net_offset(state: &ChinRecessionState) -> f32 {
70    state.recession
71}
72
73#[allow(dead_code)]
74pub fn chin_rec_blend(
75    a: &ChinRecessionState,
76    b: &ChinRecessionState,
77    t: f32,
78) -> ChinRecessionState {
79    let t = t.clamp(0.0, 1.0);
80    ChinRecessionState {
81        recession: a.recession + (b.recession - a.recession) * t,
82        vertical_shift: a.vertical_shift + (b.vertical_shift - a.vertical_shift) * t,
83        lateral_tilt: a.lateral_tilt + (b.lateral_tilt - a.lateral_tilt) * t,
84    }
85}
86
87#[allow(dead_code)]
88pub fn chin_rec_to_weights(state: &ChinRecessionState) -> Vec<(String, f32)> {
89    vec![
90        ("chin_recession".to_string(), state.recession),
91        ("chin_vertical_shift".to_string(), state.vertical_shift),
92        ("chin_lateral_tilt".to_string(), state.lateral_tilt),
93    ]
94}
95
96#[allow(dead_code)]
97pub fn chin_rec_to_json(state: &ChinRecessionState) -> String {
98    format!(
99        r#"{{"recession":{:.4},"vertical_shift":{:.4},"lateral_tilt":{:.4}}}"#,
100        state.recession, state.vertical_shift, state.lateral_tilt
101    )
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn default_config() {
110        let cfg = default_chin_recession_config();
111        assert!((cfg.max_recession - 1.0).abs() < 1e-6);
112    }
113
114    #[test]
115    fn new_state_neutral() {
116        let s = new_chin_recession_state();
117        assert!(chin_rec_is_neutral(&s));
118    }
119
120    #[test]
121    fn set_recession_clamps() {
122        let cfg = default_chin_recession_config();
123        let mut s = new_chin_recession_state();
124        chin_rec_set_recession(&mut s, &cfg, 5.0);
125        assert!((s.recession - 1.0).abs() < 1e-6);
126    }
127
128    #[test]
129    fn set_recession_negative() {
130        let cfg = default_chin_recession_config();
131        let mut s = new_chin_recession_state();
132        chin_rec_set_recession(&mut s, &cfg, -0.5);
133        assert!((s.recession + 0.5).abs() < 1e-6);
134    }
135
136    #[test]
137    fn set_vertical() {
138        let mut s = new_chin_recession_state();
139        chin_rec_set_vertical(&mut s, 0.3);
140        assert!((s.vertical_shift - 0.3).abs() < 1e-6);
141    }
142
143    #[test]
144    fn set_tilt_clamps() {
145        let mut s = new_chin_recession_state();
146        chin_rec_set_tilt(&mut s, 2.0);
147        assert!((s.lateral_tilt - 1.0).abs() < 1e-6);
148    }
149
150    #[test]
151    fn reset_clears() {
152        let cfg = default_chin_recession_config();
153        let mut s = new_chin_recession_state();
154        chin_rec_set_recession(&mut s, &cfg, 0.8);
155        chin_rec_reset(&mut s);
156        assert!(chin_rec_is_neutral(&s));
157    }
158
159    #[test]
160    fn blend_midpoint() {
161        let a = new_chin_recession_state();
162        let cfg = default_chin_recession_config();
163        let mut b = new_chin_recession_state();
164        chin_rec_set_recession(&mut b, &cfg, 1.0);
165        let m = chin_rec_blend(&a, &b, 0.5);
166        assert!((m.recession - 0.5).abs() < 1e-6);
167    }
168
169    #[test]
170    fn to_weights_count() {
171        let s = new_chin_recession_state();
172        assert_eq!(chin_rec_to_weights(&s).len(), 3);
173    }
174
175    #[test]
176    fn to_json_fields() {
177        let s = new_chin_recession_state();
178        let j = chin_rec_to_json(&s);
179        assert!(j.contains("recession"));
180    }
181}