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