Skip to main content

oxihuman_morph/
gum_line_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0 / #![allow(dead_code)]
2#![allow(dead_code)]
3
4//! Gum line morph controls for gingival display and tooth show.
5
6use std::f32::consts::PI;
7
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct GumLineControlConfig {
11    pub exposure: f32,
12    pub curvature: f32,
13    pub width: f32,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct GumLineControlState {
19    pub exposure: f32,
20    pub curvature: f32,
21    pub width: f32,
22    pub recession: f32,
23}
24
25#[allow(dead_code)]
26#[derive(Debug, Clone)]
27pub struct GumLineControlWeights {
28    pub exposed: f32,
29    pub curved: f32,
30    pub wide: f32,
31    pub receded: f32,
32    pub minimal: f32,
33}
34
35#[allow(dead_code)]
36pub fn default_gum_line_control_config() -> GumLineControlConfig {
37    GumLineControlConfig {
38        exposure: 0.5,
39        curvature: 0.5,
40        width: 0.5,
41    }
42}
43
44#[allow(dead_code)]
45pub fn new_gum_line_control_state() -> GumLineControlState {
46    GumLineControlState {
47        exposure: 0.5,
48        curvature: 0.5,
49        width: 0.5,
50        recession: 0.5,
51    }
52}
53
54#[allow(dead_code)]
55pub fn set_gum_line_control_exposure(state: &mut GumLineControlState, value: f32) {
56    state.exposure = value.clamp(0.0, 1.0);
57}
58
59#[allow(dead_code)]
60pub fn set_gum_line_control_curvature(state: &mut GumLineControlState, value: f32) {
61    state.curvature = value.clamp(0.0, 1.0);
62}
63
64#[allow(dead_code)]
65pub fn set_gum_line_control_width(state: &mut GumLineControlState, value: f32) {
66    state.width = value.clamp(0.0, 1.0);
67}
68
69#[allow(dead_code)]
70pub fn set_gum_line_control_recession(state: &mut GumLineControlState, value: f32) {
71    state.recession = value.clamp(0.0, 1.0);
72}
73
74#[allow(dead_code)]
75pub fn compute_gum_line_control_weights(
76    state: &GumLineControlState,
77    cfg: &GumLineControlConfig,
78) -> GumLineControlWeights {
79    let exposed = (state.exposure * cfg.exposure * (PI * 0.25).sin()).clamp(0.0, 1.0);
80    let curved = (state.curvature * cfg.curvature).clamp(0.0, 1.0);
81    let wide = (state.width * cfg.width).clamp(0.0, 1.0);
82    let receded = state.recession.clamp(0.0, 1.0);
83    let minimal = (1.0 - state.exposure).clamp(0.0, 1.0);
84    GumLineControlWeights {
85        exposed,
86        curved,
87        wide,
88        receded,
89        minimal,
90    }
91}
92
93#[allow(dead_code)]
94pub fn gum_line_control_to_json(state: &GumLineControlState) -> String {
95    format!(
96        r#"{{\"exposure\":{},\"curvature\":{},\"width\":{},\"recession\":{}}}"#,
97        state.exposure, state.curvature, state.width, state.recession
98    )
99}
100
101#[allow(dead_code)]
102pub fn blend_gum_line_controls(
103    a: &GumLineControlState,
104    b: &GumLineControlState,
105    t: f32,
106) -> GumLineControlState {
107    let t = t.clamp(0.0, 1.0);
108    GumLineControlState {
109        exposure: a.exposure + (b.exposure - a.exposure) * t,
110        curvature: a.curvature + (b.curvature - a.curvature) * t,
111        width: a.width + (b.width - a.width) * t,
112        recession: a.recession + (b.recession - a.recession) * t,
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_default_config() {
122        let cfg = default_gum_line_control_config();
123        assert!((0.0..=1.0).contains(&cfg.exposure));
124    }
125
126    #[test]
127    fn test_new_state() {
128        let s = new_gum_line_control_state();
129        assert!((s.exposure - 0.5).abs() < 1e-6);
130    }
131
132    #[test]
133    fn test_set_exposure_clamp() {
134        let mut s = new_gum_line_control_state();
135        set_gum_line_control_exposure(&mut s, 1.5);
136        assert!((s.exposure - 1.0).abs() < 1e-6);
137    }
138
139    #[test]
140    fn test_set_curvature() {
141        let mut s = new_gum_line_control_state();
142        set_gum_line_control_curvature(&mut s, 0.8);
143        assert!((s.curvature - 0.8).abs() < 1e-6);
144    }
145
146    #[test]
147    fn test_set_width() {
148        let mut s = new_gum_line_control_state();
149        set_gum_line_control_width(&mut s, 0.7);
150        assert!((s.width - 0.7).abs() < 1e-6);
151    }
152
153    #[test]
154    fn test_set_recession() {
155        let mut s = new_gum_line_control_state();
156        set_gum_line_control_recession(&mut s, 0.6);
157        assert!((s.recession - 0.6).abs() < 1e-6);
158    }
159
160    #[test]
161    fn test_compute_weights() {
162        let s = new_gum_line_control_state();
163        let cfg = default_gum_line_control_config();
164        let w = compute_gum_line_control_weights(&s, &cfg);
165        assert!((0.0..=1.0).contains(&w.exposed));
166        assert!((0.0..=1.0).contains(&w.curved));
167    }
168
169    #[test]
170    fn test_to_json() {
171        let s = new_gum_line_control_state();
172        let json = gum_line_control_to_json(&s);
173        assert!(json.contains("exposure"));
174        assert!(json.contains("recession"));
175    }
176
177    #[test]
178    fn test_blend() {
179        let a = new_gum_line_control_state();
180        let mut b = new_gum_line_control_state();
181        b.exposure = 1.0;
182        let mid = blend_gum_line_controls(&a, &b, 0.5);
183        assert!((mid.exposure - 0.75).abs() < 1e-6);
184    }
185
186    #[test]
187    fn test_blend_identity() {
188        let a = new_gum_line_control_state();
189        let r = blend_gum_line_controls(&a, &a, 0.5);
190        assert!((r.exposure - a.exposure).abs() < 1e-6);
191    }
192}