use std::collections::HashMap;
use nalgebra::Point2;
use serde::{Deserialize, Serialize};
mod classify;
mod delaunay;
mod quads;
mod topo_filter;
pub mod trace;
mod walk;
#[cfg(test)]
mod tests;
pub use classify::EdgeKind;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct AxisEstimate {
pub angle: f32,
pub sigma: f32,
}
impl Default for AxisEstimate {
fn default() -> Self {
Self {
angle: 0.0,
sigma: std::f32::consts::PI,
}
}
}
impl AxisEstimate {
pub fn from_angle(angle: f32) -> Self {
Self { angle, sigma: 0.0 }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TopologicalInputCorner {
pub position: Point2<f32>,
pub axes: [AxisEstimate; 2],
}
impl TopologicalInputCorner {
pub fn new(position: Point2<f32>, axes: [AxisEstimate; 2]) -> Self {
Self { position, axes }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct AxisClusterCenters {
pub theta0: f32,
pub theta1: f32,
}
impl AxisClusterCenters {
pub fn new(a: f32, b: f32) -> Self {
let (mut t0, mut t1) = (
crate::circular_stats::wrap_pi(a),
crate::circular_stats::wrap_pi(b),
);
if t0 > t1 {
std::mem::swap(&mut t0, &mut t1);
}
Self {
theta0: t0,
theta1: t1,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TopologicalParams {
pub axis_align_tol_rad: f32,
pub max_axis_sigma_rad: f32,
pub edge_ratio_max: f32,
pub min_quads_per_component: usize,
pub axis_cluster_centers: Option<AxisClusterCenters>,
pub cluster_axis_tol_rad: f32,
pub quad_edge_min_rel: f32,
pub quad_edge_max_rel: f32,
}
impl Default for TopologicalParams {
fn default() -> Self {
Self {
axis_align_tol_rad: 15.0_f32.to_radians(),
max_axis_sigma_rad: 0.6,
edge_ratio_max: 10.0,
min_quads_per_component: 1,
axis_cluster_centers: None,
cluster_axis_tol_rad: 16.0_f32.to_radians(),
quad_edge_min_rel: 0.0,
quad_edge_max_rel: 1.8,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct TopologicalComponent {
pub labelled: HashMap<(i32, i32), usize>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
#[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,
}
impl TopologicalGrid {
pub fn merge_components_local(
&self,
positions: &[Point2<f32>],
params: &crate::component_merge::LocalMergeParams,
) -> crate::component_merge::ComponentMergeResult {
let views: Vec<_> = self
.components
.iter()
.map(|component| crate::component_merge::ComponentInput {
labelled: &component.labelled,
positions,
})
.collect();
crate::component_merge::merge_components_local(&views, params)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TriangleClass {
Mergeable,
AllGrid,
MultiDiagonal,
HasSpurious,
}
#[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,
},
}
#[inline]
fn axis_passes_cluster(a: &AxisEstimate, centers: &AxisClusterCenters, tol: f32) -> bool {
use crate::circular_stats::{angular_dist_pi, wrap_pi};
if !a.sigma.is_finite() || a.sigma >= std::f32::consts::PI - f32::EPSILON {
return false;
}
let angle = wrap_pi(a.angle);
angular_dist_pi(angle, centers.theta0).min(angular_dist_pi(angle, centers.theta1)) < tol
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
skip_all,
fields(num_corners = axes.len()),
)
)]
fn usable_mask(axes: &[[AxisEstimate; 2]], params: &TopologicalParams) -> Vec<bool> {
let centers = params.axis_cluster_centers.as_ref();
let tol = params.cluster_axis_tol_rad;
axes.iter()
.map(|a| {
let sigma_ok =
a[0].sigma < params.max_axis_sigma_rad || a[1].sigma < params.max_axis_sigma_rad;
if !sigma_ok {
return false;
}
match centers {
None => true,
Some(c) => axis_passes_cluster(&a[0], c, tol) || axis_passes_cluster(&a[1], c, tol),
}
})
.collect()
}
fn triangulate_usable(
positions: &[Point2<f32>],
usable: &[bool],
) -> (delaunay::Triangulation, Vec<usize>) {
let mut packed_to_global: Vec<usize> = Vec::with_capacity(positions.len());
let mut packed_positions: Vec<Point2<f32>> = Vec::with_capacity(positions.len());
for (i, (&u, &p)) in usable.iter().zip(positions.iter()).enumerate() {
if u {
packed_to_global.push(i);
packed_positions.push(p);
}
}
let mut triangulation = delaunay::triangulate(&packed_positions);
for v in triangulation.triangles.iter_mut() {
*v = packed_to_global[*v];
}
(triangulation, packed_to_global)
}
pub(super) fn triangle_class(edge_kinds: &[EdgeKind], t: usize) -> TriangleClass {
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 {
TriangleClass::HasSpurious
} else if d == 1 && g == 2 {
TriangleClass::Mergeable
} else if d == 0 && g == 3 {
TriangleClass::AllGrid
} else {
TriangleClass::MultiDiagonal
}
}
pub(super) fn update_edge_stats(stats: &mut TopologicalStats, edge_kinds: &[EdgeKind]) {
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,
}
}
}
pub(super) fn update_triangle_stats(stats: &mut TopologicalStats, edge_kinds: &[EdgeKind]) {
for t in 0..stats.triangles {
match triangle_class(edge_kinds, t) {
TriangleClass::Mergeable => stats.triangles_mergeable += 1,
TriangleClass::AllGrid => stats.triangles_all_grid += 1,
TriangleClass::MultiDiagonal => stats.triangles_multi_diag += 1,
TriangleClass::HasSpurious => stats.triangles_has_spurious += 1,
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "info",
skip_all,
fields(num_corners = positions.len()),
)
)]
pub fn build_grid_topological(
positions: &[Point2<f32>],
axes: &[[AxisEstimate; 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 = usable_mask(axes, params);
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, _packed_to_global) = triangulate_usable(positions, &usable_mask);
stats.triangles = triangulation.triangles.len() / 3;
let edge_kinds = classify::classify_all_edges(positions, axes, &triangulation, params);
update_edge_stats(&mut stats, &edge_kinds);
update_triangle_stats(&mut stats, &edge_kinds);
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,
})
}
pub fn detect_topological_grid(
corners: &[TopologicalInputCorner],
params: &TopologicalParams,
) -> Result<TopologicalGrid, TopologicalError> {
let positions: Vec<Point2<f32>> = corners.iter().map(|c| c.position).collect();
let axes: Vec<[AxisEstimate; 2]> = corners.iter().map(|c| c.axes).collect();
build_grid_topological(&positions, &axes, params)
}
pub fn recover_topological_grid(
positions: &[Point2<f32>],
axes: &[[AxisEstimate; 2]],
topo_params: &TopologicalParams,
merge_params: &crate::component_merge::LocalMergeParams,
) -> Result<crate::component_merge::ComponentMergeResult, TopologicalError> {
let grid = build_grid_topological(positions, axes, topo_params)?;
Ok(grid.merge_components_local(positions, merge_params))
}