use glam::IVec3;
use roxlap_formats::edit::{set_cube, set_rect, set_sphere};
use crate::addr::{voxel_split, GridLocalPos};
use crate::{Grid, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
#[inline]
fn chunk_size_ivec3() -> IVec3 {
#[allow(clippy::cast_possible_wrap)]
IVec3::new(
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_Z as i32,
)
}
impl Grid {
pub fn set_voxel(&mut self, voxel: IVec3, color: Option<u32>) {
self.billboards = None;
let (chunk_idx, in_chunk) = voxel_split(voxel);
if color.is_some() {
let vxl = self.ensure_chunk(chunk_idx);
#[allow(clippy::cast_possible_wrap)]
set_cube(
vxl,
in_chunk.x as i32,
in_chunk.y as i32,
in_chunk.z as i32,
color,
);
self.bump_chunk_version(chunk_idx);
} else if let Some(vxl) = self.chunks.get_mut(&chunk_idx) {
#[allow(clippy::cast_possible_wrap)]
set_cube(
vxl,
in_chunk.x as i32,
in_chunk.y as i32,
in_chunk.z as i32,
None,
);
self.bump_chunk_version(chunk_idx);
}
}
pub fn set_rect(&mut self, lo: IVec3, hi: IVec3, color: Option<u32>) {
self.billboards = None;
let lo_n = lo.min(hi);
let hi_n = lo.max(hi);
let (lo_c, _) = voxel_split(lo_n);
let (hi_c, _) = voxel_split(hi_n);
let cs = chunk_size_ivec3();
for cz in lo_c.z..=hi_c.z {
for cy in lo_c.y..=hi_c.y {
for cx in lo_c.x..=hi_c.x {
let chunk_idx = IVec3::new(cx, cy, cz);
let chunk_origin = chunk_idx * cs;
let chunk_end = chunk_origin + cs - IVec3::ONE;
let local_lo = lo_n.max(chunk_origin) - chunk_origin;
let local_hi = hi_n.min(chunk_end) - chunk_origin;
apply_set_rect(self, chunk_idx, local_lo, local_hi, color);
}
}
}
}
pub fn set_sphere(&mut self, centre: IVec3, radius: u32, color: Option<u32>) {
self.billboards = None;
#[allow(clippy::cast_possible_wrap)]
let r_i = radius as i32;
let lo = centre - IVec3::splat(r_i);
let hi = centre + IVec3::splat(r_i);
let (lo_c, _) = voxel_split(lo);
let (hi_c, _) = voxel_split(hi);
let cs = chunk_size_ivec3();
for cz in lo_c.z..=hi_c.z {
for cy in lo_c.y..=hi_c.y {
for cx in lo_c.x..=hi_c.x {
let chunk_idx = IVec3::new(cx, cy, cz);
let chunk_origin = chunk_idx * cs;
let local_centre = centre - chunk_origin;
apply_set_sphere(self, chunk_idx, local_centre, radius, color);
}
}
}
}
}
fn apply_set_rect(
grid: &mut Grid,
chunk_idx: IVec3,
local_lo: IVec3,
local_hi: IVec3,
color: Option<u32>,
) {
let mut wrote = false;
if color.is_some() {
let vxl = grid.ensure_chunk(chunk_idx);
set_rect(vxl, local_lo.into(), local_hi.into(), color);
wrote = true;
} else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
set_rect(vxl, local_lo.into(), local_hi.into(), None);
wrote = true;
}
if wrote {
grid.bump_chunk_version(chunk_idx);
}
}
fn apply_set_sphere(
grid: &mut Grid,
chunk_idx: IVec3,
local_centre: IVec3,
radius: u32,
color: Option<u32>,
) {
let mut wrote = false;
if color.is_some() {
let vxl = grid.ensure_chunk(chunk_idx);
set_sphere(vxl, local_centre.into(), radius, color);
wrote = true;
} else if let Some(vxl) = grid.chunks.get_mut(&chunk_idx) {
set_sphere(vxl, local_centre.into(), radius, None);
wrote = true;
}
if wrote {
grid.bump_chunk_version(chunk_idx);
}
}
#[must_use]
pub fn voxel_at(local: &GridLocalPos) -> IVec3 {
crate::addr::voxel_global(local.chunk, local.voxel)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chunks::tests::voxel_is_solid;
use crate::GridTransform;
const TEST_COL: u32 = 0x80_aa_bb_cc;
#[test]
fn set_voxel_inserts_in_correct_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(5, 6, 7), Some(TEST_COL));
let vxl = g.chunk(IVec3::ZERO).expect("chunk created");
assert!(voxel_is_solid(vxl, 5, 6, 7));
assert!(!voxel_is_solid(vxl, 5, 6, 8));
assert_eq!(g.chunk_count(), 1);
}
#[test]
fn set_voxel_negative_coords_use_neg_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(-1, 0, 0), Some(TEST_COL));
assert!(g.chunk(IVec3::new(-1, 0, 0)).is_some());
let vxl = g.chunk(IVec3::new(-1, 0, 0)).unwrap();
assert!(voxel_is_solid(vxl, CHUNK_SIZE_XY - 1, 0, 0));
assert!(g.chunk(IVec3::ZERO).is_none());
}
#[test]
fn set_voxel_carve_then_insert_round_trips() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(10, 10, 10), Some(TEST_COL));
assert!(voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
g.set_voxel(IVec3::new(10, 10, 10), None);
assert!(!voxel_is_solid(g.chunk(IVec3::ZERO).unwrap(), 10, 10, 10));
}
#[test]
fn set_voxel_carve_in_missing_chunk_is_noop() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(5, 5, 5), None);
assert_eq!(g.chunk_count(), 0);
}
#[test]
fn set_rect_within_one_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
assert_eq!(g.chunk_count(), 1);
let vxl = g.chunk(IVec3::ZERO).unwrap();
for z in 0..=3 {
for y in 0..=3 {
for x in 0..=3 {
assert!(voxel_is_solid(vxl, x, y, z), "({x},{y},{z}) air");
}
}
}
assert!(!voxel_is_solid(vxl, 4, 0, 0));
assert!(!voxel_is_solid(vxl, 0, 4, 0));
assert!(!voxel_is_solid(vxl, 0, 0, 4));
}
#[test]
fn set_rect_spans_two_chunks_x() {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
assert_eq!(g.chunk_count(), 2);
let v0 = g.chunk(IVec3::ZERO).unwrap();
assert!(voxel_is_solid(v0, 126, 0, 0));
assert!(voxel_is_solid(v0, 127, 0, 0));
assert!(!voxel_is_solid(v0, 125, 0, 0));
let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
assert!(voxel_is_solid(v1, 0, 0, 0));
assert!(voxel_is_solid(v1, 1, 0, 0));
assert!(!voxel_is_solid(v1, 2, 0, 0));
}
#[test]
fn set_rect_spans_z_boundary() {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(IVec3::new(0, 0, 254), IVec3::new(0, 0, 257), Some(TEST_COL));
assert_eq!(g.chunk_count(), 2);
let v0 = g.chunk(IVec3::ZERO).unwrap();
assert!(voxel_is_solid(v0, 0, 0, 254));
assert!(voxel_is_solid(v0, 0, 0, 255));
let v1 = g.chunk(IVec3::new(0, 0, 1)).unwrap();
assert!(voxel_is_solid(v1, 0, 0, 0));
assert!(voxel_is_solid(v1, 0, 0, 1));
assert!(!voxel_is_solid(v1, 0, 0, 2));
}
#[test]
fn set_rect_unsorted_lo_hi_normalised() {
let mut g1 = Grid::new(GridTransform::identity());
let mut g2 = Grid::new(GridTransform::identity());
g1.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
g2.set_rect(IVec3::new(3, 3, 3), IVec3::new(0, 0, 0), Some(TEST_COL));
let v1 = g1.chunk(IVec3::ZERO).unwrap();
let v2 = g2.chunk(IVec3::ZERO).unwrap();
for z in 0..=3 {
for y in 0..=3 {
for x in 0..=3 {
assert_eq!(voxel_is_solid(v1, x, y, z), voxel_is_solid(v2, x, y, z));
}
}
}
}
#[test]
fn set_sphere_within_one_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
assert_eq!(g.chunk_count(), 1);
let vxl = g.chunk(IVec3::ZERO).unwrap();
assert!(voxel_is_solid(vxl, 64, 64, 100));
assert!(voxel_is_solid(vxl, 65, 64, 100));
assert!(voxel_is_solid(vxl, 64, 64, 105));
assert!(!voxel_is_solid(vxl, 70, 64, 100));
}
#[test]
fn set_sphere_spans_chunk_boundary() {
let mut g = Grid::new(GridTransform::identity());
g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
assert_eq!(g.chunk_count(), 2);
let v0 = g.chunk(IVec3::ZERO).unwrap();
assert!(voxel_is_solid(v0, 127, 64, 100));
assert!(voxel_is_solid(v0, 124, 64, 100));
let v1 = g.chunk(IVec3::new(1, 0, 0)).unwrap();
assert!(voxel_is_solid(v1, 0, 64, 100));
assert!(voxel_is_solid(v1, 2, 64, 100));
}
fn stamp_sentinel_cache(g: &mut Grid) {
g.billboards = Some(crate::BillboardCache::new_empty(32));
}
#[test]
fn set_voxel_invalidates_billboard_cache() {
let mut g = Grid::new(GridTransform::identity());
stamp_sentinel_cache(&mut g);
assert!(g.billboards.is_some());
g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
assert!(
g.billboards.is_none(),
"set_voxel should clear the billboard cache"
);
}
#[test]
fn set_voxel_carve_also_invalidates() {
let mut g = Grid::new(GridTransform::identity());
stamp_sentinel_cache(&mut g);
g.set_voxel(IVec3::new(5, 5, 5), None); assert!(
g.billboards.is_none(),
"carve should clear the cache (conservative)"
);
}
#[test]
fn set_rect_invalidates_billboard_cache() {
let mut g = Grid::new(GridTransform::identity());
stamp_sentinel_cache(&mut g);
g.set_rect(IVec3::new(0, 0, 0), IVec3::new(3, 3, 3), Some(TEST_COL));
assert!(g.billboards.is_none(), "set_rect should clear the cache");
}
#[test]
fn set_sphere_invalidates_billboard_cache() {
let mut g = Grid::new(GridTransform::identity());
stamp_sentinel_cache(&mut g);
g.set_sphere(IVec3::new(64, 64, 100), 5, Some(TEST_COL));
assert!(g.billboards.is_none(), "set_sphere should clear the cache");
}
#[test]
fn set_voxel_dispatches_to_correct_chunk_on_y_z_axes() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(200, 300, 500), Some(TEST_COL));
let vxl = g
.chunk(IVec3::new(1, 2, 1))
.expect("expected chunk (1, 2, 1)");
assert!(voxel_is_solid(vxl, 72, 44, 244));
}
#[test]
fn chunk_version_defaults_to_zero_for_missing() {
let g = Grid::new(GridTransform::identity());
assert_eq!(g.chunk_version(IVec3::ZERO), 0);
assert_eq!(g.chunk_version(IVec3::new(7, -3, 12)), 0);
}
#[test]
fn set_voxel_insert_bumps_to_one() {
let mut g = Grid::new(GridTransform::identity());
assert_eq!(g.chunk_version(IVec3::ZERO), 0);
g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
assert_eq!(g.chunk_version(IVec3::ZERO), 1);
}
#[test]
fn set_voxel_carve_in_existing_chunk_bumps() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(5, 5, 5), Some(TEST_COL));
g.set_voxel(IVec3::new(5, 5, 5), None);
assert_eq!(g.chunk_version(IVec3::ZERO), 2);
}
#[test]
fn set_voxel_carve_in_missing_chunk_does_not_bump() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(5, 5, 5), None);
assert_eq!(g.chunk_version(IVec3::ZERO), 0);
assert!(g.chunk_versions.is_empty());
}
#[test]
fn set_rect_multi_chunk_bumps_every_touched_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), Some(TEST_COL));
assert_eq!(g.chunk_version(IVec3::ZERO), 1);
assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
assert_eq!(g.chunk_versions.len(), 2);
}
#[test]
fn set_rect_carve_bumps_only_existing_chunks() {
let mut g = Grid::new(GridTransform::identity());
g.set_voxel(IVec3::new(0, 0, 0), Some(TEST_COL));
assert_eq!(g.chunk_version(IVec3::ZERO), 1);
g.set_rect(IVec3::new(126, 0, 0), IVec3::new(129, 0, 0), None);
assert_eq!(g.chunk_version(IVec3::ZERO), 2);
assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 0);
}
#[test]
fn set_sphere_multi_chunk_bumps_every_written_chunk() {
let mut g = Grid::new(GridTransform::identity());
g.set_sphere(IVec3::new(127, 64, 100), 4, Some(TEST_COL));
assert_eq!(g.chunk_version(IVec3::ZERO), 1);
assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
}
}