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] =
109 src.data[((h - 1) * w + col) as usize];
110 }
111 for row in 0..h {
113 data[((row + 1) * new_w) as usize] = src.data[(row * w) as usize];
114 }
115 for row in 0..h {
117 data[((row + 1) * new_w + w + 1) as usize] =
118 src.data[(row * w + w - 1) as usize];
119 }
120
121 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] =
126 src.data[((h - 1) * w + w - 1) as usize]; ElevationGrid {
130 width: new_w,
131 height: new_h,
132 min_elev: src.min_elev,
133 max_elev: src.max_elev,
134 data,
135 tile: src.tile,
136 }
137}
138
139pub fn patch_border_edge(
162 target: &mut ElevationGrid,
163 neighbor: &ElevationGrid,
164 dx: i32,
165 dy: i32,
166) {
167 let (tw, th) = interior_dims(target);
168 let (nw, nh) = interior_dims(neighbor);
169 if tw != nw || th != nh || tw == 0 || th == 0 {
170 return;
171 }
172
173 let stride = target.width; let interior_range = target.max_elev - target.min_elev;
181 let margin = (interior_range * 0.5).max(200.0);
182 let clamp_lo = target.min_elev - margin;
183 let clamp_hi = target.max_elev + margin;
184
185 let mut write = |idx: usize, val: f32| {
186 target.data[idx] = val.clamp(clamp_lo, clamp_hi);
187 };
188
189 match (dx, dy) {
190 (0, -1) => {
192 let src_row = nh;
195 for col in 0..tw {
196 let src_idx = (src_row * stride + col + 1) as usize;
197 let dst_idx = (col + 1) as usize; write(dst_idx, neighbor.data[src_idx]);
199 }
200 }
201 (0, 1) => {
202 let src_row = 1u32;
205 let dst_row = th + 1;
206 for col in 0..tw {
207 let src_idx = (src_row * stride + col + 1) as usize;
208 let dst_idx = (dst_row * stride + col + 1) as usize;
209 write(dst_idx, neighbor.data[src_idx]);
210 }
211 }
212 (-1, 0) => {
213 let src_col = nw;
216 for row in 0..th {
217 let src_idx = ((row + 1) * stride + src_col) as usize;
218 let dst_idx = ((row + 1) * stride) as usize; write(dst_idx, neighbor.data[src_idx]);
220 }
221 }
222 (1, 0) => {
223 let src_col = 1u32;
226 let dst_col = tw + 1;
227 for row in 0..th {
228 let src_idx = ((row + 1) * stride + src_col) as usize;
229 let dst_idx = ((row + 1) * stride + dst_col) as usize;
230 write(dst_idx, neighbor.data[src_idx]);
231 }
232 }
233
234 (-1, -1) => {
236 let src_idx = (nh * stride + nw) as usize;
238 write(0, neighbor.data[src_idx]);
239 }
240 (1, -1) => {
241 let src_idx = (nh * stride + 1) as usize;
243 write((tw + 1) as usize, neighbor.data[src_idx]);
244 }
245 (-1, 1) => {
246 let src_idx = (1 * stride + nw) as usize;
248 write(((th + 1) * stride) as usize, neighbor.data[src_idx]);
249 }
250 (1, 1) => {
251 let src_idx = (1 * stride + 1) as usize;
253 write(((th + 1) * stride + tw + 1) as usize, neighbor.data[src_idx]);
254 }
255
256 _ => {} }
258}
259
260pub const NEIGHBOR_OFFSETS: [(i32, i32); 8] = [
262 (0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (1, -1), (-1, 1), (1, 1), ];
271
272#[derive(Debug, Clone, Default)]
278pub struct BackfillState {
279 pub filled: [bool; 8],
282}
283
284impl BackfillState {
285 pub fn reset(&mut self) {
287 self.filled = [false; 8];
288 }
289
290 #[inline]
292 pub fn mark(&mut self, offset_index: usize) {
293 if offset_index < 8 {
294 self.filled[offset_index] = true;
295 }
296 }
297
298 #[inline]
300 pub fn is_filled(&self, offset_index: usize) -> bool {
301 offset_index < 8 && self.filled[offset_index]
302 }
303}
304
305pub fn patch_changed_tiles(
318 cache: &mut HashMap<TileId, ElevationGrid>,
319 backfill_states: &mut HashMap<TileId, BackfillState>,
320 changed: &std::collections::HashSet<TileId>,
321) -> std::collections::HashSet<TileId> {
322 let mut modified = std::collections::HashSet::new();
323
324 struct PatchOp {
328 target: TileId,
329 neighbor: TileId,
330 offset_index: usize,
331 dx: i32,
332 dy: i32,
333 }
334
335 let mut ops: Vec<PatchOp> = Vec::new();
336
337 for &tile in changed {
340 backfill_states.entry(tile).or_default().reset();
341 }
342
343 for &tile in changed {
344 for (idx, &(dx, dy)) in NEIGHBOR_OFFSETS.iter().enumerate() {
346 if let Some(nb) = tile.neighbor(dx, dy) {
347 if cache.contains_key(&nb) {
348 ops.push(PatchOp {
349 target: tile,
350 neighbor: nb,
351 offset_index: idx,
352 dx,
353 dy,
354 });
355 }
356 }
357 }
358
359 for (_idx, &(dx, dy)) in NEIGHBOR_OFFSETS.iter().enumerate() {
361 if let Some(nb) = tile.neighbor(dx, dy) {
362 if cache.contains_key(&nb) {
363 let rev_idx = NEIGHBOR_OFFSETS
367 .iter()
368 .position(|&(rdx, rdy)| rdx == -dx && rdy == -dy);
369 if let Some(ri) = rev_idx {
370 let nb_state = backfill_states.entry(nb).or_default();
371 if !nb_state.is_filled(ri) {
372 ops.push(PatchOp {
373 target: nb,
374 neighbor: tile,
375 offset_index: ri,
376 dx: -dx,
377 dy: -dy,
378 });
379 }
380 }
381 }
382 }
383 }
384 }
385
386 for op in &ops {
393 if op.target == op.neighbor {
394 continue;
395 }
396
397 let neighbor_grid = cache.get(&op.neighbor).cloned();
398 if let Some(ref nb_grid) = neighbor_grid {
399 if let Some(target_grid) = cache.get_mut(&op.target) {
400 patch_border_edge(target_grid, nb_grid, op.dx, op.dy);
401 backfill_states
402 .entry(op.target)
403 .or_default()
404 .mark(op.offset_index);
405 modified.insert(op.target);
406 }
407 }
408 }
409
410 modified
411}
412
413#[cfg(test)]
418mod tests {
419 use super::*;
420
421 fn const_grid(tile: TileId, size: u32, value: f32) -> ElevationGrid {
423 ElevationGrid::from_data(tile, size, size, vec![value; (size * size) as usize])
424 .expect("valid grid")
425 }
426
427 fn sequential_grid(tile: TileId, size: u32, base_val: f32) -> ElevationGrid {
429 let data: Vec<f32> = (0..size * size)
430 .map(|i| base_val + i as f32)
431 .collect();
432 ElevationGrid::from_data(tile, size, size, data).expect("valid grid")
433 }
434
435 #[test]
438 fn expand_produces_correct_dimensions() {
439 let tile = TileId::new(3, 4, 4);
440 let src = const_grid(tile, 4, 100.0);
441 let expanded = expand_with_clamped_border(&src);
442 assert_eq!(expanded.width, 6);
443 assert_eq!(expanded.height, 6);
444 assert_eq!(interior_dims(&expanded), (4, 4));
445 }
446
447 #[test]
448 fn expand_interior_unchanged() {
449 let tile = TileId::new(3, 4, 4);
450 let src = sequential_grid(tile, 4, 0.0);
451 let expanded = expand_with_clamped_border(&src);
452
453 for row in 0..4u32 {
455 for col in 0..4u32 {
456 let orig = src.data[(row * 4 + col) as usize];
457 let exp = expanded.data[((row + 1) * 6 + (col + 1)) as usize];
458 assert!(
459 (orig - exp).abs() < 1e-6,
460 "interior mismatch at ({col},{row}): {orig} vs {exp}"
461 );
462 }
463 }
464 }
465
466 #[test]
467 fn expand_borders_clamp_to_nearest_interior() {
468 let tile = TileId::new(3, 4, 4);
469 let src = sequential_grid(tile, 4, 0.0);
470 let expanded = expand_with_clamped_border(&src);
471 let s = 6u32; for col in 0..4u32 {
475 assert!(
476 (expanded.data[(col + 1) as usize] - src.data[col as usize]).abs() < 1e-6,
477 "north border mismatch at col {col}"
478 );
479 }
480 for col in 0..4u32 {
482 assert!(
483 (expanded.data[(5 * s + col + 1) as usize]
484 - src.data[(3 * 4 + col) as usize])
485 .abs()
486 < 1e-6,
487 "south border mismatch at col {col}"
488 );
489 }
490 for row in 0..4u32 {
492 assert!(
493 (expanded.data[((row + 1) * s) as usize]
494 - src.data[(row * 4) as usize])
495 .abs()
496 < 1e-6,
497 "west border mismatch at row {row}"
498 );
499 }
500 for row in 0..4u32 {
502 assert!(
503 (expanded.data[((row + 1) * s + 5) as usize]
504 - src.data[(row * 4 + 3) as usize])
505 .abs()
506 < 1e-6,
507 "east border mismatch at row {row}"
508 );
509 }
510 assert!((expanded.data[0] - src.data[0]).abs() < 1e-6);
512 assert!(
514 (expanded.data[(5 * s + 5) as usize] - src.data[(3 * 4 + 3) as usize]).abs()
515 < 1e-6
516 );
517 }
518
519 #[test]
522 fn patch_north_border() {
523 let tile = TileId::new(3, 4, 4);
524 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
525
526 let north_tile = TileId::new(3, 4, 3);
527 let neighbor = expand_with_clamped_border(&const_grid(north_tile, 4, 200.0));
528
529 patch_border_edge(&mut target, &neighbor, 0, -1);
530
531 let s = 6u32;
532 for col in 1..=4u32 {
534 let val = target.data[col as usize];
535 assert!(
536 (val - 200.0).abs() < 1e-6,
537 "north border at col {col}: expected 200, got {val}"
538 );
539 }
540 for col in 1..=4u32 {
542 let val = target.data[(5 * s + col) as usize];
543 assert!(
544 (val - 100.0).abs() < 1e-6,
545 "south border should still be 100 at col {col}, got {val}"
546 );
547 }
548 }
549
550 #[test]
551 fn patch_east_border() {
552 let tile = TileId::new(3, 4, 4);
553 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
554
555 let east_tile = TileId::new(3, 5, 4);
556 let neighbor = expand_with_clamped_border(&const_grid(east_tile, 4, 300.0));
557
558 patch_border_edge(&mut target, &neighbor, 1, 0);
559
560 let s = 6u32;
561 for row in 1..=4u32 {
563 let val = target.data[(row * s + 5) as usize];
564 assert!(
565 (val - 300.0).abs() < 1e-6,
566 "east border at row {row}: expected 300, got {val}"
567 );
568 }
569 }
570
571 #[test]
572 fn patch_nw_corner() {
573 let tile = TileId::new(3, 4, 4);
574 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
575
576 let nw_tile = TileId::new(3, 3, 3);
577 let mut nw_src = const_grid(nw_tile, 4, 500.0);
578 nw_src.data[(3 * 4 + 3) as usize] = 999.0;
580 let neighbor = expand_with_clamped_border(&nw_src);
581
582 patch_border_edge(&mut target, &neighbor, -1, -1);
583
584 assert!(
587 (target.data[0] - 300.0).abs() < 1e-6,
588 "NW corner should be clamped to 300.0, got {}",
589 target.data[0]
590 );
591 }
592
593 #[test]
594 fn patch_mismatched_dims_is_noop() {
595 let tile = TileId::new(3, 4, 4);
596 let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
597
598 let nb_tile = TileId::new(3, 5, 4);
600 let neighbor = expand_with_clamped_border(&const_grid(nb_tile, 8, 999.0));
601
602 patch_border_edge(&mut target, &neighbor, 1, 0);
603
604 let s = 6u32;
606 for row in 1..=4u32 {
607 let val = target.data[(row * s + 5) as usize];
608 assert!(
609 (val - 100.0).abs() < 1e-6,
610 "east border should still be 100 at row {row}, got {val}"
611 );
612 }
613 }
614
615 #[test]
618 fn patch_changed_two_horizontal_neighbours() {
619 let left = TileId::new(3, 3, 4);
620 let right = TileId::new(3, 4, 4);
621
622 let mut cache = HashMap::new();
623 cache.insert(left, expand_with_clamped_border(&const_grid(left, 4, 100.0)));
624 cache.insert(
625 right,
626 expand_with_clamped_border(&const_grid(right, 4, 200.0)),
627 );
628
629 let mut states = HashMap::new();
630 let changed: std::collections::HashSet<TileId> =
631 [left, right].iter().copied().collect();
632
633 let modified = patch_changed_tiles(&mut cache, &mut states, &changed);
634
635 assert!(modified.contains(&left));
637 assert!(modified.contains(&right));
638
639 let s = 6u32;
640 let left_grid = cache.get(&left).unwrap();
642 for row in 1..=4u32 {
643 let val = left_grid.data[(row * s + 5) as usize];
644 assert!(
645 (val - 200.0).abs() < 1e-6,
646 "left east border at row {row}: expected 200, got {val}"
647 );
648 }
649
650 let right_grid = cache.get(&right).unwrap();
652 for row in 1..=4u32 {
653 let val = right_grid.data[(row * s) as usize];
654 assert!(
655 (val - 100.0).abs() < 1e-6,
656 "right west border at row {row}: expected 100, got {val}"
657 );
658 }
659 }
660
661 #[test]
662 fn patch_changed_vertical_neighbours() {
663 let upper = TileId::new(3, 4, 3);
664 let lower = TileId::new(3, 4, 4);
665
666 let mut cache = HashMap::new();
667 cache.insert(
668 upper,
669 expand_with_clamped_border(&const_grid(upper, 4, 50.0)),
670 );
671 cache.insert(
672 lower,
673 expand_with_clamped_border(&const_grid(lower, 4, 150.0)),
674 );
675
676 let mut states = HashMap::new();
677 let changed: std::collections::HashSet<TileId> =
678 [upper, lower].iter().copied().collect();
679
680 patch_changed_tiles(&mut cache, &mut states, &changed);
681
682 let s = 6u32;
683 let upper_grid = cache.get(&upper).unwrap();
685 for col in 1..=4u32 {
686 let val = upper_grid.data[(5 * s + col) as usize];
687 assert!(
688 (val - 150.0).abs() < 1e-6,
689 "upper south border at col {col}: expected 150, got {val}"
690 );
691 }
692 let lower_grid = cache.get(&lower).unwrap();
694 for col in 1..=4u32 {
695 let val = lower_grid.data[col as usize];
696 assert!(
697 (val - 50.0).abs() < 1e-6,
698 "lower north border at col {col}: expected 50, got {val}"
699 );
700 }
701 }
702
703 #[test]
704 fn backfill_state_tracks_filled_directions() {
705 let mut state = BackfillState::default();
706 assert!(!state.is_filled(0));
707 state.mark(0);
708 assert!(state.is_filled(0));
709 state.reset();
710 assert!(!state.is_filled(0));
711 }
712
713 #[test]
714 fn incremental_patch_skips_already_filled() {
715 let left = TileId::new(3, 3, 4);
717 let right = TileId::new(3, 4, 4);
718
719 let mut cache = HashMap::new();
720 let mut states = HashMap::new();
721
722 cache.insert(left, expand_with_clamped_border(&const_grid(left, 4, 100.0)));
724 let changed1: std::collections::HashSet<TileId> = [left].iter().copied().collect();
725 patch_changed_tiles(&mut cache, &mut states, &changed1);
726
727 let s = 6u32;
729 let left_grid = cache.get(&left).unwrap();
730 for row in 1..=4u32 {
731 let val = left_grid.data[(row * s + 5) as usize];
732 assert!(
733 (val - 100.0).abs() < 1e-6,
734 "left east should be clamped 100 before right arrives, got {val}"
735 );
736 }
737
738 cache.insert(
740 right,
741 expand_with_clamped_border(&const_grid(right, 4, 200.0)),
742 );
743 let changed2: std::collections::HashSet<TileId> = [right].iter().copied().collect();
744 patch_changed_tiles(&mut cache, &mut states, &changed2);
745
746 let left_grid = cache.get(&left).unwrap();
748 for row in 1..=4u32 {
749 let val = left_grid.data[(row * s + 5) as usize];
750 assert!(
751 (val - 200.0).abs() < 1e-6,
752 "left east border should be 200 after right arrives, got {val}"
753 );
754 }
755
756 let right_grid = cache.get(&right).unwrap();
758 for row in 1..=4u32 {
759 let val = right_grid.data[(row * s) as usize];
760 assert!(
761 (val - 100.0).abs() < 1e-6,
762 "right west border should be 100, got {val}"
763 );
764 }
765 }
766
767 #[test]
770 fn neighbor_east() {
771 let tile = TileId::new(3, 4, 4);
772 let east = tile.neighbor(1, 0).expect("east");
773 assert_eq!(east, TileId::new(3, 5, 4));
774 }
775
776 #[test]
777 fn neighbor_west_wrap() {
778 let tile = TileId::new(3, 0, 4);
779 let west = tile.neighbor(-1, 0).expect("west wrap");
780 assert_eq!(west, TileId::new(3, 7, 4));
781 }
782
783 #[test]
784 fn neighbor_east_wrap() {
785 let tile = TileId::new(3, 7, 4);
786 let east = tile.neighbor(1, 0).expect("east wrap");
787 assert_eq!(east, TileId::new(3, 0, 4));
788 }
789
790 #[test]
791 fn neighbor_north_pole() {
792 let tile = TileId::new(3, 4, 0);
793 assert!(tile.neighbor(0, -1).is_none());
794 }
795
796 #[test]
797 fn neighbor_south_pole() {
798 let tile = TileId::new(3, 4, 7);
799 assert!(tile.neighbor(0, 1).is_none());
800 }
801}