Skip to main content

oxihuman_export/
face_normal_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Per-face normal export for triangle meshes.
6
7/// Per-face normal export.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct FaceNormalExport {
11    pub normals: Vec<[f32; 3]>,
12}
13
14/// Compute normalized face normal for a triangle.
15#[allow(dead_code)]
16pub fn compute_face_normal_fn(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
17    let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
18    let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
19    let n = [
20        ab[1] * ac[2] - ab[2] * ac[1],
21        ab[2] * ac[0] - ab[0] * ac[2],
22        ab[0] * ac[1] - ab[1] * ac[0],
23    ];
24    let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
25    if len < 1e-12 {
26        [0.0, 0.0, 1.0]
27    } else {
28        [n[0] / len, n[1] / len, n[2] / len]
29    }
30}
31
32/// Build per-face normal export from a triangle mesh.
33#[allow(dead_code)]
34pub fn export_face_normals(positions: &[[f32; 3]], indices: &[u32]) -> FaceNormalExport {
35    let face_count = indices.len() / 3;
36    let mut normals = Vec::with_capacity(face_count);
37    for f in 0..face_count {
38        let i0 = indices[f * 3] as usize;
39        let i1 = indices[f * 3 + 1] as usize;
40        let i2 = indices[f * 3 + 2] as usize;
41        normals.push(compute_face_normal_fn(
42            positions[i0],
43            positions[i1],
44            positions[i2],
45        ));
46    }
47    FaceNormalExport { normals }
48}
49
50/// Face count.
51#[allow(dead_code)]
52pub fn fn_face_count(e: &FaceNormalExport) -> usize {
53    e.normals.len()
54}
55
56/// Get normal at face index.
57#[allow(dead_code)]
58pub fn get_face_normal(e: &FaceNormalExport, idx: usize) -> Option<[f32; 3]> {
59    e.normals.get(idx).copied()
60}
61
62/// Average normal (component-wise).
63#[allow(dead_code)]
64pub fn avg_face_normal(e: &FaceNormalExport) -> [f32; 3] {
65    if e.normals.is_empty() {
66        return [0.0; 3];
67    }
68    let n = e.normals.len() as f32;
69    let sum = e.normals.iter().fold([0.0f32; 3], |acc, &n| {
70        [acc[0] + n[0], acc[1] + n[1], acc[2] + n[2]]
71    });
72    [sum[0] / n, sum[1] / n, sum[2] / n]
73}
74
75/// Validate: all normals are unit length within tolerance.
76#[allow(dead_code)]
77pub fn validate_face_normals(e: &FaceNormalExport, tol: f32) -> bool {
78    e.normals.iter().all(|&n| {
79        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
80        (len - 1.0).abs() < tol
81    })
82}
83
84/// Export to CSV string.
85#[allow(dead_code)]
86pub fn face_normals_to_csv(e: &FaceNormalExport) -> String {
87    let mut s = "face,nx,ny,nz\n".to_string();
88    for (i, &n) in e.normals.iter().enumerate() {
89        s.push_str(&format!("{},{:.6},{:.6},{:.6}\n", i, n[0], n[1], n[2]));
90    }
91    s
92}
93
94/// Export to JSON.
95#[allow(dead_code)]
96pub fn face_normal_to_json(e: &FaceNormalExport) -> String {
97    format!("{{\"face_count\":{}}}", fn_face_count(e))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn unit_tri_up() -> ([f32; 3], [f32; 3], [f32; 3]) {
105        ([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
106    }
107
108    #[test]
109    fn test_compute_face_normal_up() {
110        let (a, b, c) = unit_tri_up();
111        let n = compute_face_normal_fn(a, b, c);
112        assert!((n[2] - 1.0).abs() < 1e-5);
113    }
114
115    #[test]
116    fn test_compute_face_normal_degenerate() {
117        let n = compute_face_normal_fn([0.0; 3], [0.0; 3], [0.0; 3]);
118        // falls back to [0,0,1]
119        assert!((n[2] - 1.0).abs() < 1e-5);
120    }
121
122    #[test]
123    fn test_export_face_normals_empty() {
124        let e = export_face_normals(&[], &[]);
125        assert_eq!(fn_face_count(&e), 0);
126    }
127
128    #[test]
129    fn test_export_face_normals_single() {
130        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
131        let idx = vec![0u32, 1, 2];
132        let e = export_face_normals(&pos, &idx);
133        assert_eq!(fn_face_count(&e), 1);
134    }
135
136    #[test]
137    fn test_get_face_normal() {
138        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
139        let idx = vec![0u32, 1, 2];
140        let e = export_face_normals(&pos, &idx);
141        assert!(get_face_normal(&e, 0).is_some());
142        assert!(get_face_normal(&e, 99).is_none());
143    }
144
145    #[test]
146    fn test_validate_face_normals() {
147        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
148        let idx = vec![0u32, 1, 2];
149        let e = export_face_normals(&pos, &idx);
150        assert!(validate_face_normals(&e, 1e-4));
151    }
152
153    #[test]
154    fn test_avg_face_normal_single() {
155        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
156        let idx = vec![0u32, 1, 2];
157        let e = export_face_normals(&pos, &idx);
158        let avg = avg_face_normal(&e);
159        assert!((avg[2] - 1.0).abs() < 1e-5);
160    }
161
162    #[test]
163    fn test_face_normals_to_csv() {
164        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
165        let idx = vec![0u32, 1, 2];
166        let e = export_face_normals(&pos, &idx);
167        let csv = face_normals_to_csv(&e);
168        assert!(csv.contains("face,nx,ny,nz"));
169    }
170
171    #[test]
172    fn test_face_normal_to_json() {
173        let e = export_face_normals(&[], &[]);
174        let j = face_normal_to_json(&e);
175        assert!(j.contains("\"face_count\":0"));
176    }
177
178    #[test]
179    fn test_normals_unit_length() {
180        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
181        let idx = vec![0u32, 1, 2];
182        let e = export_face_normals(&pos, &idx);
183        let n = e.normals[0];
184        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
185        assert!((len - 1.0).abs() < 1e-5);
186    }
187}