projective_grid/topological/
classify.rs1use std::f32::consts::{FRAC_PI_2, PI};
23
24use nalgebra::Point2;
25use serde::{Deserialize, Serialize};
26
27use super::delaunay::Triangulation;
28use super::{AxisEstimate, TopologicalParams};
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum EdgeKind {
35 Grid,
37 Diagonal,
39 Spurious,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
45struct GridAxisMatch {
46 slot: usize,
47 distance_rad: f32,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51struct GridEdgeMatch {
52 start_slot: usize,
53 end_slot: usize,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub(crate) struct EdgeMetric {
58 pub(crate) grid_distance_rad: Option<f32>,
59 pub(crate) grid_margin_rad: Option<f32>,
60}
61
62#[inline]
67fn axis_diff(theta: f32, alpha: f32) -> f32 {
68 let mut d = (theta - alpha).rem_euclid(PI);
69 if d > FRAC_PI_2 {
70 d = PI - d;
71 }
72 d
73}
74
75fn nearest_axis_at_corner(
83 theta: f32,
84 axes: &[AxisEstimate; 2],
85 params: &TopologicalParams,
86) -> Option<GridAxisMatch> {
87 let mut best: Option<GridAxisMatch> = None;
88 for (slot, a) in axes.iter().enumerate() {
89 if !a.sigma.is_finite() || a.sigma >= params.max_axis_sigma_rad {
90 continue;
91 }
92 let d = axis_diff(theta, a.angle);
93 if !d.is_finite() {
94 continue;
95 }
96 if best.is_none_or(|m| d < m.distance_rad) {
97 best = Some(GridAxisMatch {
98 slot,
99 distance_rad: d,
100 });
101 }
102 }
103 best
104}
105
106fn grid_distance_at_corner(
108 theta: f32,
109 axes: &[AxisEstimate; 2],
110 params: &TopologicalParams,
111) -> f32 {
112 let best = nearest_axis_at_corner(theta, axes, params);
113 debug_assert!(
114 best.is_some(),
115 "topological pre-filter must guarantee at least one usable axis per endpoint"
116 );
117 best.map_or(f32::INFINITY, |m| m.distance_rad)
118}
119
120fn grid_match_at_corner(
121 theta: f32,
122 axes: &[AxisEstimate; 2],
123 params: &TopologicalParams,
124) -> Option<GridAxisMatch> {
125 let best = nearest_axis_at_corner(theta, axes, params)?;
126 (best.distance_rad < params.axis_align_tol_rad).then_some(best)
127}
128
129pub(crate) fn classify_edge_metric(
130 positions: &[Point2<f32>],
131 axes: &[[AxisEstimate; 2]],
132 triangulation: &Triangulation,
133 edge: usize,
134 params: &TopologicalParams,
135) -> EdgeMetric {
136 let a = triangulation.triangles[edge];
137 let b = triangulation.triangles[Triangulation::next_edge(edge)];
138 let pa = positions[a];
139 let pb = positions[b];
140 let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
141 let a_grid = grid_distance_at_corner(theta, &axes[a], params);
142 let b_grid = grid_distance_at_corner(theta, &axes[b], params);
143 let grid_distance_rad = a_grid.max(b_grid);
144 EdgeMetric {
145 grid_distance_rad: Some(grid_distance_rad),
146 grid_margin_rad: Some(params.axis_align_tol_rad - grid_distance_rad),
147 }
148}
149
150fn edge_vertices(triangulation: &Triangulation, edge: usize) -> (usize, usize) {
151 (
152 triangulation.triangles[edge],
153 triangulation.triangles[Triangulation::next_edge(edge)],
154 )
155}
156
157fn grid_axis_slot_at_vertex(
158 triangulation: &Triangulation,
159 grid_matches: &[Option<GridEdgeMatch>],
160 edge: usize,
161 vertex: usize,
162) -> Option<usize> {
163 let grid = grid_matches[edge]?;
164 let (start, end) = edge_vertices(triangulation, edge);
165 if vertex == start {
166 Some(grid.start_slot)
167 } else if vertex == end {
168 Some(grid.end_slot)
169 } else {
170 None
171 }
172}
173
174fn shared_vertex_of_edges(
175 triangulation: &Triangulation,
176 edge_a: usize,
177 edge_b: usize,
178) -> Option<usize> {
179 let (a0, a1) = edge_vertices(triangulation, edge_a);
180 let (b0, b1) = edge_vertices(triangulation, edge_b);
181 if a0 == b0 || a0 == b1 {
182 Some(a0)
183 } else if a1 == b0 || a1 == b1 {
184 Some(a1)
185 } else {
186 None
187 }
188}
189
190fn infer_triangle_diagonal(
191 triangulation: &Triangulation,
192 grid_matches: &[Option<GridEdgeMatch>],
193 kinds: &[EdgeKind],
194 triangle: usize,
195) -> Option<usize> {
196 let base = 3 * triangle;
197 let mut grid_edges = [usize::MAX; 2];
198 let mut grid_count = 0usize;
199 let mut non_grid_edge: Option<usize> = None;
200
201 for k in 0..3 {
202 let edge = base + k;
203 match kinds[edge] {
204 EdgeKind::Grid => {
205 if grid_count >= grid_edges.len() {
206 return None;
207 }
208 grid_edges[grid_count] = edge;
209 grid_count += 1;
210 }
211 EdgeKind::Spurious => {
212 if non_grid_edge.is_some() {
213 return None;
214 }
215 non_grid_edge = Some(k);
216 }
217 EdgeKind::Diagonal => return None,
218 }
219 }
220 if grid_count != 2 {
221 return None;
222 }
223
224 let shared = shared_vertex_of_edges(triangulation, grid_edges[0], grid_edges[1])?;
225 let slot0 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[0], shared)?;
226 let slot1 = grid_axis_slot_at_vertex(triangulation, grid_matches, grid_edges[1], shared)?;
227 (slot0 != slot1).then_some(non_grid_edge?)
228}
229
230fn promote_triangle_diagonals_from_grid_edges(
231 triangulation: &Triangulation,
232 grid_matches: &[Option<GridEdgeMatch>],
233 kinds: &mut [EdgeKind],
234) {
235 for triangle in 0..triangulation.num_tri() {
236 if let Some(k) = infer_triangle_diagonal(triangulation, grid_matches, kinds, triangle) {
237 kinds[3 * triangle + k] = EdgeKind::Diagonal;
238 }
239 }
240}
241
242#[cfg_attr(
246 feature = "tracing",
247 tracing::instrument(
248 level = "debug",
249 skip_all,
250 fields(num_edges = triangulation.triangles.len()),
251 )
252)]
253pub(crate) fn classify_all_edges(
254 positions: &[Point2<f32>],
255 axes: &[[AxisEstimate; 2]],
256 triangulation: &Triangulation,
257 params: &TopologicalParams,
258) -> Vec<EdgeKind> {
259 let n = triangulation.triangles.len();
260 let mut kinds = vec![EdgeKind::Spurious; n];
261 let mut grid_matches = vec![None; n];
262 for (e, kind) in kinds.iter_mut().enumerate().take(n) {
263 let a = triangulation.triangles[e];
264 let b = triangulation.triangles[Triangulation::next_edge(e)];
265 let pa = positions[a];
266 let pb = positions[b];
267 let theta = (pb.y - pa.y).atan2(pb.x - pa.x);
268 let at_a = grid_match_at_corner(theta, &axes[a], params);
269 let at_b = grid_match_at_corner(theta, &axes[b], params);
270 if let (Some(a_match), Some(b_match)) = (at_a, at_b) {
271 grid_matches[e] = Some(GridEdgeMatch {
272 start_slot: a_match.slot,
273 end_slot: b_match.slot,
274 });
275 *kind = EdgeKind::Grid;
276 }
277 }
278 promote_triangle_diagonals_from_grid_edges(triangulation, &grid_matches, &mut kinds);
279 kinds
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::f32::consts::FRAC_PI_4;
286
287 fn axes(angle0: f32, angle1: f32) -> [AxisEstimate; 2] {
288 [
289 AxisEstimate {
290 angle: angle0,
291 sigma: 0.05,
292 },
293 AxisEstimate {
294 angle: angle1,
295 sigma: 0.05,
296 },
297 ]
298 }
299
300 #[test]
301 fn axis_diff_is_symmetric_modulo_pi() {
302 assert!((axis_diff(0.0, PI) - 0.0).abs() < 1e-6);
303 assert!((axis_diff(0.1, 0.0) - 0.1).abs() < 1e-6);
304 assert!((axis_diff(PI - 0.1, 0.0) - 0.1).abs() < 1e-6);
305 assert!((axis_diff(FRAC_PI_4, 0.0) - FRAC_PI_4).abs() < 1e-6);
306 }
307
308 #[test]
309 fn axis_aligned_edge_is_grid() {
310 let p = TopologicalParams::default();
311 let a = axes(0.0, FRAC_PI_2);
312 let horizontal = grid_match_at_corner(0.0, &a, &p).unwrap();
314 assert_eq!(horizontal.slot, 0);
315 assert!(horizontal.distance_rad < 1e-6);
316 let vertical = grid_match_at_corner(FRAC_PI_2, &a, &p).unwrap();
318 assert_eq!(vertical.slot, 1);
319 assert!(vertical.distance_rad < 1e-6);
320 }
321
322 #[test]
323 fn diagonal_angle_edge_is_not_a_grid_match() {
324 let p = TopologicalParams::default();
325 let a = axes(0.0, FRAC_PI_2);
326 assert!(grid_match_at_corner(FRAC_PI_4, &a, &p).is_none());
328 assert!((grid_distance_at_corner(FRAC_PI_4, &a, &p) - FRAC_PI_4).abs() < 1e-6);
329 }
330
331 #[test]
332 fn unaligned_edge_is_spurious() {
333 let p = TopologicalParams::default();
334 let a = axes(0.0, FRAC_PI_2);
335 assert!(grid_match_at_corner(22.0_f32.to_radians(), &a, &p).is_none());
337 }
338}