Skip to main content

oxihuman_morph/
tooth_shape_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Tooth shape and alignment morph control.
5
6/// Tooth shape parameters.
7#[allow(dead_code)]
8#[derive(Debug, Clone, PartialEq)]
9pub struct ToothShapeParams {
10    /// Overall tooth width scale 0..=1.
11    pub width: f32,
12    /// Tooth height scale 0..=1.
13    pub height: f32,
14    /// Incisor rounding 0..=1 (0 = sharp, 1 = very round).
15    pub rounding: f32,
16    /// Overbite magnitude 0..=1.
17    pub overbite: f32,
18    /// Crowding 0..=1 (overlap/misalignment).
19    pub crowding: f32,
20    /// Whiteness 0..=1.
21    pub whiteness: f32,
22    /// Translucency of incisal edge 0..=1.
23    pub translucency: f32,
24}
25
26impl Default for ToothShapeParams {
27    fn default() -> Self {
28        Self {
29            width: 0.5,
30            height: 0.5,
31            rounding: 0.4,
32            overbite: 0.2,
33            crowding: 0.0,
34            whiteness: 0.8,
35            translucency: 0.2,
36        }
37    }
38}
39
40/// Create default params.
41#[allow(dead_code)]
42pub fn default_tooth_shape_params() -> ToothShapeParams {
43    ToothShapeParams::default()
44}
45
46/// Set tooth width.
47#[allow(dead_code)]
48pub fn set_tooth_width(params: &mut ToothShapeParams, value: f32) {
49    params.width = value.clamp(0.0, 1.0);
50}
51
52/// Set tooth height.
53#[allow(dead_code)]
54pub fn set_tooth_height(params: &mut ToothShapeParams, value: f32) {
55    params.height = value.clamp(0.0, 1.0);
56}
57
58/// Set rounding.
59#[allow(dead_code)]
60pub fn set_tooth_rounding(params: &mut ToothShapeParams, value: f32) {
61    params.rounding = value.clamp(0.0, 1.0);
62}
63
64/// Set overbite.
65#[allow(dead_code)]
66pub fn set_tooth_overbite(params: &mut ToothShapeParams, value: f32) {
67    params.overbite = value.clamp(0.0, 1.0);
68}
69
70/// Set crowding.
71#[allow(dead_code)]
72pub fn set_tooth_crowding(params: &mut ToothShapeParams, value: f32) {
73    params.crowding = value.clamp(0.0, 1.0);
74}
75
76/// Set whiteness.
77#[allow(dead_code)]
78pub fn set_tooth_whiteness(params: &mut ToothShapeParams, value: f32) {
79    params.whiteness = value.clamp(0.0, 1.0);
80}
81
82/// Compute tooth color RGB from whiteness and translucency.
83#[allow(dead_code)]
84pub fn tooth_color_rgb(params: &ToothShapeParams) -> [f32; 3] {
85    let w = params.whiteness.clamp(0.0, 1.0);
86    let t = params.translucency.clamp(0.0, 1.0);
87    let r = 0.85 + w * 0.12;
88    let g = 0.80 + w * 0.10 - t * 0.03;
89    let b = 0.70 + w * 0.08 - t * 0.05;
90    [r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)]
91}
92
93/// Blend two tooth shape params.
94#[allow(dead_code)]
95pub fn blend_tooth_shape(a: &ToothShapeParams, b: &ToothShapeParams, t: f32) -> ToothShapeParams {
96    let t = t.clamp(0.0, 1.0);
97    let inv = 1.0 - t;
98    ToothShapeParams {
99        width: a.width * inv + b.width * t,
100        height: a.height * inv + b.height * t,
101        rounding: a.rounding * inv + b.rounding * t,
102        overbite: a.overbite * inv + b.overbite * t,
103        crowding: a.crowding * inv + b.crowding * t,
104        whiteness: a.whiteness * inv + b.whiteness * t,
105        translucency: a.translucency * inv + b.translucency * t,
106    }
107}
108
109/// Reset to default.
110#[allow(dead_code)]
111pub fn reset_tooth_shape(params: &mut ToothShapeParams) {
112    *params = ToothShapeParams::default();
113}
114
115/// Serialize to JSON.
116#[allow(dead_code)]
117pub fn tooth_shape_to_json(params: &ToothShapeParams) -> String {
118    format!(
119        r#"{{"width":{:.4},"height":{:.4},"rounding":{:.4},"overbite":{:.4},"crowding":{:.4},"whiteness":{:.4}}}"#,
120        params.width,
121        params.height,
122        params.rounding,
123        params.overbite,
124        params.crowding,
125        params.whiteness
126    )
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_default() {
135        let p = ToothShapeParams::default();
136        assert!((0.0..=1.0).contains(&p.width));
137    }
138
139    #[test]
140    fn test_set_width_clamp() {
141        let mut p = ToothShapeParams::default();
142        set_tooth_width(&mut p, 5.0);
143        assert!((p.width - 1.0).abs() < 1e-6);
144    }
145
146    #[test]
147    fn test_set_height_clamp() {
148        let mut p = ToothShapeParams::default();
149        set_tooth_height(&mut p, -1.0);
150        assert!(p.height.abs() < 1e-6);
151    }
152
153    #[test]
154    fn test_set_rounding() {
155        let mut p = ToothShapeParams::default();
156        set_tooth_rounding(&mut p, 0.9);
157        assert!((p.rounding - 0.9).abs() < 1e-6);
158    }
159
160    #[test]
161    fn test_set_whiteness() {
162        let mut p = ToothShapeParams::default();
163        set_tooth_whiteness(&mut p, 0.3);
164        assert!((p.whiteness - 0.3).abs() < 1e-6);
165    }
166
167    #[test]
168    fn test_tooth_color_bright() {
169        let p = ToothShapeParams {
170            whiteness: 1.0,
171            translucency: 0.0,
172            ..Default::default()
173        };
174        let c = tooth_color_rgb(&p);
175        assert!(c[0] > 0.9);
176    }
177
178    #[test]
179    fn test_tooth_color_range() {
180        let p = ToothShapeParams::default();
181        let c = tooth_color_rgb(&p);
182        for ch in c {
183            assert!((0.0..=1.0).contains(&ch));
184        }
185    }
186
187    #[test]
188    fn test_blend_midpoint() {
189        let a = ToothShapeParams {
190            width: 0.0,
191            ..Default::default()
192        };
193        let b = ToothShapeParams {
194            width: 1.0,
195            ..Default::default()
196        };
197        let r = blend_tooth_shape(&a, &b, 0.5);
198        assert!((r.width - 0.5).abs() < 1e-6);
199    }
200
201    #[test]
202    fn test_reset() {
203        let mut p = ToothShapeParams {
204            whiteness: 0.1,
205            ..Default::default()
206        };
207        reset_tooth_shape(&mut p);
208        assert!((p.whiteness - 0.8).abs() < 1e-6);
209    }
210
211    #[test]
212    fn test_to_json() {
213        let j = tooth_shape_to_json(&ToothShapeParams::default());
214        assert!(j.contains("whiteness"));
215        assert!(j.contains("crowding"));
216    }
217}