use glam::IVec3;
use roxlap_formats::edit::{set_spans, Vspan};
use roxlap_formats::vxl::Vxl;
use crate::{Grid, CHUNK_SIZE_XY};
const CHUNK_EDIT_HEADROOM_PER_COLUMN: usize = 256;
pub(crate) fn empty_chunk_vxl() -> Vxl {
let vsid = CHUNK_SIZE_XY;
let n_cols = (vsid as usize) * (vsid as usize);
let mut data: Vec<u8> = Vec::with_capacity(n_cols * 8);
let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
for _ in 0..n_cols {
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
data.extend_from_slice(&[0, 0, 0, 0]); data.extend_from_slice(&[0, 0, 0, 0]); }
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
let mut vxl = Vxl {
vsid,
ipo: [0.0; 3],
ist: [1.0, 0.0, 0.0],
ihe: [0.0, 0.0, 1.0],
ifo: [0.0, 1.0, 0.0],
data: data.into_boxed_slice(),
column_offset: column_offset.into_boxed_slice(),
mip_base_offsets: Box::new([0, n_cols + 1]),
vbit: Box::new([]),
vbiti: 0,
};
vxl.reserve_edit_capacity(n_cols * CHUNK_EDIT_HEADROOM_PER_COLUMN);
let mut spans: Vec<Vspan> = Vec::with_capacity(n_cols);
for y in 0..vsid {
for x in 0..vsid {
spans.push(Vspan {
x,
y,
z0: 0,
z1: u8::MAX,
});
}
}
set_spans(&mut vxl, &spans, None);
vxl
}
impl Grid {
#[must_use]
pub fn chunk(&self, chunk_idx: IVec3) -> Option<&Vxl> {
self.chunks.get(&chunk_idx)
}
pub fn chunk_mut(&mut self, chunk_idx: IVec3) -> Option<&mut Vxl> {
self.chunks.get_mut(&chunk_idx)
}
pub fn ensure_chunk(&mut self, chunk_idx: IVec3) -> &mut Vxl {
self.chunks.entry(chunk_idx).or_insert_with(empty_chunk_vxl)
}
#[must_use]
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
#[must_use]
pub fn chunk_xy_backing(&self) -> Option<ChunkXyBacking<'_>> {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut any = false;
for chunk_idx in self.chunks.keys() {
if chunk_idx.z != 0 {
continue;
}
min_x = min_x.min(chunk_idx.x);
min_y = min_y.min(chunk_idx.y);
max_x = max_x.max(chunk_idx.x);
max_y = max_y.max(chunk_idx.y);
any = true;
}
if !any {
return None;
}
#[allow(clippy::cast_sign_loss)]
let chunks_x = (max_x - min_x + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_y = (max_y - min_y + 1) as u32;
let mut table: Vec<Option<roxlap_core::GridView<'_>>> =
vec![None; (chunks_x * chunks_y) as usize];
for (chunk_idx, vxl) in &self.chunks {
if chunk_idx.z != 0 {
continue;
}
let dx = chunk_idx.x - min_x;
let dy = chunk_idx.y - min_y;
#[allow(clippy::cast_sign_loss)]
let i = (dy as u32 * chunks_x + dx as u32) as usize;
table[i] = Some(roxlap_core::GridView::from_single_vxl(vxl));
}
Some(ChunkXyBacking {
chunks: table,
origin_chunk_xy: [min_x, min_y],
origin_chunk_z: 0,
chunks_x,
chunks_y,
chunks_z: 1,
})
}
#[must_use]
pub fn chunk_xyz_backing(&self) -> Option<ChunkXyBacking<'_>> {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut min_z = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
let mut max_z = i32::MIN;
let mut any = false;
for chunk_idx in self.chunks.keys() {
min_x = min_x.min(chunk_idx.x);
min_y = min_y.min(chunk_idx.y);
min_z = min_z.min(chunk_idx.z);
max_x = max_x.max(chunk_idx.x);
max_y = max_y.max(chunk_idx.y);
max_z = max_z.max(chunk_idx.z);
any = true;
}
if !any {
return None;
}
#[allow(clippy::cast_sign_loss)]
let chunks_x = (max_x - min_x + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_y = (max_y - min_y + 1) as u32;
#[allow(clippy::cast_sign_loss)]
let chunks_z = (max_z - min_z + 1) as u32;
let mut table: Vec<Option<roxlap_core::GridView<'_>>> =
vec![None; (chunks_x * chunks_y * chunks_z) as usize];
for (chunk_idx, vxl) in &self.chunks {
let dx = chunk_idx.x - min_x;
let dy = chunk_idx.y - min_y;
let dz = chunk_idx.z - min_z;
#[allow(clippy::cast_sign_loss)]
let (dx, dy, dz) = (dx as u32, dy as u32, dz as u32);
let i = ((dz * chunks_y + dy) * chunks_x + dx) as usize;
table[i] = Some(roxlap_core::GridView::from_single_vxl(vxl));
}
Some(ChunkXyBacking {
chunks: table,
origin_chunk_xy: [min_x, min_y],
origin_chunk_z: min_z,
chunks_x,
chunks_y,
chunks_z,
})
}
}
pub struct ChunkXyBacking<'a> {
pub chunks: Vec<Option<roxlap_core::GridView<'a>>>,
pub origin_chunk_xy: [i32; 2],
pub origin_chunk_z: i32,
pub chunks_x: u32,
pub chunks_y: u32,
pub chunks_z: u32,
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{GridTransform, CHUNK_SIZE_Z};
use roxlap_formats::edit::expandrle;
#[allow(clippy::cast_possible_wrap)]
pub(crate) fn voxel_is_solid(vxl: &Vxl, x: u32, y: u32, z: u32) -> bool {
let idx = (y * vxl.vsid + x) as usize;
let column = vxl.column_data(idx);
let maxzdim = CHUNK_SIZE_Z as i32;
let mut b2 = vec![maxzdim; 2 * (CHUNK_SIZE_Z as usize) + 4];
expandrle(column, &mut b2);
let z = z as i32;
let mut i = 0;
while b2[i] < maxzdim {
let top = b2[i];
let bot = b2[i + 1];
if z >= top && z < bot {
return true;
}
i += 2;
}
false
}
#[test]
fn empty_chunk_has_correct_vsid() {
let vxl = empty_chunk_vxl();
assert_eq!(vxl.vsid, CHUNK_SIZE_XY);
}
#[test]
fn empty_chunk_is_all_air() {
let vxl = empty_chunk_vxl();
for &(x, y, z) in &[
(0u32, 0u32, 0u32),
(0, 0, 100),
(0, 0, 200),
(CHUNK_SIZE_XY - 1, CHUNK_SIZE_XY - 1, 0),
(64, 64, 128),
] {
assert!(
!voxel_is_solid(&vxl, x, y, z),
"voxel ({x}, {y}, {z}) should be air"
);
}
}
#[test]
fn empty_chunk_air_above_bedrock_on_grid_sample() {
let vxl = empty_chunk_vxl();
let bedrock_z = CHUNK_SIZE_Z - 1;
for y in (0..CHUNK_SIZE_XY).step_by(16) {
for x in (0..CHUNK_SIZE_XY).step_by(16) {
for z in (0..bedrock_z).step_by(16) {
assert!(
!voxel_is_solid(&vxl, x, y, z),
"voxel ({x}, {y}, {z}) leaked solid in empty chunk"
);
}
assert!(voxel_is_solid(&vxl, x, y, bedrock_z));
}
}
}
#[test]
fn empty_chunk_keeps_bedrock_placeholder() {
let vxl = empty_chunk_vxl();
assert!(voxel_is_solid(&vxl, 0, 0, CHUNK_SIZE_Z - 1));
assert!(voxel_is_solid(&vxl, 64, 64, CHUNK_SIZE_Z - 1));
}
#[test]
fn ensure_chunk_creates_when_missing() {
let mut g = Grid::new(GridTransform::identity());
assert_eq!(g.chunk_count(), 0);
assert!(g.chunk(IVec3::ZERO).is_none());
let _ = g.ensure_chunk(IVec3::ZERO);
assert_eq!(g.chunk_count(), 1);
assert!(g.chunk(IVec3::ZERO).is_some());
}
#[test]
fn ensure_chunk_returns_existing() {
let mut g = Grid::new(GridTransform::identity());
let chunk = IVec3::new(2, -1, 0);
g.ensure_chunk(chunk);
g.set_voxel(IVec3::new(261, -122, 7), Some(0x80_aa_bb_cc));
let vxl = g.ensure_chunk(chunk);
assert!(voxel_is_solid(vxl, 5, 6, 7));
assert_eq!(g.chunk_count(), 1);
}
#[test]
fn chunk_mut_returns_none_for_missing() {
let mut g = Grid::new(GridTransform::identity());
assert!(g.chunk_mut(IVec3::ZERO).is_none());
}
#[test]
fn chunk_xy_backing_returns_chunks_z_one() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, 0));
g.ensure_chunk(IVec3::new(1, 0, 0));
g.ensure_chunk(IVec3::new(0, 0, 1));
let backing = g.chunk_xy_backing().expect("two chz=0 chunks present");
assert_eq!(backing.chunks_z, 1);
assert_eq!(backing.origin_chunk_z, 0);
assert_eq!(backing.chunks_x, 2);
assert_eq!(backing.chunks_y, 1);
assert_eq!(backing.chunks.len(), 2);
}
#[test]
fn chunk_xyz_backing_with_stacked_chunks_enumerates_all_z() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, 0));
g.ensure_chunk(IVec3::new(1, 0, 0));
g.ensure_chunk(IVec3::new(0, 0, 1));
let backing = g.chunk_xyz_backing().expect("at least one chunk");
assert_eq!(backing.chunks_x, 2);
assert_eq!(backing.chunks_y, 1);
assert_eq!(backing.chunks_z, 2);
assert_eq!(backing.origin_chunk_xy, [0, 0]);
assert_eq!(backing.origin_chunk_z, 0);
assert_eq!(backing.chunks.len(), 2 * 1 * 2);
assert!(backing.chunks[0].is_some(), "(0, 0, 0) present");
assert!(backing.chunks[1].is_some(), "(1, 0, 0) present");
assert!(backing.chunks[2].is_some(), "(0, 0, 1) present");
assert!(backing.chunks[3].is_none(), "(1, 0, 1) implicit-air");
}
#[test]
fn chunk_xyz_backing_with_negative_origin_chunk_z() {
let mut g = Grid::new(GridTransform::identity());
g.ensure_chunk(IVec3::new(0, 0, -2));
g.ensure_chunk(IVec3::new(0, 0, 0));
let backing = g.chunk_xyz_backing().expect("at least one chunk");
assert_eq!(backing.chunks_z, 3); assert_eq!(backing.origin_chunk_z, -2);
assert!(backing.chunks[0].is_some(), "chz=-2 → dz=0");
assert!(backing.chunks[1].is_none(), "chz=-1 → dz=1 implicit-air");
assert!(backing.chunks[2].is_some(), "chz=0 → dz=2");
}
}