#![allow(dead_code)]
use std::collections::HashMap;
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct SharpFeatureEdge {
pub v0: u32,
pub v1: u32,
pub dihedral_deg: f32,
}
#[allow(dead_code)]
pub struct SharpFeatureResult {
pub sharp_edges: Vec<SharpFeatureEdge>,
pub sharp_vertices: Vec<u32>,
pub threshold_deg: f32,
}
fn face_normal(p0: [f32; 3], p1: [f32; 3], p2: [f32; 3]) -> [f32; 3] {
let ab = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
let ac = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
let n = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len < 1e-9 {
[0.0, 0.0, 1.0]
} else {
[n[0] / len, n[1] / len, n[2] / len]
}
}
fn dihedral_angle_between(n0: [f32; 3], n1: [f32; 3]) -> f32 {
let d = (n0[0] * n1[0] + n0[1] * n1[1] + n0[2] * n1[2]).clamp(-1.0, 1.0);
d.acos().to_degrees()
}
#[allow(dead_code)]
pub fn detect_sharp_features(
positions: &[[f32; 3]],
indices: &[u32],
threshold_deg: f32,
) -> SharpFeatureResult {
let n_tri = indices.len() / 3;
let mut face_normals: Vec<[f32; 3]> = Vec::with_capacity(n_tri);
for t in 0..n_tri {
let p0 = positions[indices[t * 3] as usize];
let p1 = positions[indices[t * 3 + 1] as usize];
let p2 = positions[indices[t * 3 + 2] as usize];
face_normals.push(face_normal(p0, p1, p2));
}
#[allow(clippy::type_complexity)]
let mut edge_faces: HashMap<(u32, u32), Vec<(usize, [f32; 3])>> = HashMap::new();
for t in 0..n_tri {
let verts = [indices[t * 3], indices[t * 3 + 1], indices[t * 3 + 2]];
for e in 0..3 {
let a = verts[e];
let b = verts[(e + 1) % 3];
let key = if a < b { (a, b) } else { (b, a) };
edge_faces
.entry(key)
.or_default()
.push((t, face_normals[t]));
}
}
let mut sharp_edges = Vec::new();
let mut sharp_vert_set = std::collections::HashSet::new();
for ((v0, v1), faces) in &edge_faces {
if faces.len() < 2 {
continue;
}
let n0 = faces[0].1;
let n1 = faces[1].1;
let angle = dihedral_angle_between(n0, n1);
if angle > threshold_deg {
sharp_edges.push(SharpFeatureEdge {
v0: *v0,
v1: *v1,
dihedral_deg: angle,
});
sharp_vert_set.insert(*v0);
sharp_vert_set.insert(*v1);
}
}
let mut sharp_vertices: Vec<u32> = sharp_vert_set.into_iter().collect();
sharp_vertices.sort();
SharpFeatureResult {
sharp_edges,
sharp_vertices,
threshold_deg,
}
}
#[allow(dead_code)]
pub fn sharp_feature_edge_count(result: &SharpFeatureResult) -> usize {
result.sharp_edges.len()
}
#[allow(dead_code)]
pub fn sharp_feature_vertex_count(result: &SharpFeatureResult) -> usize {
result.sharp_vertices.len()
}
#[allow(dead_code)]
pub fn max_sharp_dihedral(result: &SharpFeatureResult) -> f32 {
result
.sharp_edges
.iter()
.map(|e| e.dihedral_deg)
.fold(f32::NEG_INFINITY, f32::max)
}
#[allow(dead_code)]
pub fn is_sharp_vertex(vertex_idx: u32, result: &SharpFeatureResult) -> bool {
result.sharp_vertices.binary_search(&vertex_idx).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_mesh() -> (Vec<[f32; 3]>, Vec<u32>) {
let positions = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
[1.0, 0.0, 1.0],
[1.0, 1.0, 1.0],
[0.0, 1.0, 1.0],
];
let indices: Vec<u32> = vec![
0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 5, 1, 0, 4, 5, 2, 6, 3, 3, 6, 7, 1, 6, 2, 1, 5,
6, 0, 3, 7, 0, 7, 4,
];
(positions, indices)
}
#[test]
fn cube_sharp_at_90_deg() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 45.0);
assert!(!result.sharp_edges.is_empty());
}
#[test]
fn no_sharp_at_high_threshold() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 150.0);
assert_eq!(result.sharp_edges.len(), 0);
}
#[test]
fn sharp_feature_edge_count_consistent() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 45.0);
assert_eq!(sharp_feature_edge_count(&result), result.sharp_edges.len());
}
#[test]
fn sharp_feature_vertex_count_consistent() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 45.0);
assert_eq!(
sharp_feature_vertex_count(&result),
result.sharp_vertices.len()
);
}
#[test]
fn max_sharp_dihedral_above_threshold() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 45.0);
if !result.sharp_edges.is_empty() {
assert!(max_sharp_dihedral(&result) > 45.0);
}
}
#[test]
fn is_sharp_vertex_finds_tagged() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 45.0);
if let Some(v) = result.sharp_vertices.first() {
assert!(is_sharp_vertex(*v, &result));
}
}
#[test]
fn face_normal_unit() {
let n = face_normal([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
assert!((len - 1.0).abs() < 1e-5);
}
#[test]
fn dihedral_angle_parallel_faces_zero() {
let n = [0.0, 0.0, 1.0];
let angle = dihedral_angle_between(n, n);
assert!(angle < 1e-4);
}
#[test]
fn dihedral_angle_perpendicular_90() {
let n0 = [0.0, 0.0, 1.0];
let n1 = [1.0, 0.0, 0.0];
let angle = dihedral_angle_between(n0, n1);
assert!((angle - 90.0).abs() < 1e-3);
}
#[test]
fn threshold_stored_in_result() {
let (pos, idx) = cube_mesh();
let result = detect_sharp_features(&pos, &idx, 60.0);
assert!((result.threshold_deg - 60.0).abs() < 1e-5);
}
}