use half::f16;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use shortestpath::mesh_25d::Compact25D;
use shortestpath::mesh_source::{CellType, Source3D};
#[cfg(feature = "image")]
use shortestpath::mesh_source::Source3DFromImage;
use shortestpath::{Error as SpError, Gate, Mesh};
const NO_CELL: usize = usize::MAX;
pub const LEVEL_MAX_W: usize = 512;
pub const LEVEL_MAX_H: usize = 512;
pub const LEVEL_MAX_D: usize = 8;
pub const LEVEL_MAX_VOLUME: usize = 512 * 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QuadMeshError {
WidthExceeded { width: usize, max: usize },
HeightExceeded { height: usize, max: usize },
DepthExceeded { depth: usize, max: usize },
VolumeExceeded { volume: usize, max: usize },
ShortestPath(String),
}
impl std::fmt::Display for QuadMeshError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QuadMeshError::WidthExceeded { width, max } => {
write!(f, "Level width {} exceeds maximum {}", width, max)
}
QuadMeshError::HeightExceeded { height, max } => {
write!(f, "Level height {} exceeds maximum {}", height, max)
}
QuadMeshError::DepthExceeded { depth, max } => {
write!(f, "Level depth {} exceeds maximum {}", depth, max)
}
QuadMeshError::VolumeExceeded { volume, max } => {
write!(f, "Level volume {} exceeds maximum {}", volume, max)
}
QuadMeshError::ShortestPath(msg) => {
write!(f, "Shortestpath error: {}", msg)
}
}
}
}
impl std::error::Error for QuadMeshError {}
impl From<SpError> for QuadMeshError {
fn from(err: SpError) -> Self {
QuadMeshError::ShortestPath(err.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum QuadDirection {
NNE = 0,
NE = 1,
ENE = 2,
ESE = 3,
SE = 4,
SSE = 5,
SSW = 6,
SW = 7,
WSW = 8,
WNW = 9,
NW = 10,
NNW = 11,
UP = 12,
DOWN = 13,
}
impl QuadDirection {
pub const ALL: [QuadDirection; 14] = [
QuadDirection::NNE,
QuadDirection::NE,
QuadDirection::ENE,
QuadDirection::ESE,
QuadDirection::SE,
QuadDirection::SSE,
QuadDirection::SSW,
QuadDirection::SW,
QuadDirection::WSW,
QuadDirection::WNW,
QuadDirection::NW,
QuadDirection::NNW,
QuadDirection::UP,
QuadDirection::DOWN,
];
pub const HORIZONTAL: [QuadDirection; 12] = [
QuadDirection::NNE,
QuadDirection::NE,
QuadDirection::ENE,
QuadDirection::ESE,
QuadDirection::SE,
QuadDirection::SSE,
QuadDirection::SSW,
QuadDirection::SW,
QuadDirection::WSW,
QuadDirection::WNW,
QuadDirection::NW,
QuadDirection::NNW,
];
pub fn opposite(self) -> QuadDirection {
match self {
QuadDirection::NNE => QuadDirection::SSW,
QuadDirection::NE => QuadDirection::SW,
QuadDirection::ENE => QuadDirection::WSW,
QuadDirection::ESE => QuadDirection::WNW,
QuadDirection::SE => QuadDirection::NW,
QuadDirection::SSE => QuadDirection::NNW,
QuadDirection::SSW => QuadDirection::NNE,
QuadDirection::SW => QuadDirection::NE,
QuadDirection::WSW => QuadDirection::ENE,
QuadDirection::WNW => QuadDirection::ESE,
QuadDirection::NW => QuadDirection::SE,
QuadDirection::NNW => QuadDirection::SSE,
QuadDirection::UP => QuadDirection::DOWN,
QuadDirection::DOWN => QuadDirection::UP,
}
}
pub fn offset_2d(self) -> (i32, i32) {
match self {
QuadDirection::NNE => (1, -2), QuadDirection::NE => (1, -1), QuadDirection::ENE => (2, -1), QuadDirection::ESE => (2, 1), QuadDirection::SE => (1, 1), QuadDirection::SSE => (1, 2), QuadDirection::SSW => (-1, 2), QuadDirection::SW => (-1, 1), QuadDirection::WSW => (-2, 1), QuadDirection::WNW => (-2, -1), QuadDirection::NW => (-1, -1), QuadDirection::NNW => (-1, -2), QuadDirection::UP | QuadDirection::DOWN => (0, 0),
}
}
pub fn cardinal_fallback(self) -> (i32, i32) {
match self {
QuadDirection::NNE | QuadDirection::NNW => (0, -1),
QuadDirection::ENE | QuadDirection::ESE => (1, 0),
QuadDirection::SSE | QuadDirection::SSW => (0, 1),
QuadDirection::WSW | QuadDirection::WNW => (-1, 0),
QuadDirection::NE | QuadDirection::SE | QuadDirection::SW | QuadDirection::NW => {
self.offset_2d()
}
QuadDirection::UP | QuadDirection::DOWN => (0, 0),
}
}
pub fn offset_z(self) -> i32 {
match self {
QuadDirection::UP => -1,
QuadDirection::DOWN => 1,
_ => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct NeighborLink {
pub target_index: u32,
pub distance: f16,
pub dir_x: f16,
pub dir_y: f16,
pub dir_z: f16,
}
impl NeighborLink {
pub fn new(target_index: usize, distance: f32, dir_x: f32, dir_y: f32, dir_z: f32) -> Self {
Self {
target_index: target_index as u32,
distance: f16::from_f32(distance),
dir_x: f16::from_f32(dir_x),
dir_y: f16::from_f32(dir_y),
dir_z: f16::from_f32(dir_z),
}
}
pub fn from_gate(gate: &Gate, dir_x: f32, dir_y: f32, dir_z: f32) -> Self {
Self::new(gate.target(), gate.distance, dir_x, dir_y, dir_z)
}
#[inline]
pub fn target(&self) -> usize {
self.target_index as usize
}
pub fn direction(&self) -> (f32, f32, f32) {
(self.dir_x.to_f32(), self.dir_y.to_f32(), self.dir_z.to_f32())
}
pub fn distance_f32(&self) -> f32 {
self.distance.to_f32()
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct QuadCell {
pub x: u32,
pub y: u32,
pub z: u32,
pub size: u32,
pub neighbors: Vec<NeighborLink>,
}
impl QuadCell {
pub fn new_with_neighbors(x: u32, y: u32, z: u32, neighbors: Vec<NeighborLink>) -> Self {
Self {
x,
y,
z,
size: 1,
neighbors,
}
}
pub fn new_merged(x: u32, y: u32, z: u32, size: u32, neighbors: Vec<NeighborLink>) -> Self {
Self {
x,
y,
z,
size,
neighbors,
}
}
pub fn area(&self) -> u32 {
self.size * self.size
}
#[inline]
pub fn contains(&self, px: usize, py: usize, pz: usize) -> bool {
let x = self.x as usize;
let y = self.y as usize;
let z = self.z as usize;
let size = self.size as usize;
pz == z && px >= x && px < x + size && py >= y && py < y + size
}
pub fn center(&self) -> (f32, f32) {
let half = self.size as f32 / 2.0;
(self.x as f32 + half, self.y as f32 + half)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct QuadMesh {
cells: Vec<QuadCell>,
width: usize,
height: usize,
depth: usize,
coord_to_cell: Vec<usize>,
walkable_coords_cached: usize,
}
struct FnSource<F> {
width: usize,
height: usize,
depth: usize,
is_passable: F,
}
impl<F: Fn(usize, usize, usize) -> bool> Source3D for FnSource<F> {
fn get(&self, x: usize, y: usize, z: usize) -> Result<CellType, SpError> {
if x >= self.width || y >= self.height || z >= self.depth {
return Err(SpError::invalid_xyz(x, y, z));
}
Ok(if (self.is_passable)(x, y, z) {
CellType::FLOOR
} else {
CellType::WALL
})
}
fn width(&self) -> usize {
self.width
}
fn height(&self) -> usize {
self.height
}
fn depth(&self) -> usize {
self.depth
}
}
impl QuadMesh {
fn check_dimensions(width: usize, height: usize, depth: usize) -> Result<(), QuadMeshError> {
if width > LEVEL_MAX_W {
return Err(QuadMeshError::WidthExceeded {
width,
max: LEVEL_MAX_W,
});
}
if height > LEVEL_MAX_H {
return Err(QuadMeshError::HeightExceeded {
height,
max: LEVEL_MAX_H,
});
}
if depth > LEVEL_MAX_D {
return Err(QuadMeshError::DepthExceeded {
depth,
max: LEVEL_MAX_D,
});
}
Ok(())
}
fn check_volume(width: usize, height: usize, depth: usize) -> Result<(), QuadMeshError> {
let volume = width * height * depth;
if volume > LEVEL_MAX_VOLUME {
return Err(QuadMeshError::VolumeExceeded {
volume,
max: LEVEL_MAX_VOLUME,
});
}
Ok(())
}
pub fn new<F>(width: usize, height: usize, depth: usize, is_passable: F) -> Result<Self, QuadMeshError>
where
F: Fn(usize, usize, usize) -> bool,
{
use shortestpath::mesh_3d::Index3D;
Self::check_dimensions(width, height, depth)?;
Self::check_volume(width, height, depth)?;
let top_z = depth.saturating_sub(1);
let top_has_passable = (0..height)
.any(|y| (0..width).any(|x| is_passable(x, y, top_z)));
let final_depth = if top_has_passable { depth + 1 } else { depth };
let total = width * height * final_depth;
let wrapped_passable = |x: usize, y: usize, z: usize| {
if z >= depth {
false } else {
is_passable(x, y, z)
}
};
let source = FnSource {
width,
height,
depth: final_depth,
is_passable: wrapped_passable,
};
let base_mesh = Compact25D::from_source(&source)
.expect("Failed to create Compact25D")
.unique();
let mut cells = Vec::with_capacity(base_mesh.len());
let mut coord_to_cell = vec![NO_CELL; total];
for compact_idx in 0..base_mesh.len() {
let (x, y, z) = base_mesh
.index_to_xyz(compact_idx)
.expect("Invalid compact index");
let full_idx = z * width * height + y * width + x;
coord_to_cell[full_idx] = cells.len();
let successors: Vec<_> = base_mesh.successors(compact_idx, false).collect();
let mut neighbors = Vec::with_capacity(successors.len());
let cx = x as f32 + 0.5; let cy = y as f32 + 0.5;
let cz = z as f32;
for gate in successors {
let (nx, ny, nz) = base_mesh
.index_to_xyz(gate.target())
.expect("Invalid neighbor index");
let ncx = nx as f32 + 0.5;
let ncy = ny as f32 + 0.5;
let ncz = nz as f32;
let dx = ncx - cx;
let dy = ncy - cy;
let dz = ncz - cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
let (dir_x, dir_y, dir_z) = if dist > 0.0 {
(dx / dist, dy / dist, dz / dist)
} else {
(0.0, 0.0, 0.0)
};
neighbors.push(NeighborLink::from_gate(&gate, dir_x, dir_y, dir_z));
}
cells.push(QuadCell::new_with_neighbors(x as u32, y as u32, z as u32, neighbors));
}
let mut mesh = Self {
cells,
width,
height,
depth: final_depth,
coord_to_cell,
walkable_coords_cached: 0, };
mesh.compact();
mesh.walkable_coords_cached = mesh.coord_to_cell.iter().filter(|&&idx| idx != NO_CELL).count();
Ok(mesh)
}
const MAX_CELL_SIZE: u32 = 8;
fn compact(&mut self) {
let mut current_size = 1u32;
while current_size < Self::MAX_CELL_SIZE {
let next_size = current_size * 2;
self.compact_pass(current_size, next_size);
current_size = next_size;
}
self.rebuild_coord_mapping();
}
const MAX_VERTICAL_SIZE_RATIO: u32 = 2;
fn is_wall(&self, x: usize, y: usize, z: usize) -> bool {
if x >= self.width || y >= self.height || z >= self.depth {
return true; }
let idx = z * self.width * self.height + y * self.width + x;
self.coord_to_cell[idx] == NO_CELL
}
fn touches_wall_or_border(&self, x: usize, y: usize, z: usize, size: usize) -> bool {
if x == 0 || y == 0 || x + size >= self.width || y + size >= self.height {
return true;
}
for px in x..x + size {
if self.is_wall(px, y - 1, z) {
return true;
}
}
for px in x..x + size {
if self.is_wall(px, y + size, z) {
return true;
}
}
for py in y..y + size {
if self.is_wall(x - 1, py, z) {
return true;
}
}
for py in y..y + size {
if self.is_wall(x + size, py, z) {
return true;
}
}
false
}
fn compact_pass(&mut self, current_size: u32, next_size: u32) {
for z in 0..self.depth {
let mut y = 0;
while y + next_size as usize <= self.height {
let mut x = 0;
while x + next_size as usize <= self.width {
self.try_merge_group(x, y, z, current_size, next_size);
x += next_size as usize;
}
y += next_size as usize;
}
}
}
fn try_merge_group(
&mut self,
x: usize,
y: usize,
z: usize,
current_size: u32,
next_size: u32,
) {
if self.touches_wall_or_border(x, y, z, next_size as usize) {
return;
}
let step = current_size as usize;
let positions = [
(x, y),
(x + step, y),
(x, y + step),
(x + step, y + step),
];
let mut cell_indices = Vec::with_capacity(4);
for (px, py) in positions {
if let Some(idx) = self.cell_at(px, py, z) {
cell_indices.push(idx);
} else {
return; }
}
let first = &self.cells[cell_indices[0]];
if first.size != current_size {
return;
}
for &idx in &cell_indices[1..] {
let cell = &self.cells[idx];
if cell.size != current_size {
return; }
}
for &idx in &cell_indices {
for link in &self.cells[idx].neighbors {
let neighbor = &self.cells[link.target()];
if neighbor.z != z as u32 {
if next_size > neighbor.size * Self::MAX_VERTICAL_SIZE_RATIO {
return; }
}
}
}
let merged_idx = cell_indices[0];
let absorbed_indices: std::collections::HashSet<usize> =
cell_indices.iter().copied().collect();
let mut external_neighbors: Vec<NeighborLink> = Vec::new();
let mut seen_targets: std::collections::HashSet<usize> = std::collections::HashSet::new();
let merged_cx = x as f32 + next_size as f32 / 2.0;
let merged_cy = y as f32 + next_size as f32 / 2.0;
let merged_cz = z as f32;
for &cell_idx in &cell_indices {
for link in &self.cells[cell_idx].neighbors {
if absorbed_indices.contains(&link.target()) {
continue;
}
if seen_targets.contains(&link.target()) {
continue;
}
seen_targets.insert(link.target());
let target = &self.cells[link.target()];
let (tcx, tcy) = target.center();
let tcz = target.z as f32;
let dx = tcx - merged_cx;
let dy = tcy - merged_cy;
let dz = tcz - merged_cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
let (dir_x, dir_y, dir_z) = if dist > 0.0 {
(dx / dist, dy / dist, dz / dist)
} else {
(0.0, 0.0, 0.0)
};
external_neighbors.push(NeighborLink::new(link.target(), dist, dir_x, dir_y, dir_z));
}
}
let cells_to_update: std::collections::HashSet<usize> = external_neighbors
.iter()
.map(|link| link.target())
.collect();
for &cell_idx in &cells_to_update {
if cell_idx >= self.cells.len() {
continue;
}
let cell = &mut self.cells[cell_idx];
if cell.size == 0 {
continue;
}
let (cx, cy) = cell.center();
let cz = cell.z as f32;
for link in &mut cell.neighbors {
if absorbed_indices.contains(&link.target()) && link.target() != merged_idx {
link.target_index = merged_idx as u32;
let dx = merged_cx - cx;
let dy = merged_cy - cy;
let dz = merged_cz - cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist > 0.0 {
link.dir_x = f16::from_f32(dx / dist);
link.dir_y = f16::from_f32(dy / dist);
link.dir_z = f16::from_f32(dz / dist);
link.distance = f16::from_f32(dist);
}
}
}
let mut seen = std::collections::HashSet::new();
cell.neighbors.retain(|link| {
if seen.contains(&link.target_index) {
false
} else {
seen.insert(link.target_index);
true
}
});
}
self.cells[merged_idx] = QuadCell::new_merged(
x as u32,
y as u32,
z as u32,
next_size,
external_neighbors,
);
for &idx in &cell_indices[1..] {
self.cells[idx].size = 0;
self.cells[idx].neighbors.clear();
}
}
fn rebuild_coord_mapping(&mut self) {
let mut old_to_new: Vec<Option<usize>> = vec![None; self.cells.len()];
let mut new_idx = 0usize;
for (old_idx, cell) in self.cells.iter().enumerate() {
if cell.size > 0 {
old_to_new[old_idx] = Some(new_idx);
new_idx += 1;
}
}
let mut new_cells = Vec::with_capacity(new_idx);
for cell in &self.cells {
if cell.size > 0 {
let mut new_cell = cell.clone();
for link in &mut new_cell.neighbors {
if let Some(new_target) = old_to_new[link.target()] {
link.target_index = new_target as u32;
}
}
new_cell.neighbors.retain(|link| old_to_new.get(link.target()).copied().flatten().is_some() || link.target() < new_idx);
new_cells.push(new_cell);
}
}
self.cells = new_cells;
for val in &mut self.coord_to_cell {
*val = NO_CELL;
}
for (cell_idx, cell) in self.cells.iter().enumerate() {
let size = cell.size as usize;
for dz in 0..1 {
for dy in 0..size {
for dx in 0..size {
let x = cell.x as usize + dx;
let y = cell.y as usize + dy;
let z = cell.z as usize + dz;
if x < self.width && y < self.height && z < self.depth {
let coord_idx = z * self.width * self.height + y * self.width + x;
self.coord_to_cell[coord_idx] = cell_idx;
}
}
}
}
}
}
pub fn cell_count(&self) -> usize {
self.cells.len()
}
pub fn total_volume(&self) -> usize {
self.width * self.height * self.depth
}
pub fn walkable_coords(&self) -> usize {
self.walkable_coords_cached
}
pub fn cell(&self, index: usize) -> Option<&QuadCell> {
self.cells.get(index)
}
pub fn cell_at(&self, x: usize, y: usize, z: usize) -> Option<usize> {
if x < self.width && y < self.height && z < self.depth {
let idx = z * self.width * self.height + y * self.width + x;
let cell_idx = self.coord_to_cell[idx];
if cell_idx != NO_CELL {
Some(cell_idx)
} else {
None
}
} else {
None
}
}
pub fn dimensions(&self) -> (usize, usize, usize) {
(self.width, self.height, self.depth)
}
pub fn direction_to_neighbor(&self, from: usize, to: usize) -> Option<(f32, f32, f32)> {
self.link_to_neighbor(from, to).map(|link| link.direction())
}
pub fn link_to_neighbor(&self, from: usize, to: usize) -> Option<&NeighborLink> {
let cell = self.cells.get(from)?;
cell.neighbors.iter().find(|link| link.target() == to)
}
pub fn from_text(text: &str) -> Result<Self, QuadMeshError> {
Self::from_lines(text.lines())
}
pub fn from_slice(input: &[&str]) -> Result<Self, QuadMeshError> {
Self::from_lines(input.iter().copied())
}
pub fn from_strings(input: &[String]) -> Result<Self, QuadMeshError> {
Self::from_lines(input.iter().map(|s| s.as_str()))
}
fn is_sep_line(line: &str) -> bool {
if line.is_empty() {
return false;
}
let mut has_sep_char = false;
for chr in line.chars() {
if chr == '-' || chr == '_' || chr == '=' {
has_sep_char = true;
} else if chr != ' ' {
return false;
}
}
has_sep_char
}
pub fn from_lines<'a, I>(input: I) -> Result<Self, QuadMeshError>
where
I: Iterator<Item = &'a str>,
{
let mut layers: Vec<Vec<&str>> = Vec::new();
let mut current_layer: Vec<&str> = Vec::new();
for line in input {
if Self::is_sep_line(line) {
if !current_layer.is_empty() {
layers.push(current_layer);
current_layer = Vec::new();
}
} else {
current_layer.push(line);
}
}
if !current_layer.is_empty() {
layers.push(current_layer);
}
let depth = layers.len().max(1);
let height = layers.iter().map(|l| l.len()).max().unwrap_or(0);
let width = layers
.iter()
.flat_map(|l| l.iter())
.map(|line| line.len())
.max()
.unwrap_or(0);
Self::new(width, height, depth, |x, y, z| {
layers
.get(z)
.and_then(|layer| layer.get(y))
.and_then(|line| line.chars().nth(x))
.map(|c| c != '#')
.unwrap_or(false)
})
}
pub fn from_source(source: &impl Source3D) -> Result<Self, QuadMeshError> {
let width = source.width();
let height = source.height();
let depth = source.depth();
Self::new(width, height, depth, |x, y, z| {
source
.get(x, y, z)
.map(|cell| cell.can_go())
.unwrap_or(false)
})
}
#[cfg(feature = "image")]
pub fn from_paths<P: AsRef<std::path::Path>>(paths: &[P]) -> Result<Self, QuadMeshError> {
Self::from_paths_with_threshold(paths, Source3DFromImage::DEFAULT_THRESHOLD)
}
#[cfg(feature = "image")]
pub fn from_paths_with_threshold<P: AsRef<std::path::Path>>(
paths: &[P],
threshold: f32,
) -> Result<Self, QuadMeshError> {
let source = Source3DFromImage::from_paths(paths, threshold)?;
Self::from_source(&source)
}
#[cfg(feature = "image")]
pub fn from_images(images: &[image::DynamicImage]) -> Result<Self, QuadMeshError> {
Self::from_images_with_threshold(images, Source3DFromImage::DEFAULT_THRESHOLD)
}
#[cfg(feature = "image")]
pub fn from_images_with_threshold(
images: &[image::DynamicImage],
threshold: f32,
) -> Result<Self, QuadMeshError> {
let source = Source3DFromImage::from_images(images, threshold)?;
Self::from_source(&source)
}
}
impl Mesh for QuadMesh {
type IntoIter = std::vec::IntoIter<Gate>;
fn successors(&self, from: usize, backward: bool) -> Self::IntoIter {
let mut gates = Vec::new();
if let Some(cell) = self.cells.get(from) {
for link in &cell.neighbors {
gates.push(Gate::new(link.target(), link.distance_f32()));
}
}
if backward {
gates.reverse();
}
gates.into_iter()
}
fn len(&self) -> usize {
self.cells.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_direction_opposite() {
assert_eq!(QuadDirection::NE.opposite(), QuadDirection::SW);
assert_eq!(QuadDirection::UP.opposite(), QuadDirection::DOWN);
assert_eq!(QuadDirection::NNE.opposite(), QuadDirection::SSW);
}
#[test]
fn test_direction_offset() {
assert_eq!(QuadDirection::NE.offset_2d(), (1, -1));
assert_eq!(QuadDirection::SW.offset_2d(), (-1, 1));
assert_eq!(QuadDirection::NNE.offset_2d(), (1, -2));
assert_eq!(QuadDirection::UP.offset_2d(), (0, 0));
assert_eq!(QuadDirection::UP.offset_z(), -1);
assert_eq!(QuadDirection::DOWN.offset_z(), 1);
}
#[test]
fn test_quad_mesh_creation() {
let mesh = QuadMesh::new(4, 4, 1, |_, _, _| true).unwrap();
assert_eq!(mesh.cell_count(), 16); assert_eq!(mesh.dimensions(), (4, 4, 2));
}
#[test]
fn test_quad_mesh_with_walls() {
let mesh = QuadMesh::new(3, 3, 1, |x, y, _| !(x == 1 && y == 1)).unwrap();
assert_eq!(mesh.cell_count(), 8);
assert!(mesh.cell_at(1, 1, 0).is_none());
assert!(mesh.cell_at(0, 0, 0).is_some());
assert!(mesh.cell_at(2, 2, 0).is_some());
}
#[test]
fn test_neighbor_connections() {
let mesh = QuadMesh::new(5, 5, 1, |x, y, _| (x + y) % 2 == 0).unwrap();
let center = mesh.cell_at(2, 2, 0).unwrap();
let cell = mesh.cell(center).unwrap();
let neighbor_count = cell.neighbors.len();
assert!(neighbor_count > 0, "Cell should have neighbors");
use shortestpath::Mesh;
let successors: Vec<_> = mesh.successors(center, false).collect();
assert!(!successors.is_empty(), "Should have successors via Mesh trait");
for neighbor in &cell.neighbors {
assert!(
mesh.cell(neighbor.target()).is_some(),
"Neighbor target should be a valid cell"
);
}
}
#[test]
fn test_compaction_uniform_4x4() {
let mesh = QuadMesh::new(4, 4, 1, |_, _, _| true).unwrap();
assert_eq!(mesh.cell_count(), 16); }
#[test]
fn test_compaction_uniform_8x8() {
let mesh = QuadMesh::new(8, 8, 1, |_, _, _| true).unwrap();
assert_eq!(mesh.cell_count(), 52);
let interior_cell = mesh.cell_at(2, 2, 0).unwrap();
assert_eq!(mesh.cell(interior_cell).unwrap().size, 2);
}
#[test]
fn test_compaction_with_obstacle() {
let mesh = QuadMesh::new(4, 4, 1, |x, y, _| !(x == 0 && y == 0)).unwrap();
assert!(mesh.cell_count() > 1);
assert!(mesh.cell_count() < 16);
}
#[test]
fn test_compaction_checkerboard() {
let mesh = QuadMesh::new(4, 4, 1, |x, y, _| (x + y) % 2 == 0).unwrap();
assert_eq!(mesh.cell_count(), 8);
}
#[test]
fn test_compaction_preserves_coord_lookup() {
let mesh = QuadMesh::new(8, 8, 1, |_, _, _| true).unwrap();
for y in 0..8 {
for x in 0..8 {
let idx = mesh.cell_at(x, y, 0);
assert!(idx.is_some(), "Expected cell at ({}, {})", x, y);
}
}
let idx_2_2 = mesh.cell_at(2, 2, 0).unwrap();
let idx_2_3 = mesh.cell_at(2, 3, 0).unwrap();
let idx_3_2 = mesh.cell_at(3, 2, 0).unwrap();
let idx_3_3 = mesh.cell_at(3, 3, 0).unwrap();
assert_eq!(idx_2_2, idx_2_3);
assert_eq!(idx_2_2, idx_3_2);
assert_eq!(idx_2_2, idx_3_3);
}
#[test]
fn test_euclidean_distance_calculation() {
let mesh = QuadMesh::new(3, 1, 1, |_, _, _| true).unwrap();
let cell_0 = mesh.cell_at(0, 0, 0).unwrap();
let _cell_2 = mesh.cell_at(2, 0, 0).unwrap();
let cell = mesh.cell(cell_0).unwrap();
assert!(!cell.neighbors.is_empty(), "Cell should have neighbors");
for link in &cell.neighbors {
let dist = link.distance_f32();
assert!(dist > 0.0 && dist < 3.0, "Distance should be reasonable");
}
}
#[test]
fn test_mesh_trait_with_gradient() {
use shortestpath::Gradient;
let mesh = QuadMesh::new(5, 5, 1, |x, y, _| !(x == 2 && y == 2)).unwrap();
let mut gradient = Gradient::from_mesh(&mesh);
let source_idx = mesh.cell_at(0, 0, 0).unwrap();
gradient.set_distance(source_idx, 0.0);
gradient.spread(&mesh);
for i in 0..mesh.cell_count() {
let dist = gradient.get_distance(i);
assert!(dist < f32::INFINITY, "Cell {} should be reachable", i);
}
}
#[test]
fn test_mesh_trait_compacted() {
use shortestpath::Gradient;
let mesh = QuadMesh::new(8, 8, 1, |_, _, _| true).unwrap();
assert_eq!(mesh.cell_count(), 52);
let mut gradient = Gradient::from_mesh(&mesh);
let source = mesh.cell_at(0, 0, 0).unwrap();
gradient.set_distance(source, 0.0);
gradient.spread(&mesh);
for i in 0..mesh.cell_count() {
assert!(gradient.get_distance(i) < f32::INFINITY);
}
}
#[test]
fn test_connection_reciprocity() {
let mesh = QuadMesh::new(10, 10, 1, |x, y, _| {
!(x == 5 && y < 8)
})
.unwrap();
for cell_idx in 0..mesh.cell_count() {
let cell = mesh.cell(cell_idx).unwrap();
for link in &cell.neighbors {
let neighbor_idx = link.target();
let neighbor = mesh.cell(neighbor_idx).unwrap();
let has_back_link = neighbor.neighbors.iter().any(|l| l.target() == cell_idx);
assert!(
has_back_link,
"Cell {} links to cell {}, but cell {} has no link back",
cell_idx, neighbor_idx, neighbor_idx
);
}
}
}
#[test]
fn test_compaction_large_interior() {
let mesh = QuadMesh::new(16, 16, 1, |_, _, _| true).unwrap();
assert!(mesh.cell_count() < 256, "Should have some compaction");
assert!(mesh.cell_count() > 4, "Borders prevent full compaction");
let interior_cell = mesh.cell_at(4, 4, 0).unwrap();
let cell = mesh.cell(interior_cell).unwrap();
assert!(cell.size >= 2, "Interior should compact to at least 2x2");
}
#[test]
fn test_from_text() {
let text = "...\n.#.\n...";
let mesh = QuadMesh::from_text(text).unwrap();
assert_eq!(mesh.dimensions(), (3, 3, 2));
assert_eq!(mesh.cell_count(), 8);
assert!(mesh.cell_at(1, 1, 0).is_none());
}
#[test]
fn test_vertical_link_size_constraint() {
let mesh = QuadMesh::new(16, 16, 2, |x, y, z| {
if z == 0 {
!(x == 5 && y < 12) && !(y == 5 && x < 12)
} else {
true
}
})
.unwrap();
let mut vertical_link_count = 0;
for (cell_idx, cell) in mesh.cells.iter().enumerate() {
for link in &cell.neighbors {
let neighbor = mesh.cell(link.target()).unwrap();
if neighbor.z != cell.z {
vertical_link_count += 1;
let (larger, smaller) = if cell.size >= neighbor.size {
(cell.size, neighbor.size)
} else {
(neighbor.size, cell.size)
};
assert!(
larger <= smaller * 2,
"Cell {} (size {}) has vertical link to cell {} (size {}), ratio {} > 2",
cell_idx,
cell.size,
link.target(),
neighbor.size,
larger / smaller
);
}
}
}
assert!(
vertical_link_count > 0,
"Should have vertical links between layers"
);
}
#[test]
fn test_ground_layer_added_when_top_has_walkable() {
let mesh = QuadMesh::new(3, 3, 1, |_, _, _| true).unwrap();
assert_eq!(mesh.depth, 2, "Should add ground layer when top has walkable cells");
for y in 0..3 {
for x in 0..3 {
assert!(mesh.cell_at(x, y, 1).is_none(), "Ceiling should be all walls");
}
}
assert!(mesh.cell_at(0, 0, 0).is_some(), "Original layer should have cells");
}
#[test]
fn test_ground_layer_not_added_when_top_is_walls() {
let mesh = QuadMesh::new(3, 3, 2, |_, _, z| z == 0).unwrap();
assert_eq!(mesh.depth, 2, "Should not add ground when top is already walls");
}
#[test]
fn test_ground_layer_idempotent() {
let mesh1 = QuadMesh::new(3, 3, 1, |_, _, _| true).unwrap();
assert_eq!(mesh1.depth, 2);
let mesh2 = QuadMesh::new(3, 3, 2, |_, _, z| z == 0).unwrap();
assert_eq!(mesh2.depth, 2, "Ceiling layer addition should be idempotent");
assert_eq!(mesh1.depth, mesh2.depth);
assert_eq!(mesh1.cell_count(), mesh2.cell_count());
}
#[test]
fn test_volume_limit_checked_before_ground_layer() {
let result = QuadMesh::new(512, 512, 1, |_, _, _| true);
assert!(result.is_ok(), "512x512x1 should be valid (volume check before ground)");
let mesh = result.unwrap();
assert_eq!(mesh.depth, 2, "Should add ground layer");
assert_eq!(mesh.dimensions(), (512, 512, 2));
}
#[test]
fn test_volume_limit_exceeded() {
let result = QuadMesh::new(512, 512, 2, |_, _, _| true);
assert!(result.is_err(), "512x512x2 should exceed volume limit");
match result {
Err(QuadMeshError::VolumeExceeded { volume, max }) => {
assert_eq!(volume, 512 * 512 * 2);
assert_eq!(max, LEVEL_MAX_VOLUME);
}
_ => panic!("Expected VolumeExceeded error"),
}
}
#[test]
fn test_max_dimensions_512x512() {
let result = QuadMesh::new(512, 512, 1, |_, _, _| true);
assert!(result.is_ok(), "512x512x1 should be valid");
let result = QuadMesh::new(513, 1, 1, |_, _, _| true);
assert!(matches!(result, Err(QuadMeshError::WidthExceeded { .. })));
let result = QuadMesh::new(1, 513, 1, |_, _, _| true);
assert!(matches!(result, Err(QuadMeshError::HeightExceeded { .. })));
}
#[test]
fn test_volume_128x256x8() {
let result = QuadMesh::new(128, 256, 8, |_, _, z| z < 7);
assert!(result.is_ok(), "128x256x8 should be valid (volume = 262144)");
let mesh = result.unwrap();
assert_eq!(mesh.dimensions(), (128, 256, 8));
}
#[test]
fn test_volume_220x220x6_exceeds() {
let result = QuadMesh::new(220, 220, 6, |_, _, _| true);
assert!(result.is_err(), "220x220x6 should exceed volume limit");
match result {
Err(QuadMeshError::VolumeExceeded { volume, max }) => {
assert_eq!(volume, 220 * 220 * 6);
assert_eq!(max, LEVEL_MAX_VOLUME);
}
_ => panic!("Expected VolumeExceeded error"),
}
}
#[test]
fn test_width_600x200x1_exceeds() {
let result = QuadMesh::new(600, 200, 1, |_, _, _| true);
assert!(result.is_err(), "600x200x1 should exceed width limit");
match result {
Err(QuadMeshError::WidthExceeded { width, max }) => {
assert_eq!(width, 600);
assert_eq!(max, LEVEL_MAX_W);
}
_ => panic!("Expected WidthExceeded error"),
}
}
#[test]
fn test_height_200x600x1_exceeds() {
let result = QuadMesh::new(200, 600, 1, |_, _, _| true);
assert!(result.is_err(), "200x600x1 should exceed height limit");
match result {
Err(QuadMeshError::HeightExceeded { height, max }) => {
assert_eq!(height, 600);
assert_eq!(max, LEVEL_MAX_H);
}
_ => panic!("Expected HeightExceeded error"),
}
}
#[test]
fn test_depth_100x100x9_exceeds() {
let result = QuadMesh::new(100, 100, 9, |_, _, _| true);
assert!(result.is_err(), "100x100x9 should exceed depth limit");
match result {
Err(QuadMeshError::DepthExceeded { depth, max }) => {
assert_eq!(depth, 9);
assert_eq!(max, LEVEL_MAX_D);
}
_ => panic!("Expected DepthExceeded error"),
}
}
}