use glam::{DQuat, DVec3, IVec3};
use roxlap_core::world_query::{getcube, Cube};
use crate::{voxel_split, Grid, Scene, CHUNK_SIZE_Z};
#[derive(Debug, Clone, Copy, Default)]
pub struct Solidity {
pub bedrock_blocks: bool,
pub passable: Option<fn(crate::VoxColor) -> bool>,
}
impl Solidity {
fn cube_blocks(self, cube: Cube) -> bool {
match cube {
Cube::Air => false,
Cube::UnexposedSolid => true,
Cube::Color(c) => !self
.passable
.is_some_and(|passes| passes(crate::VoxColor(c))),
}
}
}
#[must_use]
pub fn box_overlaps_solid(scene: &Scene, min: DVec3, max: DVec3, solidity: Solidity) -> bool {
scene
.grids()
.any(|(_id, grid)| grid_box_overlaps_solid(grid, min, max, solidity))
}
#[must_use]
pub fn point_overlaps_solid(scene: &Scene, p: DVec3, solidity: Solidity) -> bool {
box_overlaps_solid(scene, p, p, solidity)
}
#[must_use]
pub fn grid_box_overlaps_solid(grid: &Grid, min: DVec3, max: DVec3, solidity: Solidity) -> bool {
let (lmin, lmax) = if grid.transform.rotation == DQuat::IDENTITY {
(min - grid.transform.origin, max - grid.transform.origin)
} else {
let inv = grid.transform.rotation.inverse();
let mut lmin = DVec3::INFINITY;
let mut lmax = DVec3::NEG_INFINITY;
for corner in 0..8 {
let world = DVec3::new(
if corner & 1 == 0 { min.x } else { max.x },
if corner & 2 == 0 { min.y } else { max.y },
if corner & 4 == 0 { min.z } else { max.z },
);
let local = inv * (world - grid.transform.origin);
lmin = lmin.min(local);
lmax = lmax.max(local);
}
(lmin, lmax)
};
#[allow(clippy::cast_possible_truncation)]
let (lo, hi) = (lmin.floor().as_ivec3(), lmax.floor().as_ivec3());
let bedrock_z = CHUNK_SIZE_Z - 1;
for vz in lo.z..=hi.z {
for vy in lo.y..=hi.y {
for vx in lo.x..=hi.x {
let (chunk_idx, in_chunk) = voxel_split(IVec3::new(vx, vy, vz));
if !solidity.bedrock_blocks && in_chunk.z == bedrock_z {
continue;
}
let Some(chunk) = grid.chunk(chunk_idx) else {
continue;
};
#[allow(clippy::cast_possible_wrap)]
let cube = getcube(
&chunk.data,
&chunk.column_offset,
chunk.vsid,
in_chunk.x as i32,
in_chunk.y as i32,
in_chunk.z as i32,
);
if solidity.cube_blocks(cube) {
return true;
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{GridTransform, VoxColor};
const AIR_BEDROCK: Solidity = Solidity {
bedrock_blocks: false,
passable: None,
};
fn floating_voxel_scene() -> Scene {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::at(DVec3::new(0.0, 0.0, -100.0)));
let grid = scene.grid_mut(id).expect("grid present");
grid.set_voxel(IVec3::new(10, 10, 50), Some(VoxColor(0x80_aa_bb_cc)));
scene
}
fn cube_probe(scene: &Scene, world: [f64; 3], r: f64) -> bool {
let p = DVec3::from(world);
box_overlaps_solid(scene, p - r, p + r, AIR_BEDROCK)
}
#[test]
fn visible_voxel_blocks() {
let scene = floating_voxel_scene();
assert!(cube_probe(&scene, [10.0, 10.0, -50.0], 0.3));
}
#[test]
fn out_of_grid_position_is_air() {
let scene = floating_voxel_scene();
assert!(!cube_probe(&scene, [-500.0, -500.0, -50.0], 0.3));
}
#[test]
fn below_isolated_floating_voxel_is_air() {
let scene = floating_voxel_scene();
assert!(!cube_probe(&scene, [10.0, 10.0, 0.0], 0.3));
}
#[test]
fn slab_interior_unexposed_solid_blocks() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let grid = scene.grid_mut(id).expect("grid present");
grid.set_rect(
IVec3::new(0, 0, 50),
IVec3::new(20, 20, 80),
Some(VoxColor(0x80_66_77_88)),
);
assert!(cube_probe(&scene, [10.0, 10.0, 65.0], 0.3));
}
#[test]
fn bedrock_placeholder_is_policy() {
let scene = floating_voxel_scene();
let at_bedrock = [100.0, 100.0, -100.0 + f64::from(CHUNK_SIZE_Z) - 0.5];
assert!(!cube_probe(&scene, at_bedrock, 0.3));
let p = DVec3::from(at_bedrock);
assert!(box_overlaps_solid(
&scene,
p - 0.3,
p + 0.3,
Solidity {
bedrock_blocks: true,
passable: None,
},
));
}
#[test]
fn rotated_grid_blocks_conservatively() {
let mut scene = Scene::new();
let rot = DQuat::from_rotation_z(std::f64::consts::FRAC_PI_4);
let id = scene.add_grid(GridTransform {
origin: DVec3::new(200.0, 0.0, 0.0),
rotation: rot,
});
let grid = scene.grid_mut(id).expect("grid present");
grid.set_voxel(IVec3::new(10, 10, 50), Some(VoxColor(0x80_11_22_33)));
let centre_world = DVec3::new(200.0, 0.0, 0.0) + rot * DVec3::new(10.5, 10.5, 50.5);
assert!(box_overlaps_solid(
&scene,
centre_world - 0.1,
centre_world + 0.1,
AIR_BEDROCK,
));
let far = centre_world + DVec3::new(40.0, 40.0, 0.0);
assert!(!box_overlaps_solid(
&scene,
far - 0.1,
far + 0.1,
AIR_BEDROCK
));
}
#[test]
fn passable_colour_veto() {
const WATER: VoxColor = VoxColor(0x80_20_60_c0);
const GLASS: VoxColor = VoxColor(0x80_50_c0_e0);
fn water_passes(c: VoxColor) -> bool {
c.rgb_part() == WATER.rgb_part()
}
let veto = Solidity {
bedrock_blocks: false,
passable: Some(water_passes),
};
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let grid = scene.grid_mut(id).expect("grid present");
grid.set_rect(IVec3::new(10, 0, 40), IVec3::new(10, 20, 60), Some(WATER));
grid.set_rect(IVec3::new(20, 0, 40), IVec3::new(20, 20, 60), Some(GLASS));
grid.set_rect(IVec3::new(30, 0, 40), IVec3::new(40, 20, 60), Some(WATER));
let probe = |x: f64, z: f64, s: Solidity| {
let p = DVec3::new(x, 10.0, z);
box_overlaps_solid(&scene, p - 0.3, p + 0.3, s)
};
assert!(probe(10.5, 50.0, AIR_BEDROCK));
assert!(!probe(10.5, 50.0, veto));
assert!(probe(20.5, 50.0, veto));
assert!(!probe(30.5, 50.0, veto));
assert!(probe(35.5, 50.0, veto));
}
#[test]
fn point_probe_matches_degenerate_box() {
let scene = floating_voxel_scene();
assert!(point_overlaps_solid(
&scene,
DVec3::new(10.5, 10.5, -49.5),
AIR_BEDROCK,
));
assert!(!point_overlaps_solid(
&scene,
DVec3::new(10.5, 10.5, -55.5),
AIR_BEDROCK,
));
}
#[test]
fn passable_walls_must_be_at_most_two_voxels_thick() {
const WATER: VoxColor = VoxColor(0x80_30_60_c8);
fn water_passes(c: VoxColor) -> bool {
c.rgb_part() == WATER.rgb_part()
}
let veto = Solidity {
bedrock_blocks: false,
passable: Some(water_passes),
};
let crossing_blocked = |thick: i32| {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).expect("grid");
g.set_rect(
IVec3::new(0, 10, 0),
IVec3::new(50, 10 + thick - 1, 50),
Some(WATER),
);
let p = DVec3::new(25.0, 10.0 + f64::from(thick) * 0.5, 25.0);
box_overlaps_solid(
&scene,
p - DVec3::new(0.4, 0.4, 0.9),
p + DVec3::new(0.4, 0.4, 0.9),
veto,
)
};
assert!(!crossing_blocked(1));
assert!(!crossing_blocked(2));
assert!(crossing_blocked(3), "3-thick has a hidden core");
assert!(crossing_blocked(4));
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).expect("grid");
g.set_rect(IVec3::new(0, 0, 0), IVec3::new(50, 1, 50), Some(WATER));
let p = DVec3::new(25.0, 1.0, 25.0);
assert!(
box_overlaps_solid(
&scene,
p - DVec3::new(0.4, 0.4, 0.9),
p + DVec3::new(0.4, 0.4, 0.9),
veto,
),
"chunk-edge wall: edge layer has no stored colour"
);
}
}