Skip to main content

oxihuman_morph/
sideburn_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Sideburn shape and length morph control.
6
7/// Sideburn morph configuration.
8#[derive(Debug, Clone)]
9pub struct SideburnMorph {
10    pub length: f32,
11    pub width: f32,
12    pub taper: f32,
13    pub density: f32,
14}
15
16impl SideburnMorph {
17    pub fn new() -> Self {
18        Self {
19            length: 0.5,
20            width: 0.4,
21            taper: 0.5,
22            density: 0.5,
23        }
24    }
25}
26
27impl Default for SideburnMorph {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33/// Create a new sideburn morph.
34pub fn new_sideburn_morph() -> SideburnMorph {
35    SideburnMorph::new()
36}
37
38/// Set vertical length of sideburns in normalized range.
39pub fn sideburn_set_length(morph: &mut SideburnMorph, length: f32) {
40    morph.length = length.clamp(0.0, 1.0);
41}
42
43/// Set horizontal width of sideburns.
44pub fn sideburn_set_width(morph: &mut SideburnMorph, width: f32) {
45    morph.width = width.clamp(0.0, 1.0);
46}
47
48/// Set taper factor (0 = straight, 1 = fully tapered).
49pub fn sideburn_set_taper(morph: &mut SideburnMorph, taper: f32) {
50    morph.taper = taper.clamp(0.0, 1.0);
51}
52
53/// Estimate visible area as length * average_width.
54pub fn sideburn_area_estimate(morph: &SideburnMorph) -> f32 {
55    let avg_width = morph.width * (1.0 - morph.taper * 0.5);
56    morph.length * avg_width
57}
58
59/// Serialize to JSON-like string.
60pub fn sideburn_morph_to_json(morph: &SideburnMorph) -> String {
61    format!(
62        r#"{{"length":{:.4},"width":{:.4},"taper":{:.4},"density":{:.4}}}"#,
63        morph.length, morph.width, morph.taper, morph.density
64    )
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_defaults() {
73        let m = new_sideburn_morph();
74        assert!((m.width - 0.4).abs() < 1e-6);
75    }
76
77    #[test]
78    fn test_length_clamp() {
79        let mut m = new_sideburn_morph();
80        sideburn_set_length(&mut m, 2.0);
81        assert_eq!(m.length, 1.0);
82    }
83
84    #[test]
85    fn test_width_set() {
86        let mut m = new_sideburn_morph();
87        sideburn_set_width(&mut m, 0.6);
88        assert!((m.width - 0.6).abs() < 1e-6);
89    }
90
91    #[test]
92    fn test_taper_set() {
93        let mut m = new_sideburn_morph();
94        sideburn_set_taper(&mut m, 0.8);
95        assert!((m.taper - 0.8).abs() < 1e-6);
96    }
97
98    #[test]
99    fn test_area_estimate_positive() {
100        let m = new_sideburn_morph();
101        assert!(sideburn_area_estimate(&m) >= 0.0);
102    }
103
104    #[test]
105    fn test_area_zero_length() {
106        let mut m = new_sideburn_morph();
107        sideburn_set_length(&mut m, 0.0);
108        assert_eq!(sideburn_area_estimate(&m), 0.0);
109    }
110
111    #[test]
112    fn test_json() {
113        let m = new_sideburn_morph();
114        let s = sideburn_morph_to_json(&m);
115        assert!(s.contains("taper"));
116    }
117
118    #[test]
119    fn test_clone() {
120        let m = new_sideburn_morph();
121        let m2 = m.clone();
122        assert!((m2.taper - m.taper).abs() < 1e-6);
123    }
124
125    #[test]
126    fn test_default_trait() {
127        let m: SideburnMorph = Default::default();
128        assert!((m.density - 0.5).abs() < 1e-6);
129    }
130}