use std::f32::consts::{FRAC_PI_2, PI};
use nalgebra::Point2;
use serde::{Deserialize, Serialize};
use super::delaunay::Triangulation;
use super::{AxisEstimate, TopologicalParams};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EdgeKind {
Grid,
Diagonal,
Spurious,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct GridAxisMatch {
slot: usize,
distance_rad: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct GridEdgeMatch {
start_slot: usize,
end_slot: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct EdgeMetric {
pub(crate) grid_distance_rad: Option<f32>,
pub(crate) grid_margin_rad: Option<f32>,
}
#[inline]
fn axis_diff(theta: f32, alpha: f32) -> f32 {
let mut d = (theta - alpha).rem_euclid(PI);
if d > FRAC_PI_2 {
d = PI - d;
}
d
}
fn nearest_axis_at_corner(
theta: f32,
axes: &[AxisEstimate; 2],
params: &TopologicalParams,
) -> Option<GridAxisMatch> {
let mut best: Option<GridAxisMatch> = None;
for (slot, a) in axes.iter().enumerate() {
if !a.sigma.is_finite() || a.sigma >= params.max_axis_sigma_rad {
continue;
}
let d = axis_diff(theta, a.angle);
if !d.is_finite() {
continue;
}
if best.is_none_or(|m| d < m.distance_rad) {
best = Some(GridAxisMatch {
slot,
distance_rad: d,
});
}
}
best
}
fn grid_distance_at_corner(
theta: f32,
axes: &[AxisEstimate; 2],
params: &TopologicalParams,
) -> f32 {
let best = nearest_axis_at_corner(theta, axes, params);
debug_assert!(
best.is_some(),
"topological pre-filter must guarantee at least one usable axis per endpoint"
);
best.map_or(f32::INFINITY, |m| m.distance_rad)
}
fn grid_match_at_corner(
theta: f32,
axes: &[AxisEstimate; 2],
params: &TopologicalParams,
) -> Option<GridAxisMatch> {
let best = nearest_axis_at_corner(theta, axes, params)?;
(best.distance_rad < params.axis_align_tol_rad).then_some(best)
}
pub(crate) fn classify_edge_metric(
positions: &[Point2<f32>],
axes: &[[AxisEstimate; 2]],
triangulation: &Triangulation,
edge: usize,
params: &TopologicalParams,
) -> EdgeMetric {
let a = triangulation.triangles[edge];
let b = triangulation.triangles[Triangulation::next_edge(edge)];
let pa = positions[a];
let pb = positions[b];
let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
let a_grid = grid_distance_at_corner(theta, &axes[a], params);
let b_grid = grid_distance_at_corner(theta, &axes[b], params);
let grid_distance_rad = a_grid.max(b_grid);
EdgeMetric {
grid_distance_rad: Some(grid_distance_rad),
grid_margin_rad: Some(params.axis_align_tol_rad - grid_distance_rad),
}
}
fn edge_vertices(triangulation: &Triangulation, edge: usize) -> (usize, usize) {
(
triangulation.triangles[edge],
triangulation.triangles[Triangulation::next_edge(edge)],
)
}
fn grid_axis_slot_at_vertex(
triangulation: &Triangulation,
grid_matches: &[Option<GridEdgeMatch>],
edge: usize,
vertex: usize,
) -> Option<usize> {
let grid = grid_matches[edge]?;
let (start, end) = edge_vertices(triangulation, edge);
if vertex == start {
Some(grid.start_slot)
} else if vertex == end {
Some(grid.end_slot)
} else {
None
}
}
fn shared_vertex_of_edges(
triangulation: &Triangulation,
edge_a: usize,
edge_b: usize,
) -> Option<usize> {
let (a0, a1) = edge_vertices(triangulation, edge_a);
let (b0, b1) = edge_vertices(triangulation, edge_b);
if a0 == b0 || a0 == b1 {
Some(a0)
} else if a1 == b0 || a1 == b1 {
Some(a1)
} else {
None
}
}
fn infer_triangle_diagonal(
triangulation: &Triangulation,
grid_matches: &[Option<GridEdgeMatch>],
kinds: &[EdgeKind],
triangle: usize,
) -> Option<usize> {
let base = 3 * triangle;
let mut grid_edges = [usize::MAX; 2];
let mut grid_count = 0usize;
let mut non_grid_edge: Option<usize> = None;
for k in 0..3 {
let edge = base + k;
match kinds[edge] {
EdgeKind::Grid => {
if grid_count >= grid_edges.len() {
return None;
}
grid_edges[grid_count] = edge;
grid_count += 1;
}
EdgeKind::Spurious => {
if non_grid_edge.is_some() {
return None;
}
non_grid_edge = Some(k);
}
EdgeKind::Diagonal => return None,
}
}
if grid_count != 2 {
return None;
}
let shared = shared_vertex_of_edges(triangulation, grid_edges[0], grid_edges[1])?;
let slot0 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[0], shared)?;
let slot1 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[1], shared)?;
(slot0 != slot1).then_some(non_grid_edge?)
}
fn promote_triangle_diagonals_from_grid_edges(
triangulation: &Triangulation,
grid_matches: &[Option<GridEdgeMatch>],
kinds: &mut [EdgeKind],
) {
for triangle in 0..triangulation.num_tri() {
if let Some(k) = infer_triangle_diagonal(triangulation, grid_matches, kinds, triangle) {
kinds[3 * triangle + k] = EdgeKind::Diagonal;
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
skip_all,
fields(num_edges = triangulation.triangles.len()),
)
)]
pub(crate) fn classify_all_edges(
positions: &[Point2<f32>],
axes: &[[AxisEstimate; 2]],
triangulation: &Triangulation,
params: &TopologicalParams,
) -> Vec<EdgeKind> {
let n = triangulation.triangles.len();
let mut kinds = vec![EdgeKind::Spurious; n];
let mut grid_matches = vec![None; n];
for (e, kind) in kinds.iter_mut().enumerate().take(n) {
let a = triangulation.triangles[e];
let b = triangulation.triangles[Triangulation::next_edge(e)];
let pa = positions[a];
let pb = positions[b];
let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
let at_a = grid_match_at_corner(theta, &axes[a], params);
let at_b = grid_match_at_corner(theta, &axes[b], params);
if let (Some(a_match), Some(b_match)) = (at_a, at_b) {
grid_matches[e] = Some(GridEdgeMatch {
start_slot: a_match.slot,
end_slot: b_match.slot,
});
*kind = EdgeKind::Grid;
}
}
promote_triangle_diagonals_from_grid_edges(triangulation, &grid_matches, &mut kinds);
kinds
}
#[cfg(test)]
mod tests {
use super::*;
use std::f32::consts::FRAC_PI_4;
fn axes(angle0: f32, angle1: f32) -> [AxisEstimate; 2] {
[
AxisEstimate {
angle: angle0,
sigma: 0.05,
},
AxisEstimate {
angle: angle1,
sigma: 0.05,
},
]
}
#[test]
fn axis_diff_is_symmetric_modulo_pi() {
assert!((axis_diff(0.0, PI) - 0.0).abs() < 1e-6);
assert!((axis_diff(0.1, 0.0) - 0.1).abs() < 1e-6);
assert!((axis_diff(PI - 0.1, 0.0) - 0.1).abs() < 1e-6);
assert!((axis_diff(FRAC_PI_4, 0.0) - FRAC_PI_4).abs() < 1e-6);
}
#[test]
fn axis_aligned_edge_is_grid() {
let p = TopologicalParams::default();
let a = axes(0.0, FRAC_PI_2);
let horizontal = grid_match_at_corner(0.0, &a, &p).unwrap();
assert_eq!(horizontal.slot, 0);
assert!(horizontal.distance_rad < 1e-6);
let vertical = grid_match_at_corner(FRAC_PI_2, &a, &p).unwrap();
assert_eq!(vertical.slot, 1);
assert!(vertical.distance_rad < 1e-6);
}
#[test]
fn diagonal_angle_edge_is_not_a_grid_match() {
let p = TopologicalParams::default();
let a = axes(0.0, FRAC_PI_2);
assert!(grid_match_at_corner(FRAC_PI_4, &a, &p).is_none());
assert!((grid_distance_at_corner(FRAC_PI_4, &a, &p) - FRAC_PI_4).abs() < 1e-6);
}
#[test]
fn unaligned_edge_is_spurious() {
let p = TopologicalParams::default();
let a = axes(0.0, FRAC_PI_2);
assert!(grid_match_at_corner(22.0_f32.to_radians(), &a, &p).is_none());
}
}