Skip to main content

oxihuman_export/
edge_normal_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export per-edge normal data.
6
7/// A per-edge normal entry.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy)]
10pub struct EdgeNormalEntry {
11    pub edge_v0: u32,
12    pub edge_v1: u32,
13    pub normal: [f32; 3],
14}
15
16/// Per-edge normal export.
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct EdgeNormalExport {
20    pub entries: Vec<EdgeNormalEntry>,
21}
22
23/// Create a new edge normal export.
24#[allow(dead_code)]
25pub fn new_edge_normal_export() -> EdgeNormalExport {
26    EdgeNormalExport {
27        entries: Vec::new(),
28    }
29}
30
31/// Add a per-edge normal.
32#[allow(dead_code)]
33pub fn add_edge_normal(export: &mut EdgeNormalExport, entry: EdgeNormalEntry) {
34    export.entries.push(entry);
35}
36
37/// Count entries.
38#[allow(dead_code)]
39pub fn edge_normal_count(export: &EdgeNormalExport) -> usize {
40    export.entries.len()
41}
42
43/// Normalize a 3-D vector.
44#[allow(dead_code)]
45pub fn normalize_en(v: [f32; 3]) -> [f32; 3] {
46    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
47    if len < 1e-12 {
48        return [0.0, 1.0, 0.0];
49    }
50    [v[0] / len, v[1] / len, v[2] / len]
51}
52
53/// Validate that all normals are unit length.
54#[allow(dead_code)]
55pub fn normals_unit_en(export: &EdgeNormalExport) -> bool {
56    export.entries.iter().all(|e| {
57        let len2 =
58            e.normal[0] * e.normal[0] + e.normal[1] * e.normal[1] + e.normal[2] * e.normal[2];
59        (len2 - 1.0).abs() < 1e-4
60    })
61}
62
63/// Compute the average normal vector.
64#[allow(dead_code)]
65pub fn avg_edge_normal(export: &EdgeNormalExport) -> [f32; 3] {
66    let n = export.entries.len();
67    if n == 0 {
68        return [0.0, 1.0, 0.0];
69    }
70    let mut sum = [0.0f32; 3];
71    for e in &export.entries {
72        sum[0] += e.normal[0];
73        sum[1] += e.normal[1];
74        sum[2] += e.normal[2];
75    }
76    normalize_en([sum[0] / n as f32, sum[1] / n as f32, sum[2] / n as f32])
77}
78
79/// Find entry for a given edge (order-insensitive).
80#[allow(dead_code)]
81pub fn find_edge_normal(export: &EdgeNormalExport, v0: u32, v1: u32) -> Option<[f32; 3]> {
82    export
83        .entries
84        .iter()
85        .find(|e| (e.edge_v0 == v0 && e.edge_v1 == v1) || (e.edge_v0 == v1 && e.edge_v1 == v0))
86        .map(|e| e.normal)
87}
88
89/// Compute edge normals from mesh (average of adjacent face normals).
90#[allow(dead_code)]
91pub fn compute_from_mesh(positions: &[[f32; 3]], indices: &[u32]) -> EdgeNormalExport {
92    let tri_count = indices.len() / 3;
93    let mut edge_accum: std::collections::HashMap<(u32, u32), ([f32; 3], u32)> =
94        std::collections::HashMap::new();
95    for t in 0..tri_count {
96        let i0 = indices[t * 3];
97        let i1 = indices[t * 3 + 1];
98        let i2 = indices[t * 3 + 2];
99        if i0 as usize >= positions.len()
100            || i1 as usize >= positions.len()
101            || i2 as usize >= positions.len()
102        {
103            continue;
104        }
105        let p0 = positions[i0 as usize];
106        let p1 = positions[i1 as usize];
107        let p2 = positions[i2 as usize];
108        let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
109        let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
110        let n = normalize_en([
111            e1[1] * e2[2] - e1[2] * e2[1],
112            e1[2] * e2[0] - e1[0] * e2[2],
113            e1[0] * e2[1] - e1[1] * e2[0],
114        ]);
115        for (a, b) in [(i0, i1), (i1, i2), (i2, i0)] {
116            let key = if a < b { (a, b) } else { (b, a) };
117            let acc = edge_accum.entry(key).or_insert(([0.0; 3], 0));
118            acc.0[0] += n[0];
119            acc.0[1] += n[1];
120            acc.0[2] += n[2];
121            acc.1 += 1;
122        }
123    }
124    let mut entries = Vec::new();
125    for ((v0, v1), (sum, cnt)) in edge_accum {
126        let avg = normalize_en([
127            sum[0] / cnt as f32,
128            sum[1] / cnt as f32,
129            sum[2] / cnt as f32,
130        ]);
131        entries.push(EdgeNormalEntry {
132            edge_v0: v0,
133            edge_v1: v1,
134            normal: avg,
135        });
136    }
137    EdgeNormalExport { entries }
138}
139
140/// Serialize to JSON.
141#[allow(dead_code)]
142pub fn edge_normal_to_json(export: &EdgeNormalExport) -> String {
143    format!("{{\"entry_count\":{}}}", export.entries.len())
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn tri_export() -> EdgeNormalExport {
151        compute_from_mesh(
152            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
153            &[0, 1, 2],
154        )
155    }
156
157    #[test]
158    fn test_compute_from_mesh_edge_count() {
159        let e = tri_export();
160        assert_eq!(edge_normal_count(&e), 3);
161    }
162
163    #[test]
164    fn test_normals_unit() {
165        let e = tri_export();
166        assert!(normals_unit_en(&e));
167    }
168
169    #[test]
170    fn test_add_and_count() {
171        let mut e = new_edge_normal_export();
172        add_edge_normal(
173            &mut e,
174            EdgeNormalEntry {
175                edge_v0: 0,
176                edge_v1: 1,
177                normal: [0.0, 1.0, 0.0],
178            },
179        );
180        assert_eq!(edge_normal_count(&e), 1);
181    }
182
183    #[test]
184    fn test_find_edge_normal_found() {
185        let mut e = new_edge_normal_export();
186        add_edge_normal(
187            &mut e,
188            EdgeNormalEntry {
189                edge_v0: 0,
190                edge_v1: 1,
191                normal: [0.0, 1.0, 0.0],
192            },
193        );
194        assert!(find_edge_normal(&e, 0, 1).is_some());
195    }
196
197    #[test]
198    fn test_find_edge_normal_reversed() {
199        let mut e = new_edge_normal_export();
200        add_edge_normal(
201            &mut e,
202            EdgeNormalEntry {
203                edge_v0: 0,
204                edge_v1: 1,
205                normal: [0.0, 1.0, 0.0],
206            },
207        );
208        assert!(find_edge_normal(&e, 1, 0).is_some());
209    }
210
211    #[test]
212    fn test_avg_normal_single() {
213        let mut e = new_edge_normal_export();
214        add_edge_normal(
215            &mut e,
216            EdgeNormalEntry {
217                edge_v0: 0,
218                edge_v1: 1,
219                normal: [0.0, 1.0, 0.0],
220            },
221        );
222        let avg = avg_edge_normal(&e);
223        assert!((avg[1] - 1.0).abs() < 1e-5);
224    }
225
226    #[test]
227    fn test_normalize_en_unit_vector() {
228        let n = normalize_en([3.0, 0.0, 0.0]);
229        assert!((n[0] - 1.0).abs() < 1e-5);
230    }
231
232    #[test]
233    fn test_edge_normal_to_json() {
234        let e = tri_export();
235        let j = edge_normal_to_json(&e);
236        assert!(j.contains("entry_count"));
237    }
238
239    #[test]
240    fn test_empty_mesh_zero_edges() {
241        let e = compute_from_mesh(&[], &[]);
242        assert_eq!(edge_normal_count(&e), 0);
243    }
244}