projective_grid/topological/
classify.rs1use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI};
22
23use nalgebra::Point2;
24
25use super::delaunay::Triangulation;
26use super::{AxisHint, TopologicalParams};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum EdgeKind {
31 Grid,
33 Diagonal,
35 Spurious,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41enum EdgeAt {
42 Grid,
43 Diagonal,
44 Spurious,
45}
46
47#[inline]
52fn axis_diff(theta: f32, alpha: f32) -> f32 {
53 let mut d = (theta - alpha).rem_euclid(PI);
54 if d > FRAC_PI_2 {
55 d = PI - d;
56 }
57 d
58}
59
60fn classify_at_corner(theta: f32, axes: &[AxisHint; 2], params: &TopologicalParams) -> EdgeAt {
61 let mut min_d = f32::INFINITY;
65 for a in axes.iter() {
66 if a.sigma >= params.max_axis_sigma_rad {
67 continue;
68 }
69 let d = axis_diff(theta, a.angle);
70 if d < min_d {
71 min_d = d;
72 }
73 }
74 if !min_d.is_finite() {
75 return EdgeAt::Spurious;
76 }
77 if min_d < params.axis_align_tol_rad {
78 return EdgeAt::Grid;
79 }
80 let dia = (min_d - FRAC_PI_4).abs();
81 if dia < params.diagonal_angle_tol_rad {
82 return EdgeAt::Diagonal;
83 }
84 EdgeAt::Spurious
85}
86
87#[cfg_attr(
91 feature = "tracing",
92 tracing::instrument(
93 level = "debug",
94 skip_all,
95 fields(num_edges = triangulation.triangles.len()),
96 )
97)]
98pub(crate) fn classify_all_edges(
99 positions: &[Point2<f32>],
100 axes: &[[AxisHint; 2]],
101 usable: &[bool],
102 triangulation: &Triangulation,
103 params: &TopologicalParams,
104) -> Vec<EdgeKind> {
105 let n = triangulation.triangles.len();
106 let mut kinds = vec![EdgeKind::Spurious; n];
107 for (e, kind) in kinds.iter_mut().enumerate().take(n) {
108 let a = triangulation.triangles[e];
109 let b = triangulation.triangles[Triangulation::next_edge(e)];
110 if !usable[a] || !usable[b] {
111 continue;
113 }
114 let pa = positions[a];
115 let pb = positions[b];
116 let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
117 let at_a = classify_at_corner(theta, &axes[a], params);
118 let at_b = classify_at_corner(theta, &axes[b], params);
119 *kind = match (at_a, at_b) {
120 (EdgeAt::Grid, EdgeAt::Grid) => EdgeKind::Grid,
121 (EdgeAt::Diagonal, EdgeAt::Diagonal) => EdgeKind::Diagonal,
122 _ => EdgeKind::Spurious,
123 };
124 }
125 kinds
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn axes(angle0: f32, angle1: f32) -> [AxisHint; 2] {
133 [
134 AxisHint {
135 angle: angle0,
136 sigma: 0.05,
137 },
138 AxisHint {
139 angle: angle1,
140 sigma: 0.05,
141 },
142 ]
143 }
144
145 #[test]
146 fn axis_diff_is_symmetric_modulo_pi() {
147 assert!((axis_diff(0.0, PI) - 0.0).abs() < 1e-6);
148 assert!((axis_diff(0.1, 0.0) - 0.1).abs() < 1e-6);
149 assert!((axis_diff(PI - 0.1, 0.0) - 0.1).abs() < 1e-6);
150 assert!((axis_diff(FRAC_PI_4, 0.0) - FRAC_PI_4).abs() < 1e-6);
151 }
152
153 #[test]
154 fn axis_aligned_edge_is_grid() {
155 let p = TopologicalParams::default();
156 let a = axes(0.0, FRAC_PI_2);
157 assert_eq!(classify_at_corner(0.0, &a, &p), EdgeAt::Grid);
159 assert_eq!(classify_at_corner(FRAC_PI_2, &a, &p), EdgeAt::Grid);
161 }
162
163 #[test]
164 fn diagonal_edge_is_diagonal() {
165 let p = TopologicalParams::default();
166 let a = axes(0.0, FRAC_PI_2);
167 assert_eq!(classify_at_corner(FRAC_PI_4, &a, &p), EdgeAt::Diagonal);
168 assert_eq!(classify_at_corner(-FRAC_PI_4, &a, &p), EdgeAt::Diagonal);
169 }
170
171 #[test]
172 fn unaligned_edge_is_spurious() {
173 let p = TopologicalParams::default();
174 let a = axes(0.0, FRAC_PI_2);
175 assert_eq!(
178 classify_at_corner(22.0_f32.to_radians(), &a, &p),
179 EdgeAt::Spurious
180 );
181 }
182}