Skip to main content

oxihuman_viewer/
edge_normal.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Edge-normal smoothing and visualisation helpers.
6
7use std::f32::consts::FRAC_PI_2;
8
9/// A mesh edge (vertex pair).
10#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct MeshEdge {
13    pub v0: u32,
14    pub v1: u32,
15}
16
17/// Per-edge normal data.
18#[allow(dead_code)]
19#[derive(Clone, Debug)]
20pub struct EdgeNormalEntry {
21    pub edge: MeshEdge,
22    pub normal: [f32; 3],
23    /// Dihedral angle in radians between adjacent faces.
24    pub dihedral_rad: f32,
25}
26
27/// Collection of edge normals.
28#[allow(dead_code)]
29#[derive(Clone, Debug, Default)]
30pub struct EdgeNormalMap {
31    pub entries: Vec<EdgeNormalEntry>,
32}
33
34#[allow(dead_code)]
35pub fn new_edge_normal_map() -> EdgeNormalMap {
36    EdgeNormalMap::default()
37}
38
39#[allow(dead_code)]
40pub fn en_add(map: &mut EdgeNormalMap, edge: MeshEdge, normal: [f32; 3], dihedral_rad: f32) {
41    map.entries.push(EdgeNormalEntry {
42        edge,
43        normal,
44        dihedral_rad,
45    });
46}
47
48#[allow(dead_code)]
49pub fn en_count(map: &EdgeNormalMap) -> usize {
50    map.entries.len()
51}
52
53#[allow(dead_code)]
54pub fn en_clear(map: &mut EdgeNormalMap) {
55    map.entries.clear();
56}
57
58/// Normalise a 3-vector.
59#[allow(dead_code)]
60pub fn en_normalize(v: [f32; 3]) -> [f32; 3] {
61    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
62    if l < 1e-9 {
63        return [0.0, 1.0, 0.0];
64    }
65    [v[0] / l, v[1] / l, v[2] / l]
66}
67
68/// Filter edges whose dihedral angle exceeds a crease threshold (default = 60°, using FRAC_PI_2 internally).
69#[allow(dead_code)]
70pub fn en_crease_edges(map: &EdgeNormalMap, threshold_rad: f32) -> Vec<&EdgeNormalEntry> {
71    let _ = FRAC_PI_2; // used for reference
72    map.entries
73        .iter()
74        .filter(|e| e.dihedral_rad >= threshold_rad)
75        .collect()
76}
77
78#[allow(dead_code)]
79pub fn en_average_dihedral(map: &EdgeNormalMap) -> f32 {
80    if map.entries.is_empty() {
81        return 0.0;
82    }
83    map.entries.iter().map(|e| e.dihedral_rad).sum::<f32>() / map.entries.len() as f32
84}
85
86#[allow(dead_code)]
87pub fn en_max_dihedral(map: &EdgeNormalMap) -> f32 {
88    map.entries
89        .iter()
90        .map(|e| e.dihedral_rad)
91        .fold(0.0f32, f32::max)
92}
93
94#[allow(dead_code)]
95pub fn en_to_json(map: &EdgeNormalMap) -> String {
96    format!(
97        "{{\"count\":{},\"avg_dihedral\":{:.4}}}",
98        en_count(map),
99        en_average_dihedral(map)
100    )
101}
102
103#[allow(dead_code)]
104pub fn en_find_by_vertices(map: &EdgeNormalMap, v0: u32, v1: u32) -> Option<&EdgeNormalEntry> {
105    map.entries
106        .iter()
107        .find(|e| (e.edge.v0 == v0 && e.edge.v1 == v1) || (e.edge.v0 == v1 && e.edge.v1 == v0))
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn new_empty() {
116        assert_eq!(en_count(&new_edge_normal_map()), 0);
117    }
118
119    #[test]
120    fn add_entry() {
121        let mut m = new_edge_normal_map();
122        en_add(&mut m, MeshEdge { v0: 0, v1: 1 }, [0.0, 1.0, 0.0], 0.5);
123        assert_eq!(en_count(&m), 1);
124    }
125
126    #[test]
127    fn clear() {
128        let mut m = new_edge_normal_map();
129        en_add(&mut m, MeshEdge { v0: 0, v1: 1 }, [0.0, 1.0, 0.0], 0.5);
130        en_clear(&mut m);
131        assert_eq!(en_count(&m), 0);
132    }
133
134    #[test]
135    fn normalize_unit_vector() {
136        let n = en_normalize([3.0, 0.0, 0.0]);
137        assert!((n[0] - 1.0).abs() < 1e-5);
138    }
139
140    #[test]
141    fn normalize_zero_safe() {
142        let n = en_normalize([0.0, 0.0, 0.0]);
143        assert!((n[1] - 1.0).abs() < 1e-5);
144    }
145
146    #[test]
147    fn crease_filter() {
148        let mut m = new_edge_normal_map();
149        en_add(&mut m, MeshEdge { v0: 0, v1: 1 }, [0.0, 1.0, 0.0], 1.5);
150        en_add(&mut m, MeshEdge { v0: 1, v1: 2 }, [0.0, 1.0, 0.0], 0.2);
151        let c = en_crease_edges(&m, 1.0);
152        assert_eq!(c.len(), 1);
153    }
154
155    #[test]
156    fn average_dihedral_empty() {
157        assert!((en_average_dihedral(&new_edge_normal_map())).abs() < 1e-5);
158    }
159
160    #[test]
161    fn max_dihedral() {
162        let mut m = new_edge_normal_map();
163        en_add(&mut m, MeshEdge { v0: 0, v1: 1 }, [1.0, 0.0, 0.0], 2.5);
164        assert!((en_max_dihedral(&m) - 2.5).abs() < 1e-5);
165    }
166
167    #[test]
168    fn find_by_vertices() {
169        let mut m = new_edge_normal_map();
170        en_add(&mut m, MeshEdge { v0: 3, v1: 7 }, [0.0, 1.0, 0.0], 1.0);
171        assert!(en_find_by_vertices(&m, 3, 7).is_some());
172        assert!(en_find_by_vertices(&m, 7, 3).is_some());
173    }
174
175    #[test]
176    fn json_has_count() {
177        assert!(en_to_json(&new_edge_normal_map()).contains("count"));
178    }
179}