use nalgebra::Point2;
use super::axis::AxisCache;
use super::delaunay::Triangulation;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum EdgeClass {
Grid,
Diagonal,
Spurious,
}
#[derive(Clone, Copy, Debug)]
struct GridAxisMatch {
slot: usize,
distance_rad: f32,
}
#[derive(Clone, Copy, Debug)]
struct GridEdgeMatch {
start_slot: usize,
end_slot: usize,
}
#[inline]
fn axis_diff(theta: f32, alpha: f32) -> f32 {
let pi = std::f32::consts::PI;
let half_pi = pi / 2.0;
let mut d = (theta - alpha) % pi;
if d < 0.0 {
d += pi;
}
if d > half_pi {
d = pi - d;
}
d
}
fn nearest_axis_at_corner(theta: f32, cache: &AxisCache) -> Option<GridAxisMatch> {
let mut best: Option<GridAxisMatch> = None;
for slot in 0..2 {
if !cache.informative[slot] {
continue;
}
let angle = cache.angle_rad[slot];
let d = axis_diff(theta, angle);
if !d.is_finite() {
continue;
}
match best {
None => {
best = Some(GridAxisMatch {
slot,
distance_rad: d,
});
}
Some(m) if d < m.distance_rad => {
best = Some(GridAxisMatch {
slot,
distance_rad: d,
});
}
_ => {}
}
}
best
}
fn grid_match_at_corner(
theta: f32,
cache: &AxisCache,
align_tol_rad: f32,
) -> Option<GridAxisMatch> {
let best = nearest_axis_at_corner(theta, cache)?;
(best.distance_rad < align_tol_rad).then_some(best)
}
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: &[EdgeClass],
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] {
EdgeClass::Grid => {
if grid_count >= grid_edges.len() {
return None;
}
grid_edges[grid_count] = edge;
grid_count += 1;
}
EdgeClass::Spurious => {
if non_grid_edge.is_some() {
return None;
}
non_grid_edge = Some(k);
}
EdgeClass::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 [EdgeClass],
) {
for triangle in 0..triangulation.num_tri() {
if let Some(k) = infer_triangle_diagonal(triangulation, grid_matches, kinds, triangle) {
kinds[3 * triangle + k] = EdgeClass::Diagonal;
}
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
skip_all,
fields(num_edges = triangulation.triangles.len()),
)
)]
pub(super) fn classify_all_edges(
positions: &[Point2<f32>],
axes_cache: &[AxisCache],
triangulation: &Triangulation,
align_tol_rad: f32,
) -> Vec<EdgeClass> {
let n = triangulation.triangles.len();
let mut kinds = vec![EdgeClass::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_cache[a], align_tol_rad);
let at_b = grid_match_at_corner(theta, &axes_cache[b], align_tol_rad);
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 = EdgeClass::Grid;
}
}
promote_triangle_diagonals_from_grid_edges(triangulation, &grid_matches, &mut kinds);
kinds
}
#[cfg(test)]
mod tests {
use super::*;
fn cache(angle0: f32, angle1: f32) -> AxisCache {
AxisCache {
angle_rad: [angle0, angle1],
informative: [true, true],
}
}
#[test]
fn axis_diff_is_symmetric_modulo_pi() {
let pi = std::f32::consts::PI;
let frac_pi_4 = pi / 4.0;
let eps = 1e-5_f32;
assert!(axis_diff(0.0, pi).abs() < eps);
let one_tenth = 0.1_f32;
assert!((axis_diff(one_tenth, 0.0) - one_tenth).abs() < eps);
assert!((axis_diff(pi - one_tenth, 0.0) - one_tenth).abs() < eps);
assert!((axis_diff(frac_pi_4, 0.0) - frac_pi_4).abs() < eps);
}
#[test]
fn axis_aligned_edge_is_grid() {
let frac_pi_2 = std::f32::consts::FRAC_PI_2;
let tol = 15.0_f32.to_radians();
let cache = cache(0.0, frac_pi_2);
let horizontal = grid_match_at_corner(0.0, &cache, tol).unwrap();
assert_eq!(horizontal.slot, 0);
assert!(horizontal.distance_rad.abs() < 1e-5);
let vertical = grid_match_at_corner(frac_pi_2, &cache, tol).unwrap();
assert_eq!(vertical.slot, 1);
assert!(vertical.distance_rad.abs() < 1e-5);
}
#[test]
fn diagonal_angle_is_not_grid() {
let frac_pi_2 = std::f32::consts::FRAC_PI_2;
let frac_pi_4 = std::f32::consts::FRAC_PI_4;
let tol = 15.0_f32.to_radians();
let cache = cache(0.0, frac_pi_2);
assert!(grid_match_at_corner(frac_pi_4, &cache, tol).is_none());
}
#[test]
fn unaligned_edge_is_spurious() {
let frac_pi_2 = std::f32::consts::FRAC_PI_2;
let tol = 15.0_f32.to_radians();
let cache = cache(0.0, frac_pi_2);
let twenty_two = 22.0_f32.to_radians();
assert!(grid_match_at_corner(twenty_two, &cache, tol).is_none());
}
}