use std::collections::HashMap;
use nalgebra::Point2;
use serde::{Deserialize, Serialize};
mod classify;
mod delaunay;
mod quads;
mod topo_filter;
mod walk;
#[cfg(test)]
mod tests;
pub use classify::EdgeKind;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct AxisHint {
pub angle: f32,
pub sigma: f32,
}
impl Default for AxisHint {
fn default() -> Self {
Self {
angle: 0.0,
sigma: std::f32::consts::PI,
}
}
}
impl AxisHint {
pub fn from_angle(angle: f32) -> Self {
Self { angle, sigma: 0.0 }
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TopologicalParams {
pub axis_align_tol_rad: f32,
pub diagonal_angle_tol_rad: f32,
pub max_axis_sigma_rad: f32,
pub edge_ratio_max: f32,
pub min_quads_per_component: usize,
}
impl Default for TopologicalParams {
fn default() -> Self {
Self {
axis_align_tol_rad: 0.262, diagonal_angle_tol_rad: 0.262,
max_axis_sigma_rad: 0.6,
edge_ratio_max: 10.0,
min_quads_per_component: 1,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TopologicalComponent {
pub labelled: HashMap<(i32, i32), usize>,
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct TopologicalStats {
pub corners_in: usize,
pub corners_used: usize,
pub triangles: usize,
pub grid_edges: usize,
pub diagonal_edges: usize,
pub spurious_edges: usize,
pub triangles_mergeable: usize,
pub triangles_all_grid: usize,
pub triangles_multi_diag: usize,
pub triangles_has_spurious: usize,
pub quads_merged: usize,
pub quads_kept: usize,
pub components: usize,
}
#[derive(Clone, Debug, Default)]
pub struct TopologicalGrid {
pub components: Vec<TopologicalComponent>,
pub diagnostics: TopologicalStats,
}
#[derive(Clone, Copy, Debug, thiserror::Error)]
pub enum TopologicalError {
#[error("positions and axes must be the same length (got {positions} and {axes})")]
LengthMismatch { positions: usize, axes: usize },
#[error("not enough usable corners ({usable}) for Delaunay triangulation")]
NotEnoughCorners { usable: usize },
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "info",
skip_all,
fields(num_corners = positions.len()),
)
)]
pub fn build_grid_topological(
positions: &[Point2<f32>],
axes: &[[AxisHint; 2]],
params: &TopologicalParams,
) -> Result<TopologicalGrid, TopologicalError> {
if positions.len() != axes.len() {
return Err(TopologicalError::LengthMismatch {
positions: positions.len(),
axes: axes.len(),
});
}
let mut stats = TopologicalStats {
corners_in: positions.len(),
..Default::default()
};
let usable_mask: Vec<bool> = axes
.iter()
.map(|a| a[0].sigma < params.max_axis_sigma_rad || a[1].sigma < params.max_axis_sigma_rad)
.collect();
stats.corners_used = usable_mask.iter().filter(|&&b| b).count();
if stats.corners_used < 3 {
return Err(TopologicalError::NotEnoughCorners {
usable: stats.corners_used,
});
}
let triangulation = delaunay::triangulate(positions);
stats.triangles = triangulation.triangles.len() / 3;
let edge_kinds =
classify::classify_all_edges(positions, axes, &usable_mask, &triangulation, params);
for &k in &edge_kinds {
match k {
EdgeKind::Grid => stats.grid_edges += 1,
EdgeKind::Diagonal => stats.diagonal_edges += 1,
EdgeKind::Spurious => stats.spurious_edges += 1,
}
}
for t in 0..stats.triangles {
let mut g = 0;
let mut d = 0;
let mut sp = 0;
for k in 0..3 {
match edge_kinds[3 * t + k] {
EdgeKind::Grid => g += 1,
EdgeKind::Diagonal => d += 1,
EdgeKind::Spurious => sp += 1,
}
}
if sp > 0 {
stats.triangles_has_spurious += 1;
} else if d == 1 && g == 2 {
stats.triangles_mergeable += 1;
} else if d == 0 && g == 3 {
stats.triangles_all_grid += 1;
} else if d >= 2 {
stats.triangles_multi_diag += 1;
}
}
let raw_quads = quads::merge_triangle_pairs(&triangulation, &edge_kinds, positions);
stats.quads_merged = raw_quads.len();
let kept_quads = topo_filter::filter_quads(&raw_quads, positions, params);
stats.quads_kept = kept_quads.len();
let components = walk::label_components(&kept_quads, params.min_quads_per_component);
stats.components = components.len();
Ok(TopologicalGrid {
components,
diagnostics: stats,
})
}