use axiolid_core::{Point3, Tolerance};
use axiolid_mesh::TriMesh;
use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum DecimateTarget {
TriangleBudget(usize),
MaxDeviation(f64),
}
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum DecimateError {
#[error("index buffer length {0} is not a multiple of 3")]
RaggedIndices(usize),
#[error("triangle {0} references vertex {1}, which is out of range")]
IndexOutOfRange(usize, u32),
#[error("deviation bound {0} is not a positive finite length")]
InvalidBound(f64),
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct DecimateReport {
pub input_triangles: usize,
pub output_triangles: usize,
pub collapses: usize,
pub rejected_unsafe: usize,
pub rejected_deviation: usize,
pub max_deviation: f64,
}
impl DecimateReport {
pub fn is_noop(&self) -> bool {
self.collapses == 0
}
}
pub fn decimate(
mesh: &TriMesh,
target: DecimateTarget,
tolerance: Tolerance,
) -> Result<(TriMesh, DecimateReport), DecimateError> {
if mesh.indices.len() % 3 != 0 {
return Err(DecimateError::RaggedIndices(mesh.indices.len()));
}
let vertex_count = mesh.positions.len();
for (t, chunk) in mesh.indices.chunks_exact(3).enumerate() {
for &index in chunk {
if index as usize >= vertex_count {
return Err(DecimateError::IndexOutOfRange(t, index));
}
}
}
let bound = match target {
DecimateTarget::MaxDeviation(d) => {
if !d.is_finite() || d <= 0.0 {
return Err(DecimateError::InvalidBound(d));
}
d
}
DecimateTarget::TriangleBudget(_) => tolerance.linear(),
};
let budget = match target {
DecimateTarget::TriangleBudget(n) => n,
DecimateTarget::MaxDeviation(_) => 0,
};
run(mesh, budget, bound)
}
fn run(
mesh: &TriMesh,
budget: usize,
bound: f64,
) -> Result<(TriMesh, DecimateReport), DecimateError> {
let input_triangles = mesh.indices.len() / 3;
let mut positions = mesh.positions.clone();
let mut triangles: Vec<[u32; 3]> = mesh
.indices
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
let mut candidates: Vec<(u32, u32)> = unique_edges(&triangles).into_iter().collect();
candidates.sort_by(|a, b| {
let la = (positions[a.0 as usize] - positions[a.1 as usize]).length();
let lb = (positions[b.0 as usize] - positions[b.1 as usize]).length();
la.partial_cmp(&lb)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(b))
});
let mut report = DecimateReport {
input_triangles,
output_triangles: input_triangles,
collapses: 0,
rejected_unsafe: 0,
rejected_deviation: 0,
max_deviation: 0.0,
};
let mut moved = vec![0.0_f64; positions.len()];
let mut alive: Vec<bool> = vec![true; positions.len()];
for (u, v) in candidates {
if triangles.len() <= budget.max(4) && budget > 0 {
break;
}
if !alive[u as usize] || !alive[v as usize] {
continue;
}
let midpoint = (positions[u as usize] + positions[v as usize]) / 2.0;
let deviation = (midpoint - positions[u as usize])
.length()
.max((midpoint - positions[v as usize]).length())
+ moved[u as usize].max(moved[v as usize]);
if deviation > bound {
report.rejected_deviation += 1;
continue;
}
match try_collapse(&triangles, &positions, u, v, midpoint) {
Some(next) => {
triangles = next;
positions[u as usize] = midpoint;
moved[u as usize] = deviation;
alive[v as usize] = false;
report.collapses += 1;
report.max_deviation = report.max_deviation.max(deviation);
}
None => report.rejected_unsafe += 1,
}
}
report.output_triangles = triangles.len();
Ok((compact(&triangles, &positions), report))
}
fn try_collapse(
triangles: &[[u32; 3]],
positions: &[Point3],
u: u32,
v: u32,
midpoint: Point3,
) -> Option<Vec<[u32; 3]>> {
let nu = neighbours(triangles, u);
let nv = neighbours(triangles, v);
let shared = nu.intersection(&nv).count();
if shared != 2 {
return None;
}
let mut next = Vec::with_capacity(triangles.len());
for &t in triangles {
let touches_u = t.contains(&u);
let touches_v = t.contains(&v);
if touches_u && touches_v {
continue;
}
let mapped = t.map(|c| if c == v { u } else { c });
if touches_u || touches_v {
let before = normal(positions, t);
let after = normal_with(positions, mapped, u, midpoint);
if after.length_squared() == 0.0 || before.dot(after) <= 0.0 {
return None;
}
}
next.push(mapped);
}
Some(next)
}
fn neighbours(triangles: &[[u32; 3]], vertex: u32) -> BTreeSet<u32> {
let mut set = BTreeSet::new();
for t in triangles {
if t.contains(&vertex) {
for &c in t {
if c != vertex {
set.insert(c);
}
}
}
}
set
}
fn unique_edges(triangles: &[[u32; 3]]) -> BTreeSet<(u32, u32)> {
let mut set = BTreeSet::new();
for t in triangles {
for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
set.insert((a.min(b), a.max(b)));
}
}
set
}
fn normal(positions: &[Point3], t: [u32; 3]) -> axiolid_core::Vec3 {
let a = positions[t[0] as usize];
let b = positions[t[1] as usize];
let c = positions[t[2] as usize];
(b - a).cross(c - a)
}
fn normal_with(
positions: &[Point3],
t: [u32; 3],
moved_index: u32,
moved_to: Point3,
) -> axiolid_core::Vec3 {
let at = |i: u32| {
if i == moved_index {
moved_to
} else {
positions[i as usize]
}
};
let (a, b, c) = (at(t[0]), at(t[1]), at(t[2]));
(b - a).cross(c - a)
}
fn compact(triangles: &[[u32; 3]], positions: &[Point3]) -> TriMesh {
let mut remap: BTreeMap<u32, u32> = BTreeMap::new();
let mut kept = Vec::new();
let mut indices = Vec::with_capacity(triangles.len() * 3);
for t in triangles {
for &corner in t {
let next = u32::try_from(kept.len()).unwrap_or(u32::MAX);
let slot = *remap.entry(corner).or_insert_with(|| {
kept.push(positions[corner as usize]);
next
});
indices.push(slot);
}
}
TriMesh::new(kept, indices)
}