projective_grid/topological/
classify.rs1use nalgebra::Point2;
24
25use super::axis::AxisCache;
26use super::delaunay::Triangulation;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub(super) enum EdgeClass {
31 Grid,
33 Diagonal,
36 Spurious,
38}
39
40#[derive(Clone, Copy, Debug)]
42struct GridAxisMatch {
43 slot: usize,
44 distance_rad: f32,
45}
46
47#[derive(Clone, Copy, Debug)]
49struct GridEdgeMatch {
50 start_slot: usize,
51 end_slot: usize,
52}
53
54#[inline]
59fn axis_diff(theta: f32, alpha: f32) -> f32 {
60 let pi = std::f32::consts::PI;
61 let half_pi = pi / 2.0;
62 let mut d = (theta - alpha) % pi;
63 if d < 0.0 {
64 d += pi;
65 }
66 if d > half_pi {
67 d = pi - d;
68 }
69 d
70}
71
72fn nearest_axis_at_corner(theta: f32, cache: &AxisCache) -> Option<GridAxisMatch> {
74 let mut best: Option<GridAxisMatch> = None;
75 for slot in 0..2 {
76 if !cache.informative[slot] {
77 continue;
78 }
79 let angle = cache.angle_rad[slot];
80 let d = axis_diff(theta, angle);
81 if !d.is_finite() {
82 continue;
83 }
84 match best {
85 None => {
86 best = Some(GridAxisMatch {
87 slot,
88 distance_rad: d,
89 });
90 }
91 Some(m) if d < m.distance_rad => {
92 best = Some(GridAxisMatch {
93 slot,
94 distance_rad: d,
95 });
96 }
97 _ => {}
98 }
99 }
100 best
101}
102
103fn grid_match_at_corner(
104 theta: f32,
105 cache: &AxisCache,
106 align_tol_rad: f32,
107) -> Option<GridAxisMatch> {
108 let best = nearest_axis_at_corner(theta, cache)?;
109 (best.distance_rad < align_tol_rad).then_some(best)
110}
111
112fn edge_vertices(triangulation: &Triangulation, edge: usize) -> (usize, usize) {
113 (
114 triangulation.triangles[edge],
115 triangulation.triangles[Triangulation::next_edge(edge)],
116 )
117}
118
119fn grid_axis_slot_at_vertex(
120 triangulation: &Triangulation,
121 grid_matches: &[Option<GridEdgeMatch>],
122 edge: usize,
123 vertex: usize,
124) -> Option<usize> {
125 let grid = grid_matches[edge]?;
126 let (start, end) = edge_vertices(triangulation, edge);
127 if vertex == start {
128 Some(grid.start_slot)
129 } else if vertex == end {
130 Some(grid.end_slot)
131 } else {
132 None
133 }
134}
135
136fn shared_vertex_of_edges(
137 triangulation: &Triangulation,
138 edge_a: usize,
139 edge_b: usize,
140) -> Option<usize> {
141 let (a0, a1) = edge_vertices(triangulation, edge_a);
142 let (b0, b1) = edge_vertices(triangulation, edge_b);
143 if a0 == b0 || a0 == b1 {
144 Some(a0)
145 } else if a1 == b0 || a1 == b1 {
146 Some(a1)
147 } else {
148 None
149 }
150}
151
152fn infer_triangle_diagonal(
153 triangulation: &Triangulation,
154 grid_matches: &[Option<GridEdgeMatch>],
155 kinds: &[EdgeClass],
156 triangle: usize,
157) -> Option<usize> {
158 let base = 3 * triangle;
159 let mut grid_edges = [usize::MAX; 2];
160 let mut grid_count = 0usize;
161 let mut non_grid_edge: Option<usize> = None;
162
163 for k in 0..3 {
164 let edge = base + k;
165 match kinds[edge] {
166 EdgeClass::Grid => {
167 if grid_count >= grid_edges.len() {
168 return None;
169 }
170 grid_edges[grid_count] = edge;
171 grid_count += 1;
172 }
173 EdgeClass::Spurious => {
174 if non_grid_edge.is_some() {
175 return None;
176 }
177 non_grid_edge = Some(k);
178 }
179 EdgeClass::Diagonal => return None,
180 }
181 }
182 if grid_count != 2 {
183 return None;
184 }
185
186 let shared = shared_vertex_of_edges(triangulation, grid_edges[0], grid_edges[1])?;
187 let slot0 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[0], shared)?;
188 let slot1 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[1], shared)?;
189 (slot0 != slot1).then_some(non_grid_edge?)
190}
191
192fn promote_triangle_diagonals_from_grid_edges(
193 triangulation: &Triangulation,
194 grid_matches: &[Option<GridEdgeMatch>],
195 kinds: &mut [EdgeClass],
196) {
197 for triangle in 0..triangulation.num_tri() {
198 if let Some(k) = infer_triangle_diagonal(triangulation, grid_matches, kinds, triangle) {
199 kinds[3 * triangle + k] = EdgeClass::Diagonal;
200 }
201 }
202}
203
204#[cfg_attr(
211 feature = "tracing",
212 tracing::instrument(
213 level = "debug",
214 skip_all,
215 fields(num_edges = triangulation.triangles.len()),
216 )
217)]
218pub(super) fn classify_all_edges(
219 positions: &[Point2<f32>],
220 axes_cache: &[AxisCache],
221 triangulation: &Triangulation,
222 align_tol_rad: f32,
223) -> Vec<EdgeClass> {
224 let n = triangulation.triangles.len();
225 let mut kinds = vec![EdgeClass::Spurious; n];
226 let mut grid_matches = vec![None; n];
227 for (e, kind) in kinds.iter_mut().enumerate().take(n) {
228 let a = triangulation.triangles[e];
229 let b = triangulation.triangles[Triangulation::next_edge(e)];
230 let pa = positions[a];
231 let pb = positions[b];
232 let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
233 let at_a = grid_match_at_corner(theta, &axes_cache[a], align_tol_rad);
234 let at_b = grid_match_at_corner(theta, &axes_cache[b], align_tol_rad);
235 if let (Some(a_match), Some(b_match)) = (at_a, at_b) {
236 grid_matches[e] = Some(GridEdgeMatch {
237 start_slot: a_match.slot,
238 end_slot: b_match.slot,
239 });
240 *kind = EdgeClass::Grid;
241 }
242 }
243 promote_triangle_diagonals_from_grid_edges(triangulation, &grid_matches, &mut kinds);
244 kinds
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 fn cache(angle0: f32, angle1: f32) -> AxisCache {
252 AxisCache {
253 angle_rad: [angle0, angle1],
254 informative: [true, true],
255 }
256 }
257
258 #[test]
259 fn axis_diff_is_symmetric_modulo_pi() {
260 let pi = std::f32::consts::PI;
261 let frac_pi_4 = pi / 4.0;
262 let eps = 1e-5_f32;
263 assert!(axis_diff(0.0, pi).abs() < eps);
264 let one_tenth = 0.1_f32;
265 assert!((axis_diff(one_tenth, 0.0) - one_tenth).abs() < eps);
266 assert!((axis_diff(pi - one_tenth, 0.0) - one_tenth).abs() < eps);
267 assert!((axis_diff(frac_pi_4, 0.0) - frac_pi_4).abs() < eps);
268 }
269
270 #[test]
271 fn axis_aligned_edge_is_grid() {
272 let frac_pi_2 = std::f32::consts::FRAC_PI_2;
273 let tol = 15.0_f32.to_radians();
274 let cache = cache(0.0, frac_pi_2);
275 let horizontal = grid_match_at_corner(0.0, &cache, tol).unwrap();
276 assert_eq!(horizontal.slot, 0);
277 assert!(horizontal.distance_rad.abs() < 1e-5);
278 let vertical = grid_match_at_corner(frac_pi_2, &cache, tol).unwrap();
279 assert_eq!(vertical.slot, 1);
280 assert!(vertical.distance_rad.abs() < 1e-5);
281 }
282
283 #[test]
284 fn diagonal_angle_is_not_grid() {
285 let frac_pi_2 = std::f32::consts::FRAC_PI_2;
286 let frac_pi_4 = std::f32::consts::FRAC_PI_4;
287 let tol = 15.0_f32.to_radians();
288 let cache = cache(0.0, frac_pi_2);
289 assert!(grid_match_at_corner(frac_pi_4, &cache, tol).is_none());
290 }
291
292 #[test]
293 fn unaligned_edge_is_spurious() {
294 let frac_pi_2 = std::f32::consts::FRAC_PI_2;
295 let tol = 15.0_f32.to_radians();
296 let cache = cache(0.0, frac_pi_2);
297 let twenty_two = 22.0_f32.to_radians();
298 assert!(grid_match_at_corner(twenty_two, &cache, tol).is_none());
299 }
300}