Skip to main content

oxihuman_morph/
inverted_triangle_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Inverted triangle (V-shape) body figure morph.
6
7/// Configuration for the inverted triangle morph.
8#[derive(Debug, Clone)]
9pub struct InvertedTriangleConfig {
10    pub shoulder_broadness: f32,
11    pub chest_width: f32,
12    pub hip_narrowness: f32,
13}
14
15impl Default for InvertedTriangleConfig {
16    fn default() -> Self {
17        InvertedTriangleConfig {
18            shoulder_broadness: 0.85,
19            chest_width: 0.7,
20            hip_narrowness: 0.6,
21        }
22    }
23}
24
25/// State for the inverted triangle morph.
26#[derive(Debug, Clone)]
27pub struct InvertedTriangleMorph {
28    pub intensity: f32,
29    pub config: InvertedTriangleConfig,
30    pub enabled: bool,
31}
32
33/// Create a new inverted triangle morph.
34pub fn new_inverted_triangle_morph() -> InvertedTriangleMorph {
35    InvertedTriangleMorph {
36        intensity: 0.0,
37        config: InvertedTriangleConfig::default(),
38        enabled: true,
39    }
40}
41
42/// Set intensity [0, 1].
43pub fn inv_set_intensity(m: &mut InvertedTriangleMorph, v: f32) {
44    m.intensity = v.clamp(0.0, 1.0);
45}
46
47/// Shoulder broadness weight.
48pub fn inv_shoulder_broad(m: &InvertedTriangleMorph) -> f32 {
49    m.intensity * m.config.shoulder_broadness
50}
51
52/// Chest width weight.
53pub fn inv_chest_width(m: &InvertedTriangleMorph) -> f32 {
54    m.intensity * m.config.chest_width
55}
56
57/// Hip narrowness weight.
58pub fn inv_hip_narrow(m: &InvertedTriangleMorph) -> f32 {
59    m.intensity * m.config.hip_narrowness
60}
61
62/// Shoulder-to-hip ratio estimate.
63pub fn inv_shoulder_hip_ratio(m: &InvertedTriangleMorph) -> f32 {
64    1.0 + 0.4 * m.intensity
65}
66
67/// Serialise to JSON.
68pub fn inv_to_json(m: &InvertedTriangleMorph) -> String {
69    format!(
70        r#"{{"intensity":{:.3},"shoulder_broad":{:.3},"enabled":{}}}"#,
71        m.intensity,
72        inv_shoulder_broad(m),
73        m.enabled
74    )
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn default_zero() {
83        let m = new_inverted_triangle_morph();
84        assert!((m.intensity - 0.0).abs() < 1e-6 /* zero */);
85    }
86
87    #[test]
88    fn clamp() {
89        let mut m = new_inverted_triangle_morph();
90        inv_set_intensity(&mut m, 5.0);
91        assert!((m.intensity - 1.0).abs() < 1e-6 /* clamped */);
92    }
93
94    #[test]
95    fn shoulder_broad_at_full() {
96        let mut m = new_inverted_triangle_morph();
97        inv_set_intensity(&mut m, 1.0);
98        assert!((inv_shoulder_broad(&m) - m.config.shoulder_broadness).abs() < 1e-6 /* correct */);
99    }
100
101    #[test]
102    fn hip_narrow_zero_at_zero() {
103        let m = new_inverted_triangle_morph();
104        assert!((inv_hip_narrow(&m) - 0.0).abs() < 1e-6 /* zero */);
105    }
106
107    #[test]
108    fn shoulder_hip_ratio_increases() {
109        let mut m = new_inverted_triangle_morph();
110        inv_set_intensity(&mut m, 0.0);
111        let r0 = inv_shoulder_hip_ratio(&m);
112        inv_set_intensity(&mut m, 1.0);
113        let r1 = inv_shoulder_hip_ratio(&m);
114        assert!(r1 > r0 /* more inverted triangle at higher intensity */);
115    }
116
117    #[test]
118    fn json_has_shoulder() {
119        let m = new_inverted_triangle_morph();
120        assert!(inv_to_json(&m).contains("shoulder") /* json has field */);
121    }
122
123    #[test]
124    fn enabled_default() {
125        let m = new_inverted_triangle_morph();
126        assert!(m.enabled /* enabled */);
127    }
128
129    #[test]
130    fn config_positive() {
131        let m = new_inverted_triangle_morph();
132        assert!(m.config.chest_width > 0.0 /* positive */);
133    }
134}