Skip to main content

oxihuman_morph/
hair_part_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hair parting position morph control.
6
7/// Hair part position style.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum HairPartStyle {
10    Center,
11    Left,
12    Right,
13    NoParting,
14}
15
16/// Hair part morph configuration.
17#[derive(Debug, Clone)]
18pub struct HairPartMorph {
19    pub style: HairPartStyle,
20    pub offset: f32,
21    pub depth: f32,
22}
23
24impl HairPartMorph {
25    pub fn new() -> Self {
26        Self {
27            style: HairPartStyle::Center,
28            offset: 0.0,
29            depth: 0.5,
30        }
31    }
32}
33
34impl Default for HairPartMorph {
35    fn default() -> Self {
36        Self::new()
37    }
38}
39
40/// Create a new hair part morph.
41pub fn new_hair_part_morph() -> HairPartMorph {
42    HairPartMorph::new()
43}
44
45/// Set parting offset from center (negative = left, positive = right).
46pub fn hair_part_set_offset(morph: &mut HairPartMorph, offset: f32) {
47    morph.offset = offset.clamp(-1.0, 1.0);
48}
49
50/// Set depth/sharpness of the part line.
51pub fn hair_part_set_depth(morph: &mut HairPartMorph, depth: f32) {
52    morph.depth = depth.clamp(0.0, 1.0);
53}
54
55/// Set the parting style enum.
56pub fn hair_part_set_style(morph: &mut HairPartMorph, style: HairPartStyle) {
57    morph.style = style;
58}
59
60/// Infer style from offset magnitude.
61pub fn hair_part_infer_style(morph: &HairPartMorph) -> HairPartStyle {
62    if morph.offset < -0.1 {
63        HairPartStyle::Left
64    } else if morph.offset > 0.1 {
65        HairPartStyle::Right
66    } else {
67        HairPartStyle::Center
68    }
69}
70
71/// Serialize to JSON-like string.
72pub fn hair_part_morph_to_json(morph: &HairPartMorph) -> String {
73    let style_str = match morph.style {
74        HairPartStyle::Center => "center",
75        HairPartStyle::Left => "left",
76        HairPartStyle::Right => "right",
77        HairPartStyle::NoParting => "none",
78    };
79    format!(
80        r#"{{"style":"{style_str}","offset":{:.4},"depth":{:.4}}}"#,
81        morph.offset, morph.depth
82    )
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_defaults() {
91        let m = new_hair_part_morph();
92        assert_eq!(m.style, HairPartStyle::Center);
93        assert_eq!(m.offset, 0.0);
94    }
95
96    #[test]
97    fn test_offset_clamp_positive() {
98        let mut m = new_hair_part_morph();
99        hair_part_set_offset(&mut m, 2.0);
100        assert_eq!(m.offset, 1.0);
101    }
102
103    #[test]
104    fn test_offset_clamp_negative() {
105        let mut m = new_hair_part_morph();
106        hair_part_set_offset(&mut m, -2.0);
107        assert_eq!(m.offset, -1.0);
108    }
109
110    #[test]
111    fn test_depth_set() {
112        let mut m = new_hair_part_morph();
113        hair_part_set_depth(&mut m, 0.7);
114        assert!((m.depth - 0.7).abs() < 1e-6);
115    }
116
117    #[test]
118    fn test_style_set() {
119        let mut m = new_hair_part_morph();
120        hair_part_set_style(&mut m, HairPartStyle::Right);
121        assert_eq!(m.style, HairPartStyle::Right);
122    }
123
124    #[test]
125    fn test_infer_style_left() {
126        let mut m = new_hair_part_morph();
127        hair_part_set_offset(&mut m, -0.5);
128        assert_eq!(hair_part_infer_style(&m), HairPartStyle::Left);
129    }
130
131    #[test]
132    fn test_json_contains_style() {
133        let m = new_hair_part_morph();
134        let s = hair_part_morph_to_json(&m);
135        assert!(s.contains("style"));
136    }
137
138    #[test]
139    fn test_clone() {
140        let m = new_hair_part_morph();
141        let m2 = m.clone();
142        assert_eq!(m2.style, m.style);
143    }
144
145    #[test]
146    fn test_default_trait() {
147        let m: HairPartMorph = Default::default();
148        assert!((m.depth - 0.5).abs() < 1e-6);
149    }
150}