pub mod smooth;
use ahash::AHashMap;
use axiolid_core::{Point3, Scalar, Tolerance};
use axiolid_mesh::{AttributeFate, TriMesh};
use axiolid_surface::Surface;
#[derive(Debug, thiserror::Error, PartialEq)]
#[non_exhaustive]
pub enum RefineError {
#[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("edge length target {0} is not a positive finite length")]
InvalidTarget(Scalar),
#[error("surface-aware refinement refused: {0}")]
SurfaceRefused(String),
#[error("refinement would produce {produced} triangles, over the {limit} budget")]
BudgetExceeded {
produced: usize,
limit: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum RefineTarget {
Uniform {
levels: u32,
},
EdgeLength {
max_edge: Scalar,
},
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RefineReport {
pub input_triangles: usize,
pub output_triangles: usize,
pub vertices_added: usize,
pub surface_aware: bool,
pub max_deviation: Scalar,
pub attribute_fates: Vec<(String, AttributeFate)>,
}
impl RefineReport {
pub fn is_noop(&self) -> bool {
self.vertices_added == 0
}
}
const MAX_TRIANGLES: usize = 20_000_000;
fn validate(mesh: &TriMesh) -> Result<(), RefineError> {
if mesh.indices.len() % 3 != 0 {
return Err(RefineError::RaggedIndices(mesh.indices.len()));
}
let vertex_count = mesh.positions.len();
for (triangle, chunk) in mesh.indices.chunks_exact(3).enumerate() {
for &index in chunk {
if index as usize >= vertex_count {
return Err(RefineError::IndexOutOfRange(triangle, index));
}
}
}
Ok(())
}
pub fn refine(
mesh: &TriMesh,
target: RefineTarget,
surface: Option<&Surface>,
tolerance: Tolerance,
) -> Result<(TriMesh, RefineReport), RefineError> {
validate(mesh)?;
let levels = match target {
RefineTarget::Uniform { levels } => levels,
RefineTarget::EdgeLength { max_edge } => {
if !max_edge.is_finite() || max_edge <= 0.0 {
return Err(RefineError::InvalidTarget(max_edge));
}
passes_for_edge_length(mesh, max_edge)
}
};
let input_triangles = mesh.triangle_count();
let projected = input_triangles
.checked_mul(4usize.saturating_pow(levels))
.unwrap_or(usize::MAX);
if projected > MAX_TRIANGLES {
return Err(RefineError::BudgetExceeded {
produced: projected,
limit: MAX_TRIANGLES,
});
}
let mut positions = mesh.positions.clone();
let mut indices = mesh.indices.clone();
let mut max_deviation: Scalar = 0.0;
for _ in 0..levels {
let mut midpoints: AHashMap<(u32, u32), u32> = AHashMap::new();
let mut next = Vec::with_capacity(indices.len() * 4);
for chunk in indices.chunks_exact(3) {
let [a, b, c] = [chunk[0], chunk[1], chunk[2]];
let ab = split_edge(
a,
b,
&mut positions,
&mut midpoints,
surface,
tolerance,
&mut max_deviation,
)?;
let bc = split_edge(
b,
c,
&mut positions,
&mut midpoints,
surface,
tolerance,
&mut max_deviation,
)?;
let ca = split_edge(
c,
a,
&mut positions,
&mut midpoints,
surface,
tolerance,
&mut max_deviation,
)?;
next.extend_from_slice(&[a, ab, ca]);
next.extend_from_slice(&[ab, b, bc]);
next.extend_from_slice(&[ca, bc, c]);
next.extend_from_slice(&[ab, bc, ca]);
}
indices = next;
}
let vertices_added = positions.len() - mesh.positions.len();
let mut out = TriMesh::new(positions, indices);
out.normals = None;
if vertices_added == 0 {
out.normals = mesh.normals.clone();
out.attributes = mesh.attributes.clone();
}
let attribute_fates = mesh
.attributes
.iter()
.map(|channel| {
let fate = match channel.blend {
_ if vertices_added == 0 => AttributeFate::Preserved,
axiolid_mesh::Blend::None => {
AttributeFate::Dropped(axiolid_mesh::DropReason::NotBlendable)
}
_ => AttributeFate::Dropped(axiolid_mesh::DropReason::ProviderLimitation),
};
(channel.name.clone(), fate)
})
.collect();
let report = RefineReport {
input_triangles,
output_triangles: out.triangle_count(),
vertices_added,
surface_aware: surface.is_some(),
max_deviation,
attribute_fates,
};
Ok((out, report))
}
fn passes_for_edge_length(mesh: &TriMesh, max_edge: Scalar) -> u32 {
let mut longest: Scalar = 0.0;
for chunk in mesh.indices.chunks_exact(3) {
for (from, to) in [(0, 1), (1, 2), (2, 0)] {
let a = mesh.positions[chunk[from] as usize];
let b = mesh.positions[chunk[to] as usize];
longest = longest.max((b - a).length());
}
}
if longest <= max_edge || !longest.is_finite() {
return 0;
}
(longest / max_edge).log2().ceil().max(0.0) as u32
}
fn split_edge(
a: u32,
b: u32,
positions: &mut Vec<Point3>,
midpoints: &mut AHashMap<(u32, u32), u32>,
surface: Option<&Surface>,
tolerance: Tolerance,
max_deviation: &mut Scalar,
) -> Result<u32, RefineError> {
let key = if a < b { (a, b) } else { (b, a) };
if let Some(&existing) = midpoints.get(&key) {
return Ok(existing);
}
let linear = positions[a as usize].midpoint(positions[b as usize]);
let placed = match surface {
Some(surface) => {
let (u, v) = axiolid_evaluate::surface::project(surface, linear, tolerance)
.map_err(|error| RefineError::SurfaceRefused(error.to_string()))?;
let on_surface = axiolid_evaluate::surface::evaluate(surface, u, v)
.map_err(|error| RefineError::SurfaceRefused(error.to_string()))?;
if !on_surface.is_finite() {
return Err(RefineError::SurfaceRefused(
"projected midpoint is not finite".into(),
));
}
on_surface
}
None => linear,
};
*max_deviation = max_deviation.max((placed - linear).length());
let index = positions.len() as u32;
positions.push(placed);
midpoints.insert(key, index);
Ok(index)
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct SmoothReport {
pub vertices_moved: usize,
pub boundary_vertices: usize,
pub max_movement: Scalar,
pub attribute_fates: Vec<(String, AttributeFate)>,
}
fn carry_attributes(mesh: &TriMesh) -> Vec<(String, AttributeFate)> {
mesh.attributes
.iter()
.map(|channel| {
(
channel.name.clone(),
AttributeFate::Dropped(axiolid_mesh::DropReason::ProviderLimitation),
)
})
.collect()
}