1use rustial_math::{ElevationGrid, TileId};
61use std::collections::HashMap;
62
63#[inline]
72pub fn interior_dims(grid: &ElevationGrid) -> (u32, u32) {
73 (grid.width.saturating_sub(2), grid.height.saturating_sub(2))
74}
75
76pub fn expand_with_clamped_border(src: &ElevationGrid) -> ElevationGrid {
88 let w = src.width;
89 let h = src.height;
90 let new_w = w + 2;
91 let new_h = h + 2;
92 let mut data = vec![0.0f32; (new_w * new_h) as usize];
93
94 for row in 0..h {
96 let src_start = (row * w) as usize;
97 let dst_start = ((row + 1) * new_w + 1) as usize;
98 data[dst_start..dst_start + w as usize]
99 .copy_from_slice(&src.data[src_start..src_start + w as usize]);
100 }
101
102 for col in 0..w {
104 data[(col + 1) as usize] = src.data[col as usize];
105 }
106 for col in 0..w {
108 data[((h + 1) * new_w + col + 1) as usize] = src.data[((h - 1) * w + col) as usize];
109 }
110 for row in 0..h {
112 data[((row + 1) * new_w) as usize] = src.data[(row * w) as usize];
113 }
114 for row in 0..h {
116 data[((row + 1) * new_w + w + 1) as usize] = src.data[(row * w + w - 1) as usize];
117 }
118
119 data[0] = src.data[0]; data[(w + 1) as usize] = src.data[(w - 1) as usize]; data[((h + 1) * new_w) as usize] = src.data[((h - 1) * w) as usize]; data[((h + 1) * new_w + w + 1) as usize] = src.data[((h - 1) * w + w - 1) as usize]; ElevationGrid {
127 width: new_w,
128 height: new_h,
129 min_elev: src.min_elev,
130 max_elev: src.max_elev,
131 data,
132 tile: src.tile,
133 }
134}
135
136pub fn patch_border_edge(target: &mut ElevationGrid, neighbor: &ElevationGrid, dx: i32, dy: i32) {
159 let (tw, th) = interior_dims(target);
160 let (nw, nh) = interior_dims(neighbor);
161 if tw != nw || th != nh || tw == 0 || th == 0 {
162 return;
163 }
164
165 let stride = target.width; let interior_range = target.max_elev - target.min_elev;
173 let margin = (interior_range * 0.5).max(200.0);
174 let clamp_lo = target.min_elev - margin;
175 let clamp_hi = target.max_elev + margin;
176
177 let mut write = |idx: usize, val: f32| {
178 target.data[idx] = val.clamp(clamp_lo, clamp_hi);
179 };
180
181 match (dx, dy) {
182 (0, -1) => {
184 let src_row = nh;
187 for col in 0..tw {
188 let src_idx = (src_row * stride + col + 1) as usize;
189 let dst_idx = (col + 1) as usize; write(dst_idx, neighbor.data[src_idx]);
191 }
192 }
193 (0, 1) => {
194 let src_row = 1u32;
197 let dst_row = th + 1;
198 for col in 0..tw {
199 let src_idx = (src_row * stride + col + 1) as usize;
200 let dst_idx = (dst_row * stride + col + 1) as usize;
201 write(dst_idx, neighbor.data[src_idx]);
202 }
203 }
204 (-1, 0) => {
205 let src_col = nw;
208 for row in 0..th {
209 let src_idx = ((row + 1) * stride + src_col) as usize;
210 let dst_idx = ((row + 1) * stride) as usize; write(dst_idx, neighbor.data[src_idx]);
212 }
213 }
214 (1, 0) => {
215 let src_col = 1u32;
218 let dst_col = tw + 1;
219 for row in 0..th {
220 let src_idx = ((row + 1) * stride + src_col) as usize;
221 let dst_idx = ((row + 1) * stride + dst_col) as usize;
222 write(dst_idx, neighbor.data[src_idx]);
223 }
224 }
225
226 (-1, -1) => {
228 let src_idx = (nh * stride + nw) as usize;
230 write(0, neighbor.data[src_idx]);
231 }
232 (1, -1) => {
233 let src_idx = (nh * stride + 1) as usize;
235 write((tw + 1) as usize, neighbor.data[src_idx]);
236 }
237 (-1, 1) => {
238 let src_idx = (stride + nw) as usize;
240 write(((th + 1) * stride) as usize, neighbor.data[src_idx]);
241 }
242 (1, 1) => {
243 let src_idx = (stride + 1) as usize;
245 write(
246 ((th + 1) * stride + tw + 1) as usize,
247 neighbor.data[src_idx],
248 );
249 }
250
251 _ => {} }
253}
254
255pub const NEIGHBOR_OFFSETS: [(i32, i32); 8] = [
257 (0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (1, -1), (-1, 1), (1, 1), ];
266
267#[derive(Debug, Clone, Default)]
273pub struct BackfillState {
274 pub filled: [bool; 8],
277}
278
279impl BackfillState {
280 pub fn reset(&mut self) {
282 self.filled = [false; 8];
283 }
284
285 #[inline]
287 pub fn mark(&mut self, offset_index: usize) {
288 if offset_index < 8 {
289 self.filled[offset_index] = true;
290 }
291 }
292
293 #[inline]
295 pub fn is_filled(&self, offset_index: usize) -> bool {
296 offset_index < 8 && self.filled[offset_index]
297 }
298}
299
300pub fn patch_changed_tiles(
313 cache: &mut HashMap<TileId, ElevationGrid>,
314 backfill_states: &mut HashMap<TileId, BackfillState>,
315 changed: &std::collections::HashSet<TileId>,
316) -> std::collections::HashSet<TileId> {
317 let mut modified = std::collections::HashSet::new();
318
319 struct PatchOp {
323 target: TileId,
324 neighbor: TileId,
325 offset_index: usize,
326 dx: i32,
327 dy: i32,
328 }
329
330 let mut ops: Vec<PatchOp> = Vec::new();
331
332 for &tile in changed {
335 backfill_states.entry(tile).or_default().reset();
336 }
337
338 for &tile in changed {
339 for (idx, &(dx, dy)) in NEIGHBOR_OFFSETS.iter().enumerate() {
341 if let Some(nb) = tile.neighbor(dx, dy) {
342 if cache.contains_key(&nb) {
343 ops.push(PatchOp {
344 target: tile,
345 neighbor: nb,
346 offset_index: idx,
347 dx,
348 dy,
349 });
350 }
351 }
352 }
353
354 for &(dx, dy) in NEIGHBOR_OFFSETS.iter() {
356 if let Some(nb) = tile.neighbor(dx, dy) {
357 if cache.contains_key(&nb) {
358 let rev_idx = NEIGHBOR_OFFSETS
362 .iter()
363 .position(|&(rdx, rdy)| rdx == -dx && rdy == -dy);
364 if let Some(ri) = rev_idx {
365 let nb_state = backfill_states.entry(nb).or_default();
366 if !nb_state.is_filled(ri) {
367 ops.push(PatchOp {
368 target: nb,
369 neighbor: tile,
370 offset_index: ri,
371 dx: -dx,
372 dy: -dy,
373 });
374 }
375 }
376 }
377 }
378 }
379 }
380
381 for op in &ops {
388 if op.target == op.neighbor {
389 continue;
390 }
391
392 let neighbor_grid = cache.get(&op.neighbor).cloned();
393 if let Some(ref nb_grid) = neighbor_grid {
394 if let Some(target_grid) = cache.get_mut(&op.target) {
395 patch_border_edge(target_grid, nb_grid, op.dx, op.dy);
396 backfill_states
397 .entry(op.target)
398 .or_default()
399 .mark(op.offset_index);
400 modified.insert(op.target);
401 }
402 }
403 }
404
405 modified
406}
407
408#[cfg(test)]
413mod tests {
414 use super::*;
415
416 fn const_grid(tile: TileId, size: u32, value: f32) -> ElevationGrid {
418 ElevationGrid::from_data(tile, size, size, vec![value; (size * size) as usize])
419 .expect("valid grid")
420 }
421
422 fn sequential_grid(tile: TileId, size: u32, base_val: f32) -> ElevationGrid {
424 let data: Vec<f32> = (0..size * size).map(|i| base_val + i as f32).collect();
425 ElevationGrid::from_data(tile, size, size, data).expect("valid grid")
426 }
427
428 #[test]
431 fn expand_produces_correct_dimensions() {
432 let tile = TileId::new(3, 4, 4);
433 let src = const_grid(tile, 4, 100.0);
434 let expanded = expand_with_clamped_border(&src);
435 assert_eq!(expanded.width, 6);
436 assert_eq!(expanded.height, 6);
437 assert_eq!(interior_dims(&expanded), (4, 4));
438 }
439
440 #[test]
441 fn expand_interior_unchanged() {
442 let tile = TileId::new(3, 4, 4);
443 let src = sequential_grid(tile, 4, 0.0);
444 let expanded = expand_with_clamped_border(&src);
445
446 for row in 0..4u32 {
448 for col in 0..4u32 {
449 let orig = src.data[(row * 4 + col) as usize];
450 let exp = expanded.data[((row + 1) * 6 + (col + 1)) as usize];
451 assert!(
452 (orig - exp).abs() < 1e-6,
453 "interior mismatch at ({col},{row}): {orig} vs {exp}"
454 );
455 }
456 }
457 }
458
459 #[test]
460 fn expand_borders_clamp_to_nearest_interior() {
461 let tile = TileId::new(3, 4, 4);
462 let src = sequential_grid(tile, 4, 0.0);
463 let expanded = expand_with_clamped_border(&src);
464 let s = 6u32; for col in 0..4u32 {
468 assert!(
469 (expanded.data[(col + 1) as usize] - src.data[col as usize]).abs() < 1e-6,
470 "north border mismatch at col {col}"
471 );
472 }
473 for col in 0..4u32 {
475 assert!(
476 (expanded.data[(5 * s + col + 1) as usize] - src.data[(3 * 4 + col) as usize])
477 .abs()
478 < 1e-6,
479 "south border mismatch at col {col}"
480 );
481 }
482 for row in 0..4u32 {
484 assert!(
485 (expanded.data[((row + 1) * s) as usize] - src.data[(row * 4) as usize]).abs()
486 < 1e-6,
487 "west border mismatch at row {row}"
488 );
489 }
490 for row in 0..4u32 {
492 assert!(
493 (expanded.data[((row + 1) * s + 5) as usize] - src.data[(row * 4 + 3) as usize])
494 .abs()
495 < 1e-6,
496 "east border mismatch at row {row}"
497 );
498 }
499 assert!((expanded.data[0] - src.data[0]).abs() < 1e-6);
501 assert!(
503 (expanded.data[(5 * s + 5) as usize] - src.data[(3 * 4 + 3) as usize]).abs() < 1e-6
504 );
505 }
506
507 #[test]
510 fn patch_north_border() {
511 let tile = TileId::new(3, 4, 4);
512 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
513
514 let north_tile = TileId::new(3, 4, 3);
515 let neighbor = expand_with_clamped_border(&const_grid(north_tile, 4, 200.0));
516
517 patch_border_edge(&mut target, &neighbor, 0, -1);
518
519 let s = 6u32;
520 for col in 1..=4u32 {
522 let val = target.data[col as usize];
523 assert!(
524 (val - 200.0).abs() < 1e-6,
525 "north border at col {col}: expected 200, got {val}"
526 );
527 }
528 for col in 1..=4u32 {
530 let val = target.data[(5 * s + col) as usize];
531 assert!(
532 (val - 100.0).abs() < 1e-6,
533 "south border should still be 100 at col {col}, got {val}"
534 );
535 }
536 }
537
538 #[test]
539 fn patch_east_border() {
540 let tile = TileId::new(3, 4, 4);
541 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
542
543 let east_tile = TileId::new(3, 5, 4);
544 let neighbor = expand_with_clamped_border(&const_grid(east_tile, 4, 300.0));
545
546 patch_border_edge(&mut target, &neighbor, 1, 0);
547
548 let s = 6u32;
549 for row in 1..=4u32 {
551 let val = target.data[(row * s + 5) as usize];
552 assert!(
553 (val - 300.0).abs() < 1e-6,
554 "east border at row {row}: expected 300, got {val}"
555 );
556 }
557 }
558
559 #[test]
560 fn patch_nw_corner() {
561 let tile = TileId::new(3, 4, 4);
562 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
563
564 let nw_tile = TileId::new(3, 3, 3);
565 let mut nw_src = const_grid(nw_tile, 4, 500.0);
566 nw_src.data[(3 * 4 + 3) as usize] = 999.0;
568 let neighbor = expand_with_clamped_border(&nw_src);
569
570 patch_border_edge(&mut target, &neighbor, -1, -1);
571
572 assert!(
575 (target.data[0] - 300.0).abs() < 1e-6,
576 "NW corner should be clamped to 300.0, got {}",
577 target.data[0]
578 );
579 }
580
581 #[test]
582 fn patch_mismatched_dims_is_noop() {
583 let tile = TileId::new(3, 4, 4);
584 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
585
586 let nb_tile = TileId::new(3, 5, 4);
588 let neighbor = expand_with_clamped_border(&const_grid(nb_tile, 8, 999.0));
589
590 patch_border_edge(&mut target, &neighbor, 1, 0);
591
592 let s = 6u32;
594 for row in 1..=4u32 {
595 let val = target.data[(row * s + 5) as usize];
596 assert!(
597 (val - 100.0).abs() < 1e-6,
598 "east border should still be 100 at row {row}, got {val}"
599 );
600 }
601 }
602
603 #[test]
606 fn patch_changed_two_horizontal_neighbours() {
607 let left = TileId::new(3, 3, 4);
608 let right = TileId::new(3, 4, 4);
609
610 let mut cache = HashMap::new();
611 cache.insert(
612 left,
613 expand_with_clamped_border(&const_grid(left, 4, 100.0)),
614 );
615 cache.insert(
616 right,
617 expand_with_clamped_border(&const_grid(right, 4, 200.0)),
618 );
619
620 let mut states = HashMap::new();
621 let changed: std::collections::HashSet<TileId> = [left, right].iter().copied().collect();
622
623 let modified = patch_changed_tiles(&mut cache, &mut states, &changed);
624
625 assert!(modified.contains(&left));
627 assert!(modified.contains(&right));
628
629 let s = 6u32;
630 let left_grid = cache.get(&left).unwrap();
632 for row in 1..=4u32 {
633 let val = left_grid.data[(row * s + 5) as usize];
634 assert!(
635 (val - 200.0).abs() < 1e-6,
636 "left east border at row {row}: expected 200, got {val}"
637 );
638 }
639
640 let right_grid = cache.get(&right).unwrap();
642 for row in 1..=4u32 {
643 let val = right_grid.data[(row * s) as usize];
644 assert!(
645 (val - 100.0).abs() < 1e-6,
646 "right west border at row {row}: expected 100, got {val}"
647 );
648 }
649 }
650
651 #[test]
652 fn patch_changed_vertical_neighbours() {
653 let upper = TileId::new(3, 4, 3);
654 let lower = TileId::new(3, 4, 4);
655
656 let mut cache = HashMap::new();
657 cache.insert(
658 upper,
659 expand_with_clamped_border(&const_grid(upper, 4, 50.0)),
660 );
661 cache.insert(
662 lower,
663 expand_with_clamped_border(&const_grid(lower, 4, 150.0)),
664 );
665
666 let mut states = HashMap::new();
667 let changed: std::collections::HashSet<TileId> = [upper, lower].iter().copied().collect();
668
669 patch_changed_tiles(&mut cache, &mut states, &changed);
670
671 let s = 6u32;
672 let upper_grid = cache.get(&upper).unwrap();
674 for col in 1..=4u32 {
675 let val = upper_grid.data[(5 * s + col) as usize];
676 assert!(
677 (val - 150.0).abs() < 1e-6,
678 "upper south border at col {col}: expected 150, got {val}"
679 );
680 }
681 let lower_grid = cache.get(&lower).unwrap();
683 for col in 1..=4u32 {
684 let val = lower_grid.data[col as usize];
685 assert!(
686 (val - 50.0).abs() < 1e-6,
687 "lower north border at col {col}: expected 50, got {val}"
688 );
689 }
690 }
691
692 #[test]
693 fn backfill_state_tracks_filled_directions() {
694 let mut state = BackfillState::default();
695 assert!(!state.is_filled(0));
696 state.mark(0);
697 assert!(state.is_filled(0));
698 state.reset();
699 assert!(!state.is_filled(0));
700 }
701
702 #[test]
703 fn incremental_patch_skips_already_filled() {
704 let left = TileId::new(3, 3, 4);
706 let right = TileId::new(3, 4, 4);
707
708 let mut cache = HashMap::new();
709 let mut states = HashMap::new();
710
711 cache.insert(
713 left,
714 expand_with_clamped_border(&const_grid(left, 4, 100.0)),
715 );
716 let changed1: std::collections::HashSet<TileId> = [left].iter().copied().collect();
717 patch_changed_tiles(&mut cache, &mut states, &changed1);
718
719 let s = 6u32;
721 let left_grid = cache.get(&left).unwrap();
722 for row in 1..=4u32 {
723 let val = left_grid.data[(row * s + 5) as usize];
724 assert!(
725 (val - 100.0).abs() < 1e-6,
726 "left east should be clamped 100 before right arrives, got {val}"
727 );
728 }
729
730 cache.insert(
732 right,
733 expand_with_clamped_border(&const_grid(right, 4, 200.0)),
734 );
735 let changed2: std::collections::HashSet<TileId> = [right].iter().copied().collect();
736 patch_changed_tiles(&mut cache, &mut states, &changed2);
737
738 let left_grid = cache.get(&left).unwrap();
740 for row in 1..=4u32 {
741 let val = left_grid.data[(row * s + 5) as usize];
742 assert!(
743 (val - 200.0).abs() < 1e-6,
744 "left east border should be 200 after right arrives, got {val}"
745 );
746 }
747
748 let right_grid = cache.get(&right).unwrap();
750 for row in 1..=4u32 {
751 let val = right_grid.data[(row * s) as usize];
752 assert!(
753 (val - 100.0).abs() < 1e-6,
754 "right west border should be 100, got {val}"
755 );
756 }
757 }
758
759 #[test]
762 fn neighbor_east() {
763 let tile = TileId::new(3, 4, 4);
764 let east = tile.neighbor(1, 0).expect("east");
765 assert_eq!(east, TileId::new(3, 5, 4));
766 }
767
768 #[test]
769 fn neighbor_west_wrap() {
770 let tile = TileId::new(3, 0, 4);
771 let west = tile.neighbor(-1, 0).expect("west wrap");
772 assert_eq!(west, TileId::new(3, 7, 4));
773 }
774
775 #[test]
776 fn neighbor_east_wrap() {
777 let tile = TileId::new(3, 7, 4);
778 let east = tile.neighbor(1, 0).expect("east wrap");
779 assert_eq!(east, TileId::new(3, 0, 4));
780 }
781
782 #[test]
783 fn neighbor_north_pole() {
784 let tile = TileId::new(3, 4, 0);
785 assert!(tile.neighbor(0, -1).is_none());
786 }
787
788 #[test]
789 fn neighbor_south_pole() {
790 let tile = TileId::new(3, 4, 7);
791 assert!(tile.neighbor(0, 1).is_none());
792 }
793}