Skip to main content

oxihuman_morph/
chin_dimple_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Chin-dimple (cleft chin) depth and width control.
6
7use std::f32::consts::FRAC_PI_4;
8
9/// Chin dimple state.
10#[allow(dead_code)]
11#[derive(Clone, Debug)]
12pub struct ChinDimpleState {
13    /// Dimple depth (0 = no dimple, 1 = maximum).
14    pub depth: f32,
15    /// Dimple width factor (1.0 = standard, 0 = point, 2 = wide).
16    pub width: f32,
17    /// Vertical offset from chin tip centre (normalised, -1..1).
18    pub vertical_offset: f32,
19}
20
21/// Config.
22#[allow(dead_code)]
23#[derive(Clone, Debug)]
24pub struct ChinDimpleConfig {
25    pub max_depth: f32,
26    pub max_width: f32,
27}
28
29impl Default for ChinDimpleConfig {
30    fn default() -> Self {
31        Self {
32            max_depth: 1.0,
33            max_width: 2.0,
34        }
35    }
36}
37
38impl Default for ChinDimpleState {
39    fn default() -> Self {
40        Self {
41            depth: 0.0,
42            width: 1.0,
43            vertical_offset: 0.0,
44        }
45    }
46}
47
48#[allow(dead_code)]
49pub fn new_chin_dimple_state() -> ChinDimpleState {
50    ChinDimpleState::default()
51}
52
53#[allow(dead_code)]
54pub fn default_chin_dimple_config() -> ChinDimpleConfig {
55    ChinDimpleConfig::default()
56}
57
58#[allow(dead_code)]
59pub fn cd_set_depth(state: &mut ChinDimpleState, cfg: &ChinDimpleConfig, v: f32) {
60    state.depth = v.clamp(0.0, cfg.max_depth);
61}
62
63#[allow(dead_code)]
64pub fn cd_set_width(state: &mut ChinDimpleState, cfg: &ChinDimpleConfig, v: f32) {
65    state.width = v.clamp(0.1, cfg.max_width);
66}
67
68#[allow(dead_code)]
69pub fn cd_set_vertical_offset(state: &mut ChinDimpleState, v: f32) {
70    state.vertical_offset = v.clamp(-1.0, 1.0);
71}
72
73#[allow(dead_code)]
74pub fn cd_reset(state: &mut ChinDimpleState) {
75    *state = ChinDimpleState::default();
76}
77
78#[allow(dead_code)]
79pub fn cd_is_neutral(state: &ChinDimpleState) -> bool {
80    state.depth < 1e-4
81}
82
83#[allow(dead_code)]
84pub fn cd_blend(a: &ChinDimpleState, b: &ChinDimpleState, t: f32) -> ChinDimpleState {
85    let t = t.clamp(0.0, 1.0);
86    ChinDimpleState {
87        depth: a.depth + (b.depth - a.depth) * t,
88        width: a.width + (b.width - a.width) * t,
89        vertical_offset: a.vertical_offset + (b.vertical_offset - a.vertical_offset) * t,
90    }
91}
92
93/// Compute approximate dimple area in normalised units.
94#[allow(dead_code)]
95pub fn cd_area(state: &ChinDimpleState) -> f32 {
96    // Ellipse-like: depth * width * pi/4
97    state.depth * state.width * FRAC_PI_4
98}
99
100#[allow(dead_code)]
101pub fn cd_to_weights(state: &ChinDimpleState) -> [f32; 3] {
102    [state.depth, state.width - 1.0, state.vertical_offset]
103}
104
105#[allow(dead_code)]
106pub fn cd_to_json(state: &ChinDimpleState) -> String {
107    format!(
108        "{{\"depth\":{:.4},\"width\":{:.4},\"v_offset\":{:.4}}}",
109        state.depth, state.width, state.vertical_offset
110    )
111}
112
113#[allow(dead_code)]
114pub fn cd_effective_depth(state: &ChinDimpleState) -> f32 {
115    state.depth * state.width.sqrt()
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn default_neutral() {
124        assert!(cd_is_neutral(&new_chin_dimple_state()));
125    }
126
127    #[test]
128    fn depth_clamp_max() {
129        let mut s = new_chin_dimple_state();
130        let cfg = default_chin_dimple_config();
131        cd_set_depth(&mut s, &cfg, 5.0);
132        assert!(s.depth <= cfg.max_depth);
133    }
134
135    #[test]
136    fn depth_not_negative() {
137        let mut s = new_chin_dimple_state();
138        let cfg = default_chin_dimple_config();
139        cd_set_depth(&mut s, &cfg, -1.0);
140        assert!(s.depth >= 0.0);
141    }
142
143    #[test]
144    fn width_min_clamp() {
145        let mut s = new_chin_dimple_state();
146        let cfg = default_chin_dimple_config();
147        cd_set_width(&mut s, &cfg, 0.0);
148        assert!(s.width >= 0.1);
149    }
150
151    #[test]
152    fn reset_neutral() {
153        let mut s = new_chin_dimple_state();
154        let cfg = default_chin_dimple_config();
155        cd_set_depth(&mut s, &cfg, 0.8);
156        cd_reset(&mut s);
157        assert!(cd_is_neutral(&s));
158    }
159
160    #[test]
161    fn blend_half() {
162        let cfg = default_chin_dimple_config();
163        let mut a = new_chin_dimple_state();
164        let mut b = new_chin_dimple_state();
165        cd_set_depth(&mut a, &cfg, 0.0);
166        cd_set_depth(&mut b, &cfg, 1.0);
167        let m = cd_blend(&a, &b, 0.5);
168        assert!((m.depth - 0.5).abs() < 1e-4);
169    }
170
171    #[test]
172    fn area_zero_when_no_depth() {
173        let s = new_chin_dimple_state();
174        assert!(cd_area(&s).abs() < 1e-5);
175    }
176
177    #[test]
178    fn weights_len() {
179        assert_eq!(cd_to_weights(&new_chin_dimple_state()).len(), 3);
180    }
181
182    #[test]
183    fn json_contains_depth() {
184        assert!(cd_to_json(&new_chin_dimple_state()).contains("depth"));
185    }
186
187    #[test]
188    fn effective_depth_positive() {
189        let mut s = new_chin_dimple_state();
190        let cfg = default_chin_dimple_config();
191        cd_set_depth(&mut s, &cfg, 0.5);
192        assert!(cd_effective_depth(&s) > 0.0);
193    }
194}