Skip to main content

oxihuman_export/
beard_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5pub struct BeardStrand {
6    pub root: [f32; 3],
7    pub length_mm: f32,
8    pub diameter_um: f32,
9    pub color_rgb: [f32; 3],
10}
11
12pub fn new_beard_strand(root: [f32; 3], length: f32) -> BeardStrand {
13    BeardStrand {
14        root,
15        length_mm: length,
16        diameter_um: 80.0,
17        color_rgb: [0.15, 0.10, 0.08],
18    }
19}
20
21pub fn beard_strand_to_csv_line(s: &BeardStrand) -> String {
22    format!(
23        "{:.4},{:.4},{:.4},{:.3},{:.2},{:.4},{:.4},{:.4}",
24        s.root[0],
25        s.root[1],
26        s.root[2],
27        s.length_mm,
28        s.diameter_um,
29        s.color_rgb[0],
30        s.color_rgb[1],
31        s.color_rgb[2]
32    )
33}
34
35pub fn beard_strands_to_csv(strands: &[BeardStrand]) -> String {
36    let mut out = String::from("rx,ry,rz,length_mm,diameter_um,cr,cg,cb\n");
37    for s in strands {
38        out.push_str(&beard_strand_to_csv_line(s));
39        out.push('\n');
40    }
41    out
42}
43
44pub fn beard_mean_length(strands: &[BeardStrand]) -> f32 {
45    if strands.is_empty() {
46        return 0.0;
47    }
48    strands.iter().map(|s| s.length_mm).sum::<f32>() / strands.len() as f32
49}
50
51pub fn beard_coverage_density(strands: &[BeardStrand], area_cm2: f32) -> f32 {
52    if area_cm2 <= 0.0 {
53        return 0.0;
54    }
55    strands.len() as f32 / area_cm2
56}
57
58pub fn beard_count(strands: &[BeardStrand]) -> usize {
59    strands.len()
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_new_beard_strand() {
68        /* construction */
69        let s = new_beard_strand([0.0; 3], 10.0);
70        assert!((s.length_mm - 10.0).abs() < 1e-6);
71        assert!((s.diameter_um - 80.0).abs() < 1e-6);
72    }
73
74    #[test]
75    fn test_to_csv_line() {
76        /* CSV line */
77        let s = new_beard_strand([1.0, 2.0, 3.0], 10.0);
78        let line = beard_strand_to_csv_line(&s);
79        assert!(line.contains("1.0000"));
80    }
81
82    #[test]
83    fn test_strands_to_csv_header() {
84        /* CSV has header */
85        let csv = beard_strands_to_csv(&[]);
86        assert!(csv.contains("length_mm"));
87    }
88
89    #[test]
90    fn test_mean_length() {
91        /* mean */
92        let strands = vec![
93            new_beard_strand([0.0; 3], 8.0),
94            new_beard_strand([0.0; 3], 12.0),
95        ];
96        assert!((beard_mean_length(&strands) - 10.0).abs() < 1e-6);
97    }
98
99    #[test]
100    fn test_density() {
101        /* 10 over 5 cm2 = 2 */
102        let strands: Vec<BeardStrand> = (0..10).map(|_| new_beard_strand([0.0; 3], 5.0)).collect();
103        assert!((beard_coverage_density(&strands, 5.0) - 2.0).abs() < 1e-6);
104    }
105
106    #[test]
107    fn test_count() {
108        /* count */
109        let strands: Vec<BeardStrand> = (0..7).map(|_| new_beard_strand([0.0; 3], 5.0)).collect();
110        assert_eq!(beard_count(&strands), 7);
111    }
112}