Skip to main content

oxihuman_morph/
alar_base_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Nostril width (alar base) morph control.
5
6#![allow(dead_code)]
7
8/// Alar base parameters.
9#[allow(dead_code)]
10#[derive(Debug, Clone)]
11pub struct AlarBaseParams {
12    /// Width: 0.0 = narrow, 1.0 = wide.
13    pub width: f32,
14    /// Flare: 0.0 = no flare, 1.0 = maximum flare.
15    pub flare: f32,
16    /// Height offset: -1.0 = low insertion, 1.0 = high insertion.
17    pub height: f32,
18}
19
20#[allow(dead_code)]
21impl Default for AlarBaseParams {
22    fn default() -> Self {
23        Self {
24            width: 0.5,
25            flare: 0.0,
26            height: 0.0,
27        }
28    }
29}
30
31/// Morph weight output.
32#[allow(dead_code)]
33#[derive(Debug, Clone)]
34pub struct AlarBaseWeights {
35    pub width_w: f32,
36    pub flare_w: f32,
37    pub height_w: f32,
38}
39
40/// Create default alar base params.
41#[allow(dead_code)]
42pub fn default_alar_base() -> AlarBaseParams {
43    AlarBaseParams::default()
44}
45
46/// Evaluate morph weights.
47#[allow(dead_code)]
48pub fn evaluate_alar_base(p: &AlarBaseParams) -> AlarBaseWeights {
49    AlarBaseWeights {
50        width_w: p.width.clamp(0.0, 1.0),
51        flare_w: p.flare.clamp(0.0, 1.0),
52        height_w: p.height.clamp(-1.0, 1.0),
53    }
54}
55
56/// Blend two param sets.
57#[allow(dead_code)]
58pub fn blend_alar_base(a: &AlarBaseParams, b: &AlarBaseParams, t: f32) -> AlarBaseParams {
59    let t = t.clamp(0.0, 1.0);
60    AlarBaseParams {
61        width: a.width + (b.width - a.width) * t,
62        flare: a.flare + (b.flare - a.flare) * t,
63        height: a.height + (b.height - a.height) * t,
64    }
65}
66
67/// Set alar width.
68#[allow(dead_code)]
69pub fn set_alar_width(p: &mut AlarBaseParams, value: f32) {
70    p.width = value.clamp(0.0, 1.0);
71}
72
73/// Set alar flare.
74#[allow(dead_code)]
75pub fn set_alar_flare(p: &mut AlarBaseParams, value: f32) {
76    p.flare = value.clamp(0.0, 1.0);
77}
78
79/// Check validity.
80#[allow(dead_code)]
81pub fn is_valid_alar_base(p: &AlarBaseParams) -> bool {
82    (0.0..=1.0).contains(&p.width)
83        && (0.0..=1.0).contains(&p.flare)
84        && (-1.0..=1.0).contains(&p.height)
85}
86
87/// Reset to default.
88#[allow(dead_code)]
89pub fn reset_alar_base(p: &mut AlarBaseParams) {
90    *p = AlarBaseParams::default();
91}
92
93/// Serialize to JSON.
94#[allow(dead_code)]
95pub fn alar_base_to_json(p: &AlarBaseParams) -> String {
96    format!(
97        r#"{{"width":{:.4},"flare":{:.4},"height":{:.4}}}"#,
98        p.width, p.flare, p.height
99    )
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_default() {
108        let p = AlarBaseParams::default();
109        assert!((p.width - 0.5).abs() < 1e-6);
110        assert!(p.flare.abs() < 1e-6);
111    }
112
113    #[test]
114    fn test_evaluate_clamps() {
115        let p = AlarBaseParams {
116            width: 2.0,
117            flare: -1.0,
118            height: 0.0,
119        };
120        let w = evaluate_alar_base(&p);
121        assert!((w.width_w - 1.0).abs() < 1e-6);
122        assert!(w.flare_w < 1e-6);
123    }
124
125    #[test]
126    fn test_blend_identity() {
127        let a = AlarBaseParams::default();
128        let b = AlarBaseParams::default();
129        let m = blend_alar_base(&a, &b, 0.5);
130        assert!((m.width - 0.5).abs() < 1e-6);
131    }
132
133    #[test]
134    fn test_blend_midpoint() {
135        let a = AlarBaseParams {
136            width: 0.0,
137            flare: 0.0,
138            height: 0.0,
139        };
140        let b = AlarBaseParams {
141            width: 1.0,
142            flare: 1.0,
143            height: 1.0,
144        };
145        let m = blend_alar_base(&a, &b, 0.5);
146        assert!((m.width - 0.5).abs() < 1e-5);
147    }
148
149    #[test]
150    fn test_set_width_clamped() {
151        let mut p = AlarBaseParams::default();
152        set_alar_width(&mut p, -5.0);
153        assert!(p.width < 1e-6);
154    }
155
156    #[test]
157    fn test_set_flare_clamped() {
158        let mut p = AlarBaseParams::default();
159        set_alar_flare(&mut p, 5.0);
160        assert!((p.flare - 1.0).abs() < 1e-6);
161    }
162
163    #[test]
164    fn test_is_valid_default() {
165        assert!(is_valid_alar_base(&AlarBaseParams::default()));
166    }
167
168    #[test]
169    fn test_reset() {
170        let mut p = AlarBaseParams {
171            width: 0.9,
172            flare: 0.8,
173            height: 0.5,
174        };
175        reset_alar_base(&mut p);
176        assert!((p.width - 0.5).abs() < 1e-6);
177    }
178
179    #[test]
180    fn test_to_json_contains_fields() {
181        let j = alar_base_to_json(&AlarBaseParams::default());
182        assert!(j.contains("width"));
183        assert!(j.contains("flare"));
184    }
185}