Skip to main content

oxihuman_morph/
nail_shape_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Fingernail shape morph (square, oval, pointed).
6
7/// Nail shape preset.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum NailShape {
11    Square,
12    Oval,
13    Pointed,
14    Squoval,
15    Almond,
16}
17
18impl NailShape {
19    #[allow(dead_code)]
20    pub fn name(self) -> &'static str {
21        match self {
22            NailShape::Square => "square",
23            NailShape::Oval => "oval",
24            NailShape::Pointed => "pointed",
25            NailShape::Squoval => "squoval",
26            NailShape::Almond => "almond",
27        }
28    }
29}
30
31/// Parameters for nail shape morph.
32#[allow(dead_code)]
33#[derive(Debug, Clone)]
34pub struct NailShapeParams {
35    pub shape: NailShape,
36    pub length: f32,
37    pub curvature: f32,
38    pub width_scale: f32,
39}
40
41impl Default for NailShapeParams {
42    fn default() -> Self {
43        NailShapeParams {
44            shape: NailShape::Oval,
45            length: 0.0,
46            curvature: 0.0,
47            width_scale: 0.0,
48        }
49    }
50}
51
52#[allow(dead_code)]
53pub fn default_nail_shape_params() -> NailShapeParams {
54    NailShapeParams::default()
55}
56
57#[allow(dead_code)]
58pub fn nail_set_shape(p: &mut NailShapeParams, s: NailShape) {
59    p.shape = s;
60}
61
62#[allow(dead_code)]
63pub fn nail_set_length(p: &mut NailShapeParams, v: f32) {
64    p.length = v.clamp(0.0, 1.0);
65}
66
67#[allow(dead_code)]
68pub fn nail_set_curvature(p: &mut NailShapeParams, v: f32) {
69    p.curvature = v.clamp(-1.0, 1.0);
70}
71
72#[allow(dead_code)]
73pub fn nail_set_width_scale(p: &mut NailShapeParams, v: f32) {
74    p.width_scale = v.clamp(-0.5, 0.5);
75}
76
77#[allow(dead_code)]
78pub fn nail_reset(p: &mut NailShapeParams) {
79    *p = NailShapeParams::default();
80}
81
82#[allow(dead_code)]
83pub fn nail_is_neutral(p: &NailShapeParams) -> bool {
84    p.length.abs() < 1e-6 && p.curvature.abs() < 1e-6 && p.width_scale.abs() < 1e-6
85}
86
87#[allow(dead_code)]
88pub fn nail_blend(a: &NailShapeParams, b: &NailShapeParams, t: f32) -> NailShapeParams {
89    let t = t.clamp(0.0, 1.0);
90    let shape = if t < 0.5 { a.shape } else { b.shape };
91    NailShapeParams {
92        shape,
93        length: a.length + (b.length - a.length) * t,
94        curvature: a.curvature + (b.curvature - a.curvature) * t,
95        width_scale: a.width_scale + (b.width_scale - a.width_scale) * t,
96    }
97}
98
99/// Sharpness index: square=0, oval=0.5, pointed=1.
100#[allow(dead_code)]
101pub fn nail_sharpness_index(p: &NailShapeParams) -> f32 {
102    match p.shape {
103        NailShape::Square => 0.0,
104        NailShape::Squoval => 0.25,
105        NailShape::Oval => 0.5,
106        NailShape::Almond => 0.75,
107        NailShape::Pointed => 1.0,
108    }
109}
110
111#[allow(dead_code)]
112pub fn nail_to_json(p: &NailShapeParams) -> String {
113    format!(
114        r#"{{"shape":"{}","length":{:.4},"curvature":{:.4},"width_scale":{:.4}}}"#,
115        p.shape.name(),
116        p.length,
117        p.curvature,
118        p.width_scale
119    )
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn default_shape_is_oval() {
128        assert_eq!(default_nail_shape_params().shape, NailShape::Oval);
129    }
130
131    #[test]
132    fn default_is_neutral() {
133        assert!(nail_is_neutral(&default_nail_shape_params()));
134    }
135
136    #[test]
137    fn set_length_clamps() {
138        let mut p = default_nail_shape_params();
139        nail_set_length(&mut p, 5.0);
140        assert!((p.length - 1.0).abs() < 1e-6);
141    }
142
143    #[test]
144    fn set_shape_square() {
145        let mut p = default_nail_shape_params();
146        nail_set_shape(&mut p, NailShape::Square);
147        assert_eq!(p.shape, NailShape::Square);
148    }
149
150    #[test]
151    fn reset_clears() {
152        let mut p = default_nail_shape_params();
153        nail_set_length(&mut p, 0.7);
154        nail_reset(&mut p);
155        assert!(nail_is_neutral(&p));
156    }
157
158    #[test]
159    fn sharpness_square_zero() {
160        let mut p = default_nail_shape_params();
161        nail_set_shape(&mut p, NailShape::Square);
162        assert!(nail_sharpness_index(&p).abs() < 1e-6);
163    }
164
165    #[test]
166    fn sharpness_pointed_one() {
167        let mut p = default_nail_shape_params();
168        nail_set_shape(&mut p, NailShape::Pointed);
169        assert!((nail_sharpness_index(&p) - 1.0).abs() < 1e-6);
170    }
171
172    #[test]
173    fn blend_midpoint_length() {
174        let a = default_nail_shape_params();
175        let mut b = default_nail_shape_params();
176        nail_set_length(&mut b, 1.0);
177        let m = nail_blend(&a, &b, 0.5);
178        assert!((m.length - 0.5).abs() < 1e-5);
179    }
180
181    #[test]
182    fn to_json_contains_shape() {
183        assert!(nail_to_json(&default_nail_shape_params()).contains("shape"));
184    }
185}