use std::collections::BTreeMap;
use axiolid_mesh::TriMesh;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum TopologyError {
#[error("triangle {triangle} names vertex {vertex}, but the mesh has {vertices}")]
IndexOutOfRange {
triangle: usize,
vertex: u32,
vertices: usize,
},
#[error("triangle {triangle} repeats a vertex")]
DegenerateTriangle {
triangle: usize,
},
#[error(
"the mesh is not a two-manifold: {edges} edges on three or more \
triangles, {vertices} vertices whose triangles form several fans"
)]
NonManifold {
edges: usize,
vertices: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SurfaceKind {
Orientable {
genus: u32,
},
NonOrientable {
crosscaps: u32,
},
}
pub type EdgeLoop = Vec<u32>;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ComponentTopology {
pub triangles: Vec<usize>,
pub vertices: usize,
pub edges: usize,
pub euler_characteristic: i64,
pub boundary_loops: usize,
pub orientable: bool,
pub consistently_oriented: bool,
pub surface: SurfaceKind,
pub homology_basis: Option<Vec<EdgeLoop>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MeshTopology {
pub components: Vec<ComponentTopology>,
}
#[derive(Debug, Clone, Copy)]
struct Use {
triangle: usize,
forward: bool,
}
pub fn topology(mesh: &TriMesh) -> Result<MeshTopology, TopologyError> {
let triangles: Vec<[u32; 3]> = mesh
.indices
.chunks_exact(3)
.map(|t| [t[0], t[1], t[2]])
.collect();
let vertex_count = mesh.positions.len();
for (triangle, t) in triangles.iter().enumerate() {
if let Some(&vertex) = t.iter().find(|&&v| v as usize >= vertex_count) {
return Err(TopologyError::IndexOutOfRange {
triangle,
vertex,
vertices: vertex_count,
});
}
if t[0] == t[1] || t[1] == t[2] || t[2] == t[0] {
return Err(TopologyError::DegenerateTriangle { triangle });
}
}
let mut edges: BTreeMap<(u32, u32), Vec<Use>> = BTreeMap::new();
for (triangle, t) in triangles.iter().enumerate() {
for k in 0..3 {
let (a, b) = (t[k], t[(k + 1) % 3]);
edges.entry((a.min(b), a.max(b))).or_default().push(Use {
triangle,
forward: a < b,
});
}
}
let non_manifold_edges = edges.values().filter(|uses| uses.len() > 2).count();
let non_manifold_vertices = count_split_vertices(&triangles, &edges, vertex_count);
if non_manifold_edges > 0 || non_manifold_vertices > 0 {
return Err(TopologyError::NonManifold {
edges: non_manifold_edges,
vertices: non_manifold_vertices,
});
}
let mut neighbours: Vec<Vec<(usize, bool)>> = vec![Vec::new(); triangles.len()];
for uses in edges.values() {
if let [a, b] = uses[..] {
let flips = a.forward == b.forward;
neighbours[a.triangle].push((b.triangle, flips));
neighbours[b.triangle].push((a.triangle, flips));
}
}
let mut component_of = vec![usize::MAX; triangles.len()];
let mut flip = vec![false; triangles.len()];
let mut components = Vec::new();
for seed in 0..triangles.len() {
if component_of[seed] != usize::MAX {
continue;
}
let id = components.len();
component_of[seed] = id;
let mut members = vec![seed];
let mut orientable = true;
let mut consistent = true;
let mut next = 0;
while next < members.len() {
let t = members[next];
next += 1;
for &(n, flips) in &neighbours[t] {
consistent &= !flips;
let wanted = flip[t] ^ flips;
if component_of[n] == usize::MAX {
component_of[n] = id;
flip[n] = wanted;
members.push(n);
} else if flip[n] != wanted {
orientable = false;
}
}
}
members.sort_unstable();
components.push((members, orientable, consistent));
}
let mut out = Vec::with_capacity(components.len());
for (members, orientable, consistent) in components {
let component = component_of[members[0]];
let mut used: Vec<u32> = members.iter().flat_map(|&t| triangles[t]).collect();
used.sort_unstable();
used.dedup();
let own_edges: Vec<(&(u32, u32), &Vec<Use>)> = edges
.iter()
.filter(|(_, uses)| component_of[uses[0].triangle] == component)
.collect();
let boundary: Vec<(u32, u32)> = own_edges
.iter()
.filter(|(_, uses)| uses.len() == 1)
.map(|(&edge, _)| edge)
.collect();
let boundary_loops = count_loops(&boundary);
let characteristic = used.len() as i64 - own_edges.len() as i64 + members.len() as i64;
let deficit = 2 - characteristic - boundary_loops as i64;
let surface = if orientable {
SurfaceKind::Orientable {
genus: u32::try_from(deficit / 2).unwrap_or(0),
}
} else {
SurfaceKind::NonOrientable {
crosscaps: u32::try_from(deficit).unwrap_or(0),
}
};
let homology_basis =
(orientable && boundary.is_empty()).then(|| tree_cotree(&members, &own_edges));
out.push(ComponentTopology {
triangles: members,
vertices: used.len(),
edges: own_edges.len(),
euler_characteristic: characteristic,
boundary_loops,
orientable,
consistently_oriented: consistent,
surface,
homology_basis,
});
}
Ok(MeshTopology { components: out })
}
fn count_split_vertices(
triangles: &[[u32; 3]],
edges: &BTreeMap<(u32, u32), Vec<Use>>,
vertex_count: usize,
) -> usize {
let mut incident: Vec<Vec<usize>> = vec![Vec::new(); vertex_count];
for (t, tri) in triangles.iter().enumerate() {
for &v in tri {
incident[v as usize].push(t);
}
}
let mut split = 0;
for (v, around) in incident.iter().enumerate() {
if around.len() < 2 {
continue;
}
let v = v as u32;
let slot = |t: usize| around.iter().position(|&u| u == t);
let mut parent: Vec<usize> = (0..around.len()).collect();
for (i, &t) in around.iter().enumerate() {
for &w in &triangles[t] {
if w == v {
continue;
}
for other in &edges[&(v.min(w), v.max(w))] {
if let Some(j) = slot(other.triangle) {
union(&mut parent, i, j);
}
}
}
}
let fans = (0..around.len())
.filter(|&i| find(&mut parent, i) == i)
.count();
if fans > 1 {
split += 1;
}
}
split
}
fn find(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
fn union(parent: &mut [usize], a: usize, b: usize) {
let (a, b) = (find(parent, a), find(parent, b));
if a != b {
parent[a.max(b)] = a.min(b);
}
}
fn count_loops(boundary: &[(u32, u32)]) -> usize {
let mut index: BTreeMap<u32, usize> = BTreeMap::new();
for &(a, b) in boundary {
let n = index.len();
index.entry(a).or_insert(n);
let n = index.len();
index.entry(b).or_insert(n);
}
let mut parent: Vec<usize> = (0..index.len()).collect();
for &(a, b) in boundary {
union(&mut parent, index[&a], index[&b]);
}
(0..parent.len())
.filter(|&i| find(&mut parent, i) == i)
.count()
}
fn tree_cotree(members: &[usize], own_edges: &[(&(u32, u32), &Vec<Use>)]) -> Vec<EdgeLoop> {
let mut adjacent: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
for (&(a, b), _) in own_edges {
adjacent.entry(a).or_default().push(b);
adjacent.entry(b).or_default().push(a);
}
let root = *adjacent.keys().next().expect("a component has vertices");
let mut parent: BTreeMap<u32, u32> = BTreeMap::new();
let mut depth: BTreeMap<u32, usize> = BTreeMap::new();
parent.insert(root, root);
depth.insert(root, 0);
let mut queue = std::collections::VecDeque::from([root]);
while let Some(v) = queue.pop_front() {
for &w in &adjacent[&v] {
if let std::collections::btree_map::Entry::Vacant(slot) = parent.entry(w) {
slot.insert(v);
depth.insert(w, depth[&v] + 1);
queue.push_back(w);
}
}
}
let in_tree = |a: u32, b: u32| parent[&a] == b || parent[&b] == a;
let slot: BTreeMap<usize, usize> = members.iter().enumerate().map(|(i, &t)| (t, i)).collect();
let mut dual: Vec<usize> = (0..members.len()).collect();
let mut leftover = Vec::new();
for (&(a, b), uses) in own_edges {
if in_tree(a, b) {
continue;
}
let (s, t) = (slot[&uses[0].triangle], slot[&uses[1].triangle]);
if find(&mut dual, s) == find(&mut dual, t) {
leftover.push((a, b));
} else {
union(&mut dual, s, t);
}
}
leftover
.into_iter()
.map(|(a, b)| {
let (mut up_a, mut up_b) = (vec![a], vec![b]);
let (mut x, mut y) = (a, b);
while depth[&x] > depth[&y] {
x = parent[&x];
up_a.push(x);
}
while depth[&y] > depth[&x] {
y = parent[&y];
up_b.push(y);
}
while x != y {
x = parent[&x];
y = parent[&y];
up_a.push(x);
up_b.push(y);
}
up_b.pop();
up_a.extend(up_b.into_iter().rev());
up_a
})
.collect()
}