#![allow(dead_code)]
use std::f32::consts::PI;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HairlineType {
Straight,
Rounded,
MShaped,
WidowsPeak,
Receding,
}
#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct HairlineParams {
pub height: f32,
pub temple_recession: f32,
pub widows_peak: f32,
pub recession: f32,
pub width: f32,
pub asymmetry: f32,
}
impl Default for HairlineParams {
fn default() -> Self {
Self {
height: 0.5,
temple_recession: 0.2,
widows_peak: 0.0,
recession: 0.0,
width: 0.5,
asymmetry: 0.0,
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HairlineResult {
pub displacements: Vec<(usize, f32)>,
pub hairline_type: HairlineType,
pub forehead_area: f32,
}
#[allow(dead_code)]
pub fn base_curve(x: f32, height: f32) -> f32 {
let x = x.clamp(-1.0, 1.0);
height * 0.03 * (1.0 - 0.3 * x * x)
}
#[allow(dead_code)]
pub fn temple_dip(x: f32, depth: f32) -> f32 {
let x = x.clamp(-1.0, 1.0);
let ax = x.abs();
let dist = (ax - 0.7).abs();
if dist < 0.25 {
-depth * 0.02 * (1.0 - dist / 0.25)
} else {
0.0
}
}
#[allow(dead_code)]
pub fn widows_peak(x: f32, prominence: f32) -> f32 {
let x = x.clamp(-1.0, 1.0);
if x.abs() < 0.15 {
-prominence * 0.015 * (1.0 - x.abs() / 0.15)
} else {
0.0
}
}
#[allow(dead_code)]
pub fn recession_offset(recession: f32) -> f32 {
recession.clamp(0.0, 1.0) * 0.04
}
#[allow(dead_code)]
pub fn asymmetry_modifier(x: f32, asymmetry: f32) -> f32 {
let x = x.clamp(-1.0, 1.0);
1.0 + asymmetry * x * 0.2
}
#[allow(dead_code)]
pub fn classify_hairline(params: &HairlineParams) -> HairlineType {
if params.recession > 0.6 {
HairlineType::Receding
} else if params.widows_peak > 0.5 {
HairlineType::WidowsPeak
} else if params.temple_recession > 0.5 {
HairlineType::MShaped
} else if params.temple_recession < 0.1 && params.widows_peak < 0.1 {
HairlineType::Straight
} else {
HairlineType::Rounded
}
}
#[allow(dead_code)]
pub fn evaluate_hairline(
scalp_coords: &[(f32, f32)],
params: &HairlineParams,
) -> HairlineResult {
let mut displacements = Vec::with_capacity(scalp_coords.len());
for (i, &(x, dist)) in scalp_coords.iter().enumerate() {
let base = base_curve(x, params.height);
let temple = temple_dip(x, params.temple_recession);
let wp = widows_peak(x, params.widows_peak);
let recess = recession_offset(params.recession);
let asym = asymmetry_modifier(x, params.asymmetry);
let total = (base + temple + wp + recess) * asym;
let falloff = (-dist * dist * 200.0).exp();
let disp = total * falloff;
if disp.abs() > 1e-7 {
displacements.push((i, disp));
}
}
let hairline_type = classify_hairline(params);
let forehead_area = params.height * (1.0 + params.recession);
HairlineResult {
displacements,
hairline_type,
forehead_area,
}
}
#[allow(dead_code)]
pub fn blend_hairline_params(a: &HairlineParams, b: &HairlineParams, t: f32) -> HairlineParams {
let t = t.clamp(0.0, 1.0);
let inv = 1.0 - t;
HairlineParams {
height: a.height * inv + b.height * t,
temple_recession: a.temple_recession * inv + b.temple_recession * t,
widows_peak: a.widows_peak * inv + b.widows_peak * t,
recession: a.recession * inv + b.recession * t,
width: a.width * inv + b.width * t,
asymmetry: a.asymmetry * inv + b.asymmetry * t,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_params() {
let p = HairlineParams::default();
assert!((0.0..=1.0).contains(&p.height));
assert!((p.recession).abs() < 1e-6);
}
#[test]
fn test_base_curve_centre() {
let v = base_curve(0.0, 1.0);
assert!(v > 0.0);
}
#[test]
fn test_temple_dip_at_temple() {
let d = temple_dip(0.7, 1.0);
assert!(d < 0.0);
}
#[test]
fn test_temple_dip_at_centre() {
let d = temple_dip(0.0, 1.0);
assert!(d.abs() < 1e-6);
}
#[test]
fn test_widows_peak_centre() {
let w = widows_peak(0.0, 1.0);
assert!(w < 0.0);
}
#[test]
fn test_widows_peak_lateral() {
let w = widows_peak(0.5, 1.0);
assert!(w.abs() < 1e-6);
}
#[test]
fn test_classify_receding() {
let p = HairlineParams { recession: 0.8, ..Default::default() };
assert_eq!(classify_hairline(&p), HairlineType::Receding);
}
#[test]
fn test_classify_widows_peak() {
let p = HairlineParams { widows_peak: 0.7, ..Default::default() };
assert_eq!(classify_hairline(&p), HairlineType::WidowsPeak);
}
#[test]
fn test_evaluate_empty() {
let r = evaluate_hairline(&[], &HairlineParams::default());
assert!(r.displacements.is_empty());
}
#[test]
fn test_blend_hairline() {
let a = HairlineParams::default();
let b = HairlineParams { height: 1.0, ..Default::default() };
let r = blend_hairline_params(&a, &b, 0.5);
assert!((r.height - 0.75).abs() < 1e-5);
}
}