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