Skip to main content

oxihuman_morph/
hourglass_proportion.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hourglass figure proportion morph.
6
7/// Configuration for the hourglass morph.
8#[derive(Debug, Clone)]
9pub struct HourglassConfig {
10    pub bust_fullness: f32,
11    pub waist_cinch: f32,
12    pub hip_fullness: f32,
13}
14
15impl Default for HourglassConfig {
16    fn default() -> Self {
17        HourglassConfig {
18            bust_fullness: 0.7,
19            waist_cinch: 0.8,
20            hip_fullness: 0.75,
21        }
22    }
23}
24
25/// State for the hourglass proportion morph.
26#[derive(Debug, Clone)]
27pub struct HourglassProportion {
28    pub intensity: f32,
29    pub config: HourglassConfig,
30    pub enabled: bool,
31}
32
33/// Create a new hourglass proportion morph.
34pub fn new_hourglass_proportion() -> HourglassProportion {
35    HourglassProportion {
36        intensity: 0.0,
37        config: HourglassConfig::default(),
38        enabled: true,
39    }
40}
41
42/// Set intensity [0, 1].
43pub fn hg_set_intensity(m: &mut HourglassProportion, v: f32) {
44    m.intensity = v.clamp(0.0, 1.0);
45}
46
47/// Bust fullness weight.
48pub fn hg_bust(m: &HourglassProportion) -> f32 {
49    m.intensity * m.config.bust_fullness
50}
51
52/// Waist cinch (narrowing) weight.
53pub fn hg_waist(m: &HourglassProportion) -> f32 {
54    m.intensity * m.config.waist_cinch
55}
56
57/// Hip fullness weight.
58pub fn hg_hips(m: &HourglassProportion) -> f32 {
59    m.intensity * m.config.hip_fullness
60}
61
62/// Waist-to-hip ratio estimate (lower = more hourglass).
63pub fn hg_whr(m: &HourglassProportion) -> f32 {
64    let base = 0.7_f32;
65    base - 0.15 * m.intensity
66}
67
68/// Serialise to JSON.
69pub fn hg_to_json(m: &HourglassProportion) -> String {
70    format!(
71        r#"{{"intensity":{:.3},"whr":{:.3},"enabled":{}}}"#,
72        m.intensity,
73        hg_whr(m),
74        m.enabled
75    )
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn default_zero() {
84        let m = new_hourglass_proportion();
85        assert!((m.intensity - 0.0).abs() < 1e-6 /* zero */);
86    }
87
88    #[test]
89    fn clamp_intensity() {
90        let mut m = new_hourglass_proportion();
91        hg_set_intensity(&mut m, -1.0);
92        assert!((m.intensity - 0.0).abs() < 1e-6 /* clamped */);
93    }
94
95    #[test]
96    fn bust_at_max() {
97        let mut m = new_hourglass_proportion();
98        hg_set_intensity(&mut m, 1.0);
99        assert!((hg_bust(&m) - m.config.bust_fullness).abs() < 1e-6 /* correct */);
100    }
101
102    #[test]
103    fn waist_cinch_zero_at_zero() {
104        let m = new_hourglass_proportion();
105        assert!((hg_waist(&m) - 0.0).abs() < 1e-6 /* zero waist cinch */);
106    }
107
108    #[test]
109    fn whr_decreases_with_intensity() {
110        let mut m = new_hourglass_proportion();
111        hg_set_intensity(&mut m, 0.0);
112        let whr0 = hg_whr(&m);
113        hg_set_intensity(&mut m, 1.0);
114        let whr1 = hg_whr(&m);
115        assert!(whr1 < whr0 /* more hourglass at higher intensity */);
116    }
117
118    #[test]
119    fn hips_at_half_intensity() {
120        let mut m = new_hourglass_proportion();
121        hg_set_intensity(&mut m, 0.5);
122        let h = hg_hips(&m);
123        assert!(h > 0.0 && h < 1.0 /* partial */);
124    }
125
126    #[test]
127    fn json_contains_whr() {
128        let m = new_hourglass_proportion();
129        assert!(hg_to_json(&m).contains("whr") /* json has whr */);
130    }
131
132    #[test]
133    fn enabled_default() {
134        let m = new_hourglass_proportion();
135        assert!(m.enabled /* enabled */);
136    }
137}