use rustial_math::{ElevationGrid, TileId};
use std::collections::HashMap;
#[inline]
pub fn interior_dims(grid: &ElevationGrid) -> (u32, u32) {
(grid.width.saturating_sub(2), grid.height.saturating_sub(2))
}
pub fn expand_with_clamped_border(src: &ElevationGrid) -> ElevationGrid {
let w = src.width;
let h = src.height;
let new_w = w + 2;
let new_h = h + 2;
let mut data = vec![0.0f32; (new_w * new_h) as usize];
for row in 0..h {
let src_start = (row * w) as usize;
let dst_start = ((row + 1) * new_w + 1) as usize;
data[dst_start..dst_start + w as usize]
.copy_from_slice(&src.data[src_start..src_start + w as usize]);
}
for col in 0..w {
data[(col + 1) as usize] = src.data[col as usize];
}
for col in 0..w {
data[((h + 1) * new_w + col + 1) as usize] = src.data[((h - 1) * w + col) as usize];
}
for row in 0..h {
data[((row + 1) * new_w) as usize] = src.data[(row * w) as usize];
}
for row in 0..h {
data[((row + 1) * new_w + w + 1) as usize] = src.data[(row * w + w - 1) as usize];
}
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 {
width: new_w,
height: new_h,
min_elev: src.min_elev,
max_elev: src.max_elev,
data,
tile: src.tile,
}
}
pub fn patch_border_edge(target: &mut ElevationGrid, neighbor: &ElevationGrid, dx: i32, dy: i32) {
let (tw, th) = interior_dims(target);
let (nw, nh) = interior_dims(neighbor);
if tw != nw || th != nh || tw == 0 || th == 0 {
return;
}
let stride = target.width;
let interior_range = target.max_elev - target.min_elev;
let margin = (interior_range * 0.5).max(200.0);
let clamp_lo = target.min_elev - margin;
let clamp_hi = target.max_elev + margin;
let mut write = |idx: usize, val: f32| {
target.data[idx] = val.clamp(clamp_lo, clamp_hi);
};
match (dx, dy) {
(0, -1) => {
let src_row = nh;
for col in 0..tw {
let src_idx = (src_row * stride + col + 1) as usize;
let dst_idx = (col + 1) as usize; write(dst_idx, neighbor.data[src_idx]);
}
}
(0, 1) => {
let src_row = 1u32;
let dst_row = th + 1;
for col in 0..tw {
let src_idx = (src_row * stride + col + 1) as usize;
let dst_idx = (dst_row * stride + col + 1) as usize;
write(dst_idx, neighbor.data[src_idx]);
}
}
(-1, 0) => {
let src_col = nw;
for row in 0..th {
let src_idx = ((row + 1) * stride + src_col) as usize;
let dst_idx = ((row + 1) * stride) as usize; write(dst_idx, neighbor.data[src_idx]);
}
}
(1, 0) => {
let src_col = 1u32;
let dst_col = tw + 1;
for row in 0..th {
let src_idx = ((row + 1) * stride + src_col) as usize;
let dst_idx = ((row + 1) * stride + dst_col) as usize;
write(dst_idx, neighbor.data[src_idx]);
}
}
(-1, -1) => {
let src_idx = (nh * stride + nw) as usize;
write(0, neighbor.data[src_idx]);
}
(1, -1) => {
let src_idx = (nh * stride + 1) as usize;
write((tw + 1) as usize, neighbor.data[src_idx]);
}
(-1, 1) => {
let src_idx = (stride + nw) as usize;
write(((th + 1) * stride) as usize, neighbor.data[src_idx]);
}
(1, 1) => {
let src_idx = (stride + 1) as usize;
write(
((th + 1) * stride + tw + 1) as usize,
neighbor.data[src_idx],
);
}
_ => {} }
}
pub const NEIGHBOR_OFFSETS: [(i32, i32); 8] = [
(0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (1, -1), (-1, 1), (1, 1), ];
#[derive(Debug, Clone, Default)]
pub struct BackfillState {
pub filled: [bool; 8],
}
impl BackfillState {
pub fn reset(&mut self) {
self.filled = [false; 8];
}
#[inline]
pub fn mark(&mut self, offset_index: usize) {
if offset_index < 8 {
self.filled[offset_index] = true;
}
}
#[inline]
pub fn is_filled(&self, offset_index: usize) -> bool {
offset_index < 8 && self.filled[offset_index]
}
}
pub fn patch_changed_tiles(
cache: &mut HashMap<TileId, ElevationGrid>,
backfill_states: &mut HashMap<TileId, BackfillState>,
changed: &std::collections::HashSet<TileId>,
) -> std::collections::HashSet<TileId> {
let mut modified = std::collections::HashSet::new();
struct PatchOp {
target: TileId,
neighbor: TileId,
offset_index: usize,
dx: i32,
dy: i32,
}
let mut ops: Vec<PatchOp> = Vec::new();
for &tile in changed {
backfill_states.entry(tile).or_default().reset();
}
for &tile in changed {
for (idx, &(dx, dy)) in NEIGHBOR_OFFSETS.iter().enumerate() {
if let Some(nb) = tile.neighbor(dx, dy) {
if cache.contains_key(&nb) {
ops.push(PatchOp {
target: tile,
neighbor: nb,
offset_index: idx,
dx,
dy,
});
}
}
}
for &(dx, dy) in NEIGHBOR_OFFSETS.iter() {
if let Some(nb) = tile.neighbor(dx, dy) {
if cache.contains_key(&nb) {
let rev_idx = NEIGHBOR_OFFSETS
.iter()
.position(|&(rdx, rdy)| rdx == -dx && rdy == -dy);
if let Some(ri) = rev_idx {
let nb_state = backfill_states.entry(nb).or_default();
if !nb_state.is_filled(ri) {
ops.push(PatchOp {
target: nb,
neighbor: tile,
offset_index: ri,
dx: -dx,
dy: -dy,
});
}
}
}
}
}
}
for op in &ops {
if op.target == op.neighbor {
continue;
}
let neighbor_grid = cache.get(&op.neighbor).cloned();
if let Some(ref nb_grid) = neighbor_grid {
if let Some(target_grid) = cache.get_mut(&op.target) {
patch_border_edge(target_grid, nb_grid, op.dx, op.dy);
backfill_states
.entry(op.target)
.or_default()
.mark(op.offset_index);
modified.insert(op.target);
}
}
}
modified
}
#[cfg(test)]
mod tests {
use super::*;
fn const_grid(tile: TileId, size: u32, value: f32) -> ElevationGrid {
ElevationGrid::from_data(tile, size, size, vec![value; (size * size) as usize])
.expect("valid grid")
}
fn sequential_grid(tile: TileId, size: u32, base_val: f32) -> ElevationGrid {
let data: Vec<f32> = (0..size * size).map(|i| base_val + i as f32).collect();
ElevationGrid::from_data(tile, size, size, data).expect("valid grid")
}
#[test]
fn expand_produces_correct_dimensions() {
let tile = TileId::new(3, 4, 4);
let src = const_grid(tile, 4, 100.0);
let expanded = expand_with_clamped_border(&src);
assert_eq!(expanded.width, 6);
assert_eq!(expanded.height, 6);
assert_eq!(interior_dims(&expanded), (4, 4));
}
#[test]
fn expand_interior_unchanged() {
let tile = TileId::new(3, 4, 4);
let src = sequential_grid(tile, 4, 0.0);
let expanded = expand_with_clamped_border(&src);
for row in 0..4u32 {
for col in 0..4u32 {
let orig = src.data[(row * 4 + col) as usize];
let exp = expanded.data[((row + 1) * 6 + (col + 1)) as usize];
assert!(
(orig - exp).abs() < 1e-6,
"interior mismatch at ({col},{row}): {orig} vs {exp}"
);
}
}
}
#[test]
fn expand_borders_clamp_to_nearest_interior() {
let tile = TileId::new(3, 4, 4);
let src = sequential_grid(tile, 4, 0.0);
let expanded = expand_with_clamped_border(&src);
let s = 6u32;
for col in 0..4u32 {
assert!(
(expanded.data[(col + 1) as usize] - src.data[col as usize]).abs() < 1e-6,
"north border mismatch at col {col}"
);
}
for col in 0..4u32 {
assert!(
(expanded.data[(5 * s + col + 1) as usize] - src.data[(3 * 4 + col) as usize])
.abs()
< 1e-6,
"south border mismatch at col {col}"
);
}
for row in 0..4u32 {
assert!(
(expanded.data[((row + 1) * s) as usize] - src.data[(row * 4) as usize]).abs()
< 1e-6,
"west border mismatch at row {row}"
);
}
for row in 0..4u32 {
assert!(
(expanded.data[((row + 1) * s + 5) as usize] - src.data[(row * 4 + 3) as usize])
.abs()
< 1e-6,
"east border mismatch at row {row}"
);
}
assert!((expanded.data[0] - src.data[0]).abs() < 1e-6);
assert!(
(expanded.data[(5 * s + 5) as usize] - src.data[(3 * 4 + 3) as usize]).abs() < 1e-6
);
}
#[test]
fn patch_north_border() {
let tile = TileId::new(3, 4, 4);
let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
let north_tile = TileId::new(3, 4, 3);
let neighbor = expand_with_clamped_border(&const_grid(north_tile, 4, 200.0));
patch_border_edge(&mut target, &neighbor, 0, -1);
let s = 6u32;
for col in 1..=4u32 {
let val = target.data[col as usize];
assert!(
(val - 200.0).abs() < 1e-6,
"north border at col {col}: expected 200, got {val}"
);
}
for col in 1..=4u32 {
let val = target.data[(5 * s + col) as usize];
assert!(
(val - 100.0).abs() < 1e-6,
"south border should still be 100 at col {col}, got {val}"
);
}
}
#[test]
fn patch_east_border() {
let tile = TileId::new(3, 4, 4);
let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
let east_tile = TileId::new(3, 5, 4);
let neighbor = expand_with_clamped_border(&const_grid(east_tile, 4, 300.0));
patch_border_edge(&mut target, &neighbor, 1, 0);
let s = 6u32;
for row in 1..=4u32 {
let val = target.data[(row * s + 5) as usize];
assert!(
(val - 300.0).abs() < 1e-6,
"east border at row {row}: expected 300, got {val}"
);
}
}
#[test]
fn patch_nw_corner() {
let tile = TileId::new(3, 4, 4);
let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
let nw_tile = TileId::new(3, 3, 3);
let mut nw_src = const_grid(nw_tile, 4, 500.0);
nw_src.data[(3 * 4 + 3) as usize] = 999.0;
let neighbor = expand_with_clamped_border(&nw_src);
patch_border_edge(&mut target, &neighbor, -1, -1);
assert!(
(target.data[0] - 300.0).abs() < 1e-6,
"NW corner should be clamped to 300.0, got {}",
target.data[0]
);
}
#[test]
fn patch_mismatched_dims_is_noop() {
let tile = TileId::new(3, 4, 4);
let mut target = expand_with_clamped_border(&const_grid(tile, 4, 100.0));
let nb_tile = TileId::new(3, 5, 4);
let neighbor = expand_with_clamped_border(&const_grid(nb_tile, 8, 999.0));
patch_border_edge(&mut target, &neighbor, 1, 0);
let s = 6u32;
for row in 1..=4u32 {
let val = target.data[(row * s + 5) as usize];
assert!(
(val - 100.0).abs() < 1e-6,
"east border should still be 100 at row {row}, got {val}"
);
}
}
#[test]
fn patch_changed_two_horizontal_neighbours() {
let left = TileId::new(3, 3, 4);
let right = TileId::new(3, 4, 4);
let mut cache = HashMap::new();
cache.insert(
left,
expand_with_clamped_border(&const_grid(left, 4, 100.0)),
);
cache.insert(
right,
expand_with_clamped_border(&const_grid(right, 4, 200.0)),
);
let mut states = HashMap::new();
let changed: std::collections::HashSet<TileId> = [left, right].iter().copied().collect();
let modified = patch_changed_tiles(&mut cache, &mut states, &changed);
assert!(modified.contains(&left));
assert!(modified.contains(&right));
let s = 6u32;
let left_grid = cache.get(&left).unwrap();
for row in 1..=4u32 {
let val = left_grid.data[(row * s + 5) as usize];
assert!(
(val - 200.0).abs() < 1e-6,
"left east border at row {row}: expected 200, got {val}"
);
}
let right_grid = cache.get(&right).unwrap();
for row in 1..=4u32 {
let val = right_grid.data[(row * s) as usize];
assert!(
(val - 100.0).abs() < 1e-6,
"right west border at row {row}: expected 100, got {val}"
);
}
}
#[test]
fn patch_changed_vertical_neighbours() {
let upper = TileId::new(3, 4, 3);
let lower = TileId::new(3, 4, 4);
let mut cache = HashMap::new();
cache.insert(
upper,
expand_with_clamped_border(&const_grid(upper, 4, 50.0)),
);
cache.insert(
lower,
expand_with_clamped_border(&const_grid(lower, 4, 150.0)),
);
let mut states = HashMap::new();
let changed: std::collections::HashSet<TileId> = [upper, lower].iter().copied().collect();
patch_changed_tiles(&mut cache, &mut states, &changed);
let s = 6u32;
let upper_grid = cache.get(&upper).unwrap();
for col in 1..=4u32 {
let val = upper_grid.data[(5 * s + col) as usize];
assert!(
(val - 150.0).abs() < 1e-6,
"upper south border at col {col}: expected 150, got {val}"
);
}
let lower_grid = cache.get(&lower).unwrap();
for col in 1..=4u32 {
let val = lower_grid.data[col as usize];
assert!(
(val - 50.0).abs() < 1e-6,
"lower north border at col {col}: expected 50, got {val}"
);
}
}
#[test]
fn backfill_state_tracks_filled_directions() {
let mut state = BackfillState::default();
assert!(!state.is_filled(0));
state.mark(0);
assert!(state.is_filled(0));
state.reset();
assert!(!state.is_filled(0));
}
#[test]
fn incremental_patch_skips_already_filled() {
let left = TileId::new(3, 3, 4);
let right = TileId::new(3, 4, 4);
let mut cache = HashMap::new();
let mut states = HashMap::new();
cache.insert(
left,
expand_with_clamped_border(&const_grid(left, 4, 100.0)),
);
let changed1: std::collections::HashSet<TileId> = [left].iter().copied().collect();
patch_changed_tiles(&mut cache, &mut states, &changed1);
let s = 6u32;
let left_grid = cache.get(&left).unwrap();
for row in 1..=4u32 {
let val = left_grid.data[(row * s + 5) as usize];
assert!(
(val - 100.0).abs() < 1e-6,
"left east should be clamped 100 before right arrives, got {val}"
);
}
cache.insert(
right,
expand_with_clamped_border(&const_grid(right, 4, 200.0)),
);
let changed2: std::collections::HashSet<TileId> = [right].iter().copied().collect();
patch_changed_tiles(&mut cache, &mut states, &changed2);
let left_grid = cache.get(&left).unwrap();
for row in 1..=4u32 {
let val = left_grid.data[(row * s + 5) as usize];
assert!(
(val - 200.0).abs() < 1e-6,
"left east border should be 200 after right arrives, got {val}"
);
}
let right_grid = cache.get(&right).unwrap();
for row in 1..=4u32 {
let val = right_grid.data[(row * s) as usize];
assert!(
(val - 100.0).abs() < 1e-6,
"right west border should be 100, got {val}"
);
}
}
#[test]
fn neighbor_east() {
let tile = TileId::new(3, 4, 4);
let east = tile.neighbor(1, 0).expect("east");
assert_eq!(east, TileId::new(3, 5, 4));
}
#[test]
fn neighbor_west_wrap() {
let tile = TileId::new(3, 0, 4);
let west = tile.neighbor(-1, 0).expect("west wrap");
assert_eq!(west, TileId::new(3, 7, 4));
}
#[test]
fn neighbor_east_wrap() {
let tile = TileId::new(3, 7, 4);
let east = tile.neighbor(1, 0).expect("east wrap");
assert_eq!(east, TileId::new(3, 0, 4));
}
#[test]
fn neighbor_north_pole() {
let tile = TileId::new(3, 4, 0);
assert!(tile.neighbor(0, -1).is_none());
}
#[test]
fn neighbor_south_pole() {
let tile = TileId::new(3, 4, 7);
assert!(tile.neighbor(0, 1).is_none());
}
}