use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HalfEdge {
pub vertex: usize,
pub twin: usize,
pub next: usize,
pub face: usize,
}
#[derive(Debug, Clone)]
pub struct HalfEdgeMesh {
pub half_edges: Vec<HalfEdge>,
pub vertex_edge: Vec<usize>,
pub face_edge: Vec<usize>,
}
impl HalfEdgeMesh {
#[tracing::instrument(skip(triangles), level = "debug")]
pub fn from_triangles(
num_vertices: usize,
triangles: &[[usize; 3]],
) -> Result<Self, crate::HisabError> {
if triangles.is_empty() {
return Err(crate::HisabError::InvalidInput(
"half-edge mesh requires at least one triangle".into(),
));
}
let num_faces = triangles.len();
let num_he = num_faces * 3;
let mut half_edges: Vec<HalfEdge> = Vec::with_capacity(num_he);
let mut vertex_edge: Vec<usize> = vec![usize::MAX; num_vertices];
let mut face_edge: Vec<usize> = Vec::with_capacity(num_faces);
for tri in triangles {
for &v in tri {
if v >= num_vertices {
return Err(crate::HisabError::InvalidInput(format!(
"vertex index {v} out of range (num_vertices={num_vertices})"
)));
}
}
}
for (f, tri) in triangles.iter().enumerate() {
let base = f * 3;
face_edge.push(base);
for k in 0..3 {
let dst = tri[(k + 1) % 3];
let next_he = base + (k + 1) % 3;
half_edges.push(HalfEdge {
vertex: dst,
twin: usize::MAX, next: next_he,
face: f,
});
let src = tri[k];
if vertex_edge[src] == usize::MAX {
vertex_edge[src] = base + k;
}
}
}
let mut edge_map: HashMap<(usize, usize), usize> = HashMap::with_capacity(num_he);
for (f, tri) in triangles.iter().enumerate() {
for k in 0..3 {
let src = tri[k];
let dst = tri[(k + 1) % 3];
edge_map.insert((src, dst), f * 3 + k);
}
}
for (f, tri) in triangles.iter().enumerate() {
for k in 0..3 {
let src = tri[k];
let dst = tri[(k + 1) % 3];
let he_idx = f * 3 + k;
if let Some(&twin_idx) = edge_map.get(&(dst, src)) {
half_edges[he_idx].twin = twin_idx;
}
}
}
Ok(Self {
half_edges,
vertex_edge,
face_edge,
})
}
#[must_use]
pub fn adjacent_faces(&self, face: usize) -> Vec<usize> {
let base = self.face_edge[face];
let mut result = Vec::with_capacity(3);
for k in 0..3 {
let he = &self.half_edges[base + k];
if he.twin != usize::MAX {
result.push(self.half_edges[he.twin].face);
}
}
result
}
#[must_use]
pub fn vertex_faces(&self, vertex: usize) -> Vec<usize> {
let start = self.vertex_edge[vertex];
if start == usize::MAX {
return Vec::new();
}
let mut result = Vec::new();
let mut he_idx = start;
loop {
let he = &self.half_edges[he_idx];
result.push(he.face);
let he_next = &self.half_edges[he.next];
let he_next2 = &self.half_edges[he_next.next];
let incoming = he_next2; if incoming.twin == usize::MAX {
break; }
he_idx = incoming.twin;
if he_idx == start {
break; }
}
result
}
#[must_use]
pub fn is_boundary_vertex(&self, vertex: usize) -> bool {
let start = self.vertex_edge[vertex];
if start == usize::MAX {
return false;
}
let mut he_idx = start;
loop {
let he = &self.half_edges[he_idx];
if he.twin == usize::MAX {
return true;
}
let he_next = &self.half_edges[he.next];
let he_next2 = &self.half_edges[he_next.next];
let incoming_twin = he_next2.twin;
if incoming_twin == usize::MAX {
return true;
}
he_idx = incoming_twin;
if he_idx == start {
break;
}
}
false
}
#[must_use]
pub fn boundary_edges(&self) -> Vec<usize> {
self.half_edges
.iter()
.enumerate()
.filter_map(|(i, he)| if he.twin == usize::MAX { Some(i) } else { None })
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_tri_mesh() -> HalfEdgeMesh {
HalfEdgeMesh::from_triangles(4, &[[0, 1, 2], [1, 3, 2]]).unwrap()
}
fn single_tri_mesh() -> HalfEdgeMesh {
HalfEdgeMesh::from_triangles(3, &[[0, 1, 2]]).unwrap()
}
#[test]
fn from_triangles_half_edge_count() {
let mesh = two_tri_mesh();
assert_eq!(mesh.half_edges.len(), 6);
assert_eq!(mesh.face_edge.len(), 2);
assert_eq!(mesh.vertex_edge.len(), 4);
}
#[test]
fn twin_assignment_shared_edge() {
let mesh = two_tri_mesh();
assert_eq!(mesh.half_edges[1].twin, 5, "HE1 twin should be HE5");
assert_eq!(mesh.half_edges[5].twin, 1, "HE5 twin should be HE1");
}
#[test]
fn boundary_edges_single_triangle() {
let mesh = single_tri_mesh();
let boundary = mesh.boundary_edges();
assert_eq!(boundary.len(), 3);
}
#[test]
fn adjacent_faces_two_triangles() {
let mesh = two_tri_mesh();
let adj0 = mesh.adjacent_faces(0);
let adj1 = mesh.adjacent_faces(1);
assert!(adj0.contains(&1), "face 0 should be adjacent to face 1");
assert!(adj1.contains(&0), "face 1 should be adjacent to face 0");
assert_eq!(adj0.len(), 1);
assert_eq!(adj1.len(), 1);
}
#[test]
fn is_boundary_vertex_two_triangles() {
let mesh = two_tri_mesh();
assert!(mesh.is_boundary_vertex(0));
assert!(mesh.is_boundary_vertex(3));
}
#[test]
fn vertex_faces_single_triangle() {
let mesh = single_tri_mesh();
for v in 0..3 {
let faces = mesh.vertex_faces(v);
assert_eq!(faces.len(), 1, "vertex {v} should be in 1 face");
assert_eq!(faces[0], 0);
}
}
#[test]
fn invalid_vertex_index_errors() {
let err = HalfEdgeMesh::from_triangles(3, &[[0, 1, 5]]);
assert!(
err.is_err(),
"out-of-range vertex index should return error"
);
}
#[test]
fn empty_triangles_errors() {
let err = HalfEdgeMesh::from_triangles(3, &[]);
assert!(err.is_err(), "empty triangle list should return error");
}
#[test]
fn tetrahedron_no_boundary() {
let tris = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
let mesh = HalfEdgeMesh::from_triangles(4, &tris).unwrap();
let boundary = mesh.boundary_edges();
assert!(
boundary.is_empty(),
"closed tetrahedron should have no boundary edges, got {boundary:?}"
);
}
}