#[derive(Debug, Clone)]
pub struct WalkableMask {
pub width: u16,
pub height: u16,
bits: Vec<bool>,
}
impl WalkableMask {
pub fn new_open(width: u16, height: u16) -> Self {
let total = (width as usize) * (height as usize);
Self {
width,
height,
bits: vec![true; total],
}
}
pub fn mark_blocked(&mut self, x: u16, y: u16, w: u16, h: u16, pad: u16) {
let min_x = x.saturating_sub(pad);
let max_x = x.saturating_add(w).saturating_add(pad).min(self.width);
let min_y = y.saturating_sub(pad);
let max_y = y.saturating_add(h).saturating_add(pad).min(self.height);
for yy in min_y..max_y {
let row = (yy as usize) * (self.width as usize);
for xx in min_x..max_x {
self.bits[row + (xx as usize)] = false;
}
}
}
pub fn mark_walkable(&mut self, x: u16, y: u16, w: u16, h: u16) {
let max_x = x.saturating_add(w).min(self.width);
let max_y = y.saturating_add(h).min(self.height);
for yy in y..max_y {
let row = (yy as usize) * (self.width as usize);
for xx in x..max_x {
self.bits[row + (xx as usize)] = true;
}
}
}
pub fn is_walkable(&self, x: u16, y: u16) -> bool {
if x >= self.width || y >= self.height {
return false;
}
let idx = (y as usize) * (self.width as usize) + (x as usize);
self.bits[idx]
}
}
#[derive(Debug, Clone, Default)]
pub struct OccupancyOverlay {
rects: Vec<(u16, u16, u16, u16)>,
}
impl OccupancyOverlay {
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.rects.clear();
}
pub fn add(&mut self, x: u16, y: u16, w: u16, h: u16) {
self.rects.push((x, y, w, h));
}
pub fn len(&self) -> usize {
self.rects.len()
}
pub fn is_empty(&self) -> bool {
self.rects.is_empty()
}
pub fn blocks(&self, x: u16, y: u16) -> bool {
self.rects.iter().any(|&(rx, ry, rw, rh)| {
x >= rx && x < rx.saturating_add(rw) && y >= ry && y < ry.saturating_add(rh)
})
}
pub fn signature(&self) -> u64 {
let mut sorted: Vec<(u16, u16, u16, u16)> = self.rects.clone();
sorted.sort_unstable();
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &(x, y, w, h) in &sorted {
for v in [x, y, w, h] {
hash ^= v as u64;
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
}
hash
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_open_is_all_walkable() {
let m = WalkableMask::new_open(8, 4);
for y in 0..4 {
for x in 0..8 {
assert!(m.is_walkable(x, y));
}
}
}
#[test]
fn mark_blocked_pads_and_clips() {
let mut m = WalkableMask::new_open(10, 10);
m.mark_blocked(4, 4, 2, 2, 1);
for y in 3..7 {
for x in 3..7 {
assert!(!m.is_walkable(x, y), "({x},{y}) should be blocked");
}
}
assert!(m.is_walkable(2, 4));
assert!(m.is_walkable(8, 4));
}
#[test]
fn mark_walkable_carves_a_cutout() {
let mut m = WalkableMask::new_open(10, 10);
m.mark_blocked(0, 0, 10, 4, 0);
assert!(!m.is_walkable(5, 2));
m.mark_walkable(4, 0, 3, 4);
assert!(m.is_walkable(5, 2));
}
#[test]
fn out_of_bounds_query_is_not_walkable() {
let m = WalkableMask::new_open(4, 4);
assert!(!m.is_walkable(4, 0));
assert!(!m.is_walkable(0, 4));
}
#[test]
fn overlay_blocks_inside_rects() {
let mut o = OccupancyOverlay::new();
o.add(10, 10, 5, 5);
assert!(o.blocks(12, 12));
assert!(!o.blocks(9, 10));
assert!(!o.blocks(15, 10));
}
#[test]
fn overlay_signature_changes_with_contents() {
let mut o = OccupancyOverlay::new();
let s_empty = o.signature();
o.add(1, 2, 3, 4);
let s_one = o.signature();
assert_ne!(s_empty, s_one);
o.clear();
assert_eq!(o.signature(), s_empty);
}
#[test]
fn overlay_signature_is_order_independent() {
let mut a = OccupancyOverlay::new();
a.add(10, 20, 5, 5);
a.add(30, 40, 8, 8);
let mut b = OccupancyOverlay::new();
b.add(30, 40, 8, 8);
b.add(10, 20, 5, 5);
assert_eq!(a.signature(), b.signature());
}
}