1use crate::affine::AffineTransform2D;
9use crate::float_helpers::lit;
10use crate::homography::{estimate_homography, Homography};
11use crate::Float;
12use crate::GridCoords;
13use nalgebra::Point2;
14use std::collections::HashMap;
15
16fn sqrt3_half<F: Float>() -> F {
17 lit::<F>(3.0).sqrt() / lit::<F>(2.0)
18}
19
20#[non_exhaustive]
21#[derive(thiserror::Error, Debug)]
22pub enum HexMeshError {
23 #[error("not enough grid corners (need at least 3)")]
24 NotEnoughCorners,
25 #[error("no valid triangles found")]
26 NoValidTriangles,
27}
28
29#[derive(Clone, Debug)]
30struct TriangleCell<F: Float> {
31 affine: AffineTransform2D<F>,
32 homography: Homography<F>,
33}
34
35#[derive(Clone, Debug)]
42pub struct HexGridHomographyMesh<F: Float = f32> {
43 pub min_q: i32,
44 pub min_r: i32,
45 pub cells_q: usize,
47 pub cells_r: usize,
49 pub px_per_cell: F,
51 pub valid_triangles: usize,
53 pub rect_width: usize,
55 pub rect_height: usize,
56
57 cells: Vec<Option<TriangleCell<F>>>,
58
59 x_offset: F,
60 y_offset: F,
61}
62
63impl<F: Float> HexGridHomographyMesh<F> {
64 pub fn from_corners(
69 corners: &HashMap<GridCoords, Point2<F>>,
70 px_per_cell: F,
71 ) -> Result<Self, HexMeshError> {
72 if corners.len() < 3 {
73 return Err(HexMeshError::NotEnoughCorners);
74 }
75
76 let (mut min_q, mut min_r) = (i32::MAX, i32::MAX);
77 let (mut max_q, mut max_r) = (i32::MIN, i32::MIN);
78 for g in corners.keys() {
79 min_q = min_q.min(g.i);
80 min_r = min_r.min(g.j);
81 max_q = max_q.max(g.i);
82 max_r = max_r.max(g.j);
83 }
84
85 if max_q - min_q < 1 || max_r - min_r < 1 {
86 return Err(HexMeshError::NoValidTriangles);
87 }
88
89 let cells_q = (max_q - min_q) as usize;
90 let cells_r = (max_r - min_r) as usize;
91 let s = px_per_cell;
92 let s3h: F = sqrt3_half();
93 let half: F = lit(0.5);
94
95 let mut x_min = F::max_value().unwrap_or_else(|| lit(1e30));
97 let mut x_max = -x_min;
98 let mut y_min = x_min;
99 let mut y_max = -y_min;
100
101 for &q_i in &[min_q, max_q] {
102 for &r_j in &[min_r, max_r] {
103 let q: F = lit(q_i as f64);
104 let r: F = lit(r_j as f64);
105 let x = s * (q + r * half);
106 let y = s * (r * s3h);
107 x_min = if x < x_min { x } else { x_min };
108 x_max = if x > x_max { x } else { x_max };
109 y_min = if y < y_min { y } else { y_min };
110 y_max = if y > y_max { y } else { y_max };
111 }
112 }
113
114 let rect_width = nalgebra::try_convert::<F, f64>((x_max - x_min).round().max(F::one()))
115 .unwrap_or(1.0) as usize;
116 let rect_height = nalgebra::try_convert::<F, f64>((y_max - y_min).round().max(F::one()))
117 .unwrap_or(1.0) as usize;
118
119 let axial_to_rect = |qi: i32, rj: i32| -> Point2<F> {
120 let q: F = lit(qi as f64);
121 let r: F = lit(rj as f64);
122 Point2::new(s * (q + r * half) - x_min, s * (r * s3h) - y_min)
123 };
124
125 let mut cells = vec![None; cells_q * cells_r * 2];
126 let mut valid_triangles = 0usize;
127
128 for cr in 0..cells_r {
129 for cq in 0..cells_q {
130 let q0 = min_q + cq as i32;
131 let r0 = min_r + cr as i32;
132
133 let g00 = GridCoords { i: q0, j: r0 };
134 let g10 = GridCoords { i: q0 + 1, j: r0 };
135 let g01 = GridCoords { i: q0, j: r0 + 1 };
136 let g11 = GridCoords {
137 i: q0 + 1,
138 j: r0 + 1,
139 };
140
141 let p00 = corners.get(&g00).copied();
142 let p10 = corners.get(&g10).copied();
143 let p01 = corners.get(&g01).copied();
144 let p11 = corners.get(&g11).copied();
145
146 let idx_base = (cr * cells_q + cq) * 2;
147
148 if let (Some(ip00), Some(ip10), Some(ip01)) = (p00, p10, p01) {
150 let rect_tri = [
151 axial_to_rect(q0, r0),
152 axial_to_rect(q0 + 1, r0),
153 axial_to_rect(q0, r0 + 1),
154 ];
155 let img_tri = [ip00, ip10, ip01];
156
157 if let Some(affine) =
158 AffineTransform2D::from_triangle_correspondence(rect_tri, img_tri)
159 {
160 let rect_c = centroid(&rect_tri);
161 let img_c = affine.apply(rect_c);
162 let rect_4: Vec<Point2<F>> = rect_tri
163 .iter()
164 .chain(std::iter::once(&rect_c))
165 .copied()
166 .collect();
167 let img_4: Vec<Point2<F>> = img_tri
168 .iter()
169 .chain(std::iter::once(&img_c))
170 .copied()
171 .collect();
172
173 if let Some(homography) = estimate_homography(&rect_4, &img_4) {
174 cells[idx_base] = Some(TriangleCell { affine, homography });
175 valid_triangles += 1;
176 }
177 }
178 }
179
180 if let (Some(ip10), Some(ip01), Some(ip11)) = (p10, p01, p11) {
182 let rect_tri = [
183 axial_to_rect(q0 + 1, r0),
184 axial_to_rect(q0, r0 + 1),
185 axial_to_rect(q0 + 1, r0 + 1),
186 ];
187 let img_tri = [ip10, ip01, ip11];
188
189 if let Some(affine) =
190 AffineTransform2D::from_triangle_correspondence(rect_tri, img_tri)
191 {
192 let rect_c = centroid(&rect_tri);
193 let img_c = affine.apply(rect_c);
194 let rect_4: Vec<Point2<F>> = rect_tri
195 .iter()
196 .chain(std::iter::once(&rect_c))
197 .copied()
198 .collect();
199 let img_4: Vec<Point2<F>> = img_tri
200 .iter()
201 .chain(std::iter::once(&img_c))
202 .copied()
203 .collect();
204
205 if let Some(homography) = estimate_homography(&rect_4, &img_4) {
206 cells[idx_base + 1] = Some(TriangleCell { affine, homography });
207 valid_triangles += 1;
208 }
209 }
210 }
211 }
212 }
213
214 if valid_triangles == 0 {
215 return Err(HexMeshError::NoValidTriangles);
216 }
217
218 Ok(Self {
219 min_q,
220 min_r,
221 cells_q,
222 cells_r,
223 px_per_cell,
224 valid_triangles,
225 rect_width,
226 rect_height,
227 cells,
228 x_offset: x_min,
229 y_offset: y_min,
230 })
231 }
232
233 pub fn rect_to_img_affine(&self, p_rect: Point2<F>) -> Option<Point2<F>> {
238 let cell = self.lookup_cell(p_rect)?;
239 Some(cell.affine.apply(p_rect))
240 }
241
242 pub fn rect_to_img(&self, p_rect: Point2<F>) -> Option<Point2<F>> {
247 let cell = self.lookup_cell(p_rect)?;
248 Some(cell.homography.apply(p_rect))
249 }
250
251 fn lookup_cell(&self, p_rect: Point2<F>) -> Option<&TriangleCell<F>> {
253 let s = self.px_per_cell;
254 if s <= F::zero() {
255 return None;
256 }
257
258 let s3h: F = sqrt3_half();
259 let half: F = lit(0.5);
260
261 let r_frac = (p_rect.y + self.y_offset) / (s * s3h);
263 let q_frac = (p_rect.x + self.x_offset) / s - r_frac * half;
264
265 let cq_f = q_frac - lit(self.min_q as f64);
267 let cr_f = r_frac - lit(self.min_r as f64);
268
269 let cq = nalgebra::try_convert::<F, f64>(cq_f.floor()).unwrap_or(0.0) as i32;
270 let cr = nalgebra::try_convert::<F, f64>(cr_f.floor()).unwrap_or(0.0) as i32;
271
272 if cq < 0 || cr < 0 || cq >= self.cells_q as i32 || cr >= self.cells_r as i32 {
273 return None;
274 }
275
276 let frac_q = cq_f - lit(cq as f64);
278 let frac_r = cr_f - lit(cr as f64);
279 let is_upper = frac_q + frac_r > F::one();
280
281 let idx = (cr as usize * self.cells_q + cq as usize) * 2 + is_upper as usize;
282 self.cells.get(idx)?.as_ref()
283 }
284}
285
286fn centroid<F: Float>(tri: &[Point2<F>; 3]) -> Point2<F> {
287 let third: F = lit(1.0 / 3.0);
288 Point2::new(
289 (tri[0].x + tri[1].x + tri[2].x) * third,
290 (tri[0].y + tri[1].y + tri[2].y) * third,
291 )
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn make_hex_corners(radius: i32, spacing: f32) -> HashMap<GridCoords, Point2<f32>> {
299 let sqrt3 = 3.0f32.sqrt();
300 let mut map = HashMap::new();
301 for q in -radius..=radius {
302 for r in -radius..=radius {
303 if (q + r).abs() > radius {
304 continue;
305 }
306 let x = spacing * (q as f32 + r as f32 * 0.5);
307 let y = spacing * (r as f32 * sqrt3 / 2.0);
308 map.insert(GridCoords { i: q, j: r }, Point2::new(x, y));
309 }
310 }
311 map
312 }
313
314 #[test]
315 fn affine_from_triangle_identity() {
316 let tri: [Point2<f32>; 3] = [
317 Point2::new(0.0, 0.0),
318 Point2::new(1.0, 0.0),
319 Point2::new(0.0, 1.0),
320 ];
321 let aff = AffineTransform2D::from_triangle_correspondence(tri, tri).unwrap();
322 let p = Point2::new(0.3f32, 0.4);
323 let result = aff.apply(p);
324 assert!((result.x - p.x).abs() < 1e-6);
325 assert!((result.y - p.y).abs() < 1e-6);
326 }
327
328 #[test]
329 fn affine_maps_vertices_correctly() {
330 let src: [Point2<f32>; 3] = [
331 Point2::new(0.0, 0.0),
332 Point2::new(1.0, 0.0),
333 Point2::new(0.0, 1.0),
334 ];
335 let dst: [Point2<f32>; 3] = [
336 Point2::new(10.0, 20.0),
337 Point2::new(30.0, 20.0),
338 Point2::new(10.0, 50.0),
339 ];
340 let aff = AffineTransform2D::from_triangle_correspondence(src, dst).unwrap();
341 for (s, d) in src.iter().zip(dst.iter()) {
342 let result = aff.apply(*s);
343 assert!((result.x - d.x).abs() < 1e-4);
344 assert!((result.y - d.y).abs() < 1e-4);
345 }
346 }
347
348 #[test]
349 fn degenerate_triangle_returns_none() {
350 let src: [Point2<f32>; 3] = [
351 Point2::new(0.0, 0.0),
352 Point2::new(1.0, 0.0),
353 Point2::new(2.0, 0.0), ];
355 let dst = src;
356 assert!(AffineTransform2D::from_triangle_correspondence(src, dst).is_none());
357 }
358
359 #[test]
360 fn mesh_from_regular_hex_grid() {
361 let corners = make_hex_corners(3, 60.0);
362 let mesh = HexGridHomographyMesh::from_corners(&corners, 60.0).unwrap();
363 assert!(mesh.valid_triangles > 0);
364 assert!(mesh.rect_width > 0);
365 assert!(mesh.rect_height > 0);
366 }
367
368 #[test]
369 fn round_trip_through_affine_mesh() {
370 let spacing = 60.0;
371 let corners = make_hex_corners(3, spacing);
372 let mesh = HexGridHomographyMesh::from_corners(&corners, spacing).unwrap();
373
374 let s3h = 3.0f32.sqrt() / 2.0;
375
376 for (g, &img_pos) in &corners {
377 let rx = spacing * (g.i as f32 + g.j as f32 * 0.5) - mesh.x_offset;
378 let ry = spacing * (g.j as f32 * s3h) - mesh.y_offset;
379 let rect_pt = Point2::new(rx, ry);
380
381 if let Some(recovered) = mesh.rect_to_img_affine(rect_pt) {
382 assert!(
383 (recovered.x - img_pos.x).abs() < 1.0,
384 "x mismatch at ({},{}): {} vs {}",
385 g.i,
386 g.j,
387 recovered.x,
388 img_pos.x,
389 );
390 assert!(
391 (recovered.y - img_pos.y).abs() < 1.0,
392 "y mismatch at ({},{}): {} vs {}",
393 g.i,
394 g.j,
395 recovered.y,
396 img_pos.y,
397 );
398 }
399 }
400 }
401
402 #[test]
403 fn round_trip_through_homography_mesh() {
404 let spacing = 60.0;
405 let corners = make_hex_corners(3, spacing);
406 let mesh = HexGridHomographyMesh::from_corners(&corners, spacing).unwrap();
407
408 let s3h = 3.0f32.sqrt() / 2.0;
409
410 for (g, &img_pos) in &corners {
411 let rx = spacing * (g.i as f32 + g.j as f32 * 0.5) - mesh.x_offset;
412 let ry = spacing * (g.j as f32 * s3h) - mesh.y_offset;
413 let rect_pt = Point2::new(rx, ry);
414
415 if let Some(recovered) = mesh.rect_to_img(rect_pt) {
416 assert!(
417 (recovered.x - img_pos.x).abs() < 1.0,
418 "homography x mismatch at ({},{}): {} vs {}",
419 g.i,
420 g.j,
421 recovered.x,
422 img_pos.x,
423 );
424 assert!(
425 (recovered.y - img_pos.y).abs() < 1.0,
426 "homography y mismatch at ({},{}): {} vs {}",
427 g.i,
428 g.j,
429 recovered.y,
430 img_pos.y,
431 );
432 }
433 }
434 }
435
436 #[test]
437 fn too_few_corners_errors() {
438 let mut corners = HashMap::new();
439 corners.insert(GridCoords { i: 0, j: 0 }, Point2::new(0.0f32, 0.0));
440 corners.insert(GridCoords { i: 1, j: 0 }, Point2::new(50.0, 0.0));
441
442 let result = HexGridHomographyMesh::from_corners(&corners, 50.0);
443 assert!(result.is_err());
444 }
445
446 #[test]
447 fn missing_corners_handled_gracefully() {
448 let mut corners = make_hex_corners(3, 60.0);
449 corners.remove(&GridCoords { i: 0, j: 0 });
450 corners.remove(&GridCoords { i: 1, j: 1 });
451
452 let mesh = HexGridHomographyMesh::from_corners(&corners, 60.0);
453 assert!(mesh.is_ok());
454 let mesh = mesh.unwrap();
455 assert!(mesh.valid_triangles > 0);
456 }
457}