1use crate::grid_index::GridIndex;
9use crate::homography::{estimate_homography, Homography};
10use nalgebra::{Matrix2, Point2, Vector2};
11use std::collections::HashMap;
12
13const SQRT3_HALF: f64 = 0.866_025_403_784_438_6;
15
16#[non_exhaustive]
17#[derive(thiserror::Error, Debug)]
18pub enum HexMeshError {
19 #[error("not enough grid corners (need at least 3)")]
20 NotEnoughCorners,
21 #[error("no valid triangles found")]
22 NoValidTriangles,
23}
24
25#[derive(Clone, Copy, Debug)]
27pub struct AffineTransform2D {
28 pub linear: Matrix2<f64>,
30 pub translation: Vector2<f64>,
32}
33
34impl AffineTransform2D {
35 pub fn from_triangle_correspondence(
39 src: [Point2<f64>; 3],
40 dst: [Point2<f64>; 3],
41 ) -> Option<Self> {
42 let ds1 = src[1] - src[0];
45 let ds2 = src[2] - src[0];
46 let dd1 = dst[1] - dst[0];
47 let dd2 = dst[2] - dst[0];
48
49 let src_mat = Matrix2::new(ds1.x, ds2.x, ds1.y, ds2.y);
51
52 let src_inv = src_mat.try_inverse()?;
53
54 let dst_mat = Matrix2::new(dd1.x, dd2.x, dd1.y, dd2.y);
56 let linear = dst_mat * src_inv;
57
58 let t = dst[0] - linear * Vector2::new(src[0].x, src[0].y);
59 let translation = Vector2::new(t.x, t.y);
60
61 Some(Self {
62 linear,
63 translation,
64 })
65 }
66
67 pub fn apply(&self, p: Point2<f64>) -> Point2<f64> {
69 let v = self.linear * Vector2::new(p.x, p.y) + self.translation;
70 Point2::new(v.x, v.y)
71 }
72}
73
74#[derive(Clone, Debug)]
75struct TriangleCell {
76 affine: AffineTransform2D,
78 homography: Homography,
80}
81
82#[derive(Clone, Debug)]
89pub struct HexGridHomographyMesh {
90 pub min_q: i32,
91 pub min_r: i32,
92 pub cells_q: usize,
94 pub cells_r: usize,
96 pub px_per_cell: f32,
98 pub valid_triangles: usize,
100 pub rect_width: usize,
102 pub rect_height: usize,
103
104 cells: Vec<Option<TriangleCell>>,
107
108 x_offset: f64,
110 y_offset: f64,
111}
112
113impl HexGridHomographyMesh {
114 pub fn from_corners(
119 corners: &HashMap<GridIndex, Point2<f32>>,
120 px_per_cell: f32,
121 ) -> Result<Self, HexMeshError> {
122 if corners.len() < 3 {
123 return Err(HexMeshError::NotEnoughCorners);
124 }
125
126 let (mut min_q, mut min_r) = (i32::MAX, i32::MAX);
127 let (mut max_q, mut max_r) = (i32::MIN, i32::MIN);
128 for g in corners.keys() {
129 min_q = min_q.min(g.i);
130 min_r = min_r.min(g.j);
131 max_q = max_q.max(g.i);
132 max_r = max_r.max(g.j);
133 }
134
135 if max_q - min_q < 1 || max_r - min_r < 1 {
136 return Err(HexMeshError::NoValidTriangles);
137 }
138
139 let cells_q = (max_q - min_q) as usize;
140 let cells_r = (max_r - min_r) as usize;
141 let s = px_per_cell as f64;
142
143 let mut x_min = f64::MAX;
145 let mut x_max = f64::MIN;
146 let mut y_min = f64::MAX;
147 let mut y_max = f64::MIN;
148
149 for &q in &[min_q, max_q] {
151 for &r in &[min_r, max_r] {
152 let x = s * (q as f64 + r as f64 * 0.5);
153 let y = s * (r as f64 * SQRT3_HALF);
154 x_min = x_min.min(x);
155 x_max = x_max.max(x);
156 y_min = y_min.min(y);
157 y_max = y_max.max(y);
158 }
159 }
160
161 let rect_width = ((x_max - x_min).round().max(1.0)) as usize;
162 let rect_height = ((y_max - y_min).round().max(1.0)) as usize;
163
164 let axial_to_rect = |q: i32, r: i32| -> Point2<f64> {
165 Point2::new(
166 s * (q as f64 + r as f64 * 0.5) - x_min,
167 s * (r as f64 * SQRT3_HALF) - y_min,
168 )
169 };
170
171 let mut cells = vec![None; cells_q * cells_r * 2];
172 let mut valid_triangles = 0usize;
173
174 for cr in 0..cells_r {
175 for cq in 0..cells_q {
176 let q0 = min_q + cq as i32;
177 let r0 = min_r + cr as i32;
178
179 let g00 = GridIndex { i: q0, j: r0 };
180 let g10 = GridIndex { i: q0 + 1, j: r0 };
181 let g01 = GridIndex { i: q0, j: r0 + 1 };
182 let g11 = GridIndex {
183 i: q0 + 1,
184 j: r0 + 1,
185 };
186
187 let p00 = corners.get(&g00).copied();
188 let p10 = corners.get(&g10).copied();
189 let p01 = corners.get(&g01).copied();
190 let p11 = corners.get(&g11).copied();
191
192 let idx_base = (cr * cells_q + cq) * 2;
193
194 if let (Some(ip00), Some(ip10), Some(ip01)) = (p00, p10, p01) {
196 let rect_tri = [
197 axial_to_rect(q0, r0),
198 axial_to_rect(q0 + 1, r0),
199 axial_to_rect(q0, r0 + 1),
200 ];
201 let img_tri = [
202 Point2::new(ip00.x as f64, ip00.y as f64),
203 Point2::new(ip10.x as f64, ip10.y as f64),
204 Point2::new(ip01.x as f64, ip01.y as f64),
205 ];
206
207 if let Some(affine) =
208 AffineTransform2D::from_triangle_correspondence(rect_tri, img_tri)
209 {
210 let rect_c = centroid(&rect_tri);
212 let img_c = affine.apply(rect_c);
213 let rect_4: Vec<Point2<f32>> = rect_tri
214 .iter()
215 .chain(std::iter::once(&rect_c))
216 .map(|p| Point2::new(p.x as f32, p.y as f32))
217 .collect();
218 let img_4: Vec<Point2<f32>> = img_tri
219 .iter()
220 .chain(std::iter::once(&img_c))
221 .map(|p| Point2::new(p.x as f32, p.y as f32))
222 .collect();
223
224 if let Some(homography) = estimate_homography(&rect_4, &img_4) {
225 cells[idx_base] = Some(TriangleCell { affine, homography });
226 valid_triangles += 1;
227 }
228 }
229 }
230
231 if let (Some(ip10), Some(ip01), Some(ip11)) = (p10, p01, p11) {
233 let rect_tri = [
234 axial_to_rect(q0 + 1, r0),
235 axial_to_rect(q0, r0 + 1),
236 axial_to_rect(q0 + 1, r0 + 1),
237 ];
238 let img_tri = [
239 Point2::new(ip10.x as f64, ip10.y as f64),
240 Point2::new(ip01.x as f64, ip01.y as f64),
241 Point2::new(ip11.x as f64, ip11.y as f64),
242 ];
243
244 if let Some(affine) =
245 AffineTransform2D::from_triangle_correspondence(rect_tri, img_tri)
246 {
247 let rect_c = centroid(&rect_tri);
248 let img_c = affine.apply(rect_c);
249 let rect_4: Vec<Point2<f32>> = rect_tri
250 .iter()
251 .chain(std::iter::once(&rect_c))
252 .map(|p| Point2::new(p.x as f32, p.y as f32))
253 .collect();
254 let img_4: Vec<Point2<f32>> = img_tri
255 .iter()
256 .chain(std::iter::once(&img_c))
257 .map(|p| Point2::new(p.x as f32, p.y as f32))
258 .collect();
259
260 if let Some(homography) = estimate_homography(&rect_4, &img_4) {
261 cells[idx_base + 1] = Some(TriangleCell { affine, homography });
262 valid_triangles += 1;
263 }
264 }
265 }
266 }
267 }
268
269 if valid_triangles == 0 {
270 return Err(HexMeshError::NoValidTriangles);
271 }
272
273 Ok(Self {
274 min_q,
275 min_r,
276 cells_q,
277 cells_r,
278 px_per_cell,
279 valid_triangles,
280 rect_width,
281 rect_height,
282 cells,
283 x_offset: x_min,
284 y_offset: y_min,
285 })
286 }
287
288 pub fn rect_to_img_affine(&self, p_rect: Point2<f32>) -> Option<Point2<f32>> {
293 let (cell, p64) = self.lookup_cell(p_rect)?;
294 let result = cell.affine.apply(p64);
295 Some(Point2::new(result.x as f32, result.y as f32))
296 }
297
298 pub fn rect_to_img(&self, p_rect: Point2<f32>) -> Option<Point2<f32>> {
303 let (cell, _) = self.lookup_cell(p_rect)?;
304 Some(cell.homography.apply(p_rect))
305 }
306
307 fn lookup_cell(&self, p_rect: Point2<f32>) -> Option<(&TriangleCell, Point2<f64>)> {
309 let s = self.px_per_cell as f64;
310 if s <= 0.0 {
311 return None;
312 }
313
314 let p64 = Point2::new(p_rect.x as f64, p_rect.y as f64);
315
316 let r_frac = (p64.y + self.y_offset) / (s * SQRT3_HALF);
318 let q_frac = (p64.x + self.x_offset) / s - r_frac * 0.5;
319
320 let cq_f = q_frac - self.min_q as f64;
322 let cr_f = r_frac - self.min_r as f64;
323
324 let cq = cq_f.floor() as i32;
325 let cr = cr_f.floor() as i32;
326
327 if cq < 0 || cr < 0 || cq >= self.cells_q as i32 || cr >= self.cells_r as i32 {
328 return None;
329 }
330
331 let frac_q = cq_f - cq as f64;
333 let frac_r = cr_f - cr as f64;
334 let is_upper = frac_q + frac_r > 1.0;
335
336 let idx = (cr as usize * self.cells_q + cq as usize) * 2 + is_upper as usize;
337 let cell = self.cells.get(idx)?.as_ref()?;
338
339 Some((cell, p64))
340 }
341}
342
343fn centroid(tri: &[Point2<f64>; 3]) -> Point2<f64> {
344 Point2::new(
345 (tri[0].x + tri[1].x + tri[2].x) / 3.0,
346 (tri[0].y + tri[1].y + tri[2].y) / 3.0,
347 )
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 fn make_hex_corners(radius: i32, spacing: f32) -> HashMap<GridIndex, Point2<f32>> {
355 let sqrt3 = 3.0f32.sqrt();
356 let mut map = HashMap::new();
357 for q in -radius..=radius {
358 for r in -radius..=radius {
359 if (q + r).abs() > radius {
360 continue;
361 }
362 let x = spacing * (q as f32 + r as f32 * 0.5);
363 let y = spacing * (r as f32 * sqrt3 / 2.0);
364 map.insert(GridIndex { i: q, j: r }, Point2::new(x, y));
365 }
366 }
367 map
368 }
369
370 #[test]
371 fn affine_from_triangle_identity() {
372 let tri = [
373 Point2::new(0.0, 0.0),
374 Point2::new(1.0, 0.0),
375 Point2::new(0.0, 1.0),
376 ];
377 let aff = AffineTransform2D::from_triangle_correspondence(tri, tri).unwrap();
378 let p = Point2::new(0.3, 0.4);
379 let result = aff.apply(p);
380 assert!((result.x - p.x).abs() < 1e-10);
381 assert!((result.y - p.y).abs() < 1e-10);
382 }
383
384 #[test]
385 fn affine_maps_vertices_correctly() {
386 let src = [
387 Point2::new(0.0, 0.0),
388 Point2::new(1.0, 0.0),
389 Point2::new(0.0, 1.0),
390 ];
391 let dst = [
392 Point2::new(10.0, 20.0),
393 Point2::new(30.0, 20.0),
394 Point2::new(10.0, 50.0),
395 ];
396 let aff = AffineTransform2D::from_triangle_correspondence(src, dst).unwrap();
397 for (s, d) in src.iter().zip(dst.iter()) {
398 let result = aff.apply(*s);
399 assert!((result.x - d.x).abs() < 1e-10);
400 assert!((result.y - d.y).abs() < 1e-10);
401 }
402 }
403
404 #[test]
405 fn degenerate_triangle_returns_none() {
406 let src = [
407 Point2::new(0.0, 0.0),
408 Point2::new(1.0, 0.0),
409 Point2::new(2.0, 0.0), ];
411 let dst = src;
412 assert!(AffineTransform2D::from_triangle_correspondence(src, dst).is_none());
413 }
414
415 #[test]
416 fn mesh_from_regular_hex_grid() {
417 let corners = make_hex_corners(3, 60.0);
418 let mesh = HexGridHomographyMesh::from_corners(&corners, 60.0).unwrap();
419 assert!(mesh.valid_triangles > 0);
420 assert!(mesh.rect_width > 0);
421 assert!(mesh.rect_height > 0);
422 }
423
424 #[test]
425 fn round_trip_through_affine_mesh() {
426 let spacing = 60.0;
427 let corners = make_hex_corners(3, spacing);
428 let mesh = HexGridHomographyMesh::from_corners(&corners, spacing).unwrap();
429
430 let s = spacing as f64;
432
433 for (g, &img_pos) in &corners {
435 let rx = (s * (g.i as f64 + g.j as f64 * 0.5) - mesh.x_offset) as f32;
436 let ry = (s * (g.j as f64 * SQRT3_HALF) - mesh.y_offset) as f32;
437 let rect_pt = Point2::new(rx, ry);
438
439 if let Some(recovered) = mesh.rect_to_img_affine(rect_pt) {
440 assert!(
441 (recovered.x - img_pos.x).abs() < 1.0,
442 "x mismatch at ({},{}): {} vs {}",
443 g.i,
444 g.j,
445 recovered.x,
446 img_pos.x,
447 );
448 assert!(
449 (recovered.y - img_pos.y).abs() < 1.0,
450 "y mismatch at ({},{}): {} vs {}",
451 g.i,
452 g.j,
453 recovered.y,
454 img_pos.y,
455 );
456 }
457 }
459 }
460
461 #[test]
462 fn round_trip_through_homography_mesh() {
463 let spacing = 60.0;
464 let corners = make_hex_corners(3, spacing);
465 let mesh = HexGridHomographyMesh::from_corners(&corners, spacing).unwrap();
466
467 let s = spacing as f64;
468
469 for (g, &img_pos) in &corners {
470 let rx = (s * (g.i as f64 + g.j as f64 * 0.5) - mesh.x_offset) as f32;
471 let ry = (s * (g.j as f64 * SQRT3_HALF) - mesh.y_offset) as f32;
472 let rect_pt = Point2::new(rx, ry);
473
474 if let Some(recovered) = mesh.rect_to_img(rect_pt) {
475 assert!(
476 (recovered.x - img_pos.x).abs() < 1.0,
477 "homography x mismatch at ({},{}): {} vs {}",
478 g.i,
479 g.j,
480 recovered.x,
481 img_pos.x,
482 );
483 assert!(
484 (recovered.y - img_pos.y).abs() < 1.0,
485 "homography y mismatch at ({},{}): {} vs {}",
486 g.i,
487 g.j,
488 recovered.y,
489 img_pos.y,
490 );
491 }
492 }
493 }
494
495 #[test]
496 fn too_few_corners_errors() {
497 let mut corners = HashMap::new();
498 corners.insert(GridIndex { i: 0, j: 0 }, Point2::new(0.0, 0.0));
499 corners.insert(GridIndex { i: 1, j: 0 }, Point2::new(50.0, 0.0));
500
501 let result = HexGridHomographyMesh::from_corners(&corners, 50.0);
502 assert!(result.is_err());
503 }
504
505 #[test]
506 fn missing_corners_handled_gracefully() {
507 let mut corners = make_hex_corners(3, 60.0);
508 corners.remove(&GridIndex { i: 0, j: 0 });
510 corners.remove(&GridIndex { i: 1, j: 1 });
511
512 let mesh = HexGridHomographyMesh::from_corners(&corners, 60.0);
513 assert!(mesh.is_ok());
515 let mesh = mesh.unwrap();
516 assert!(mesh.valid_triangles > 0);
517 }
518}