use crate::abi;
use crate::world::WorldError;
use crate::{Host, Load};
pub const READ_MAX: u64 = abi::Voxel::READ_MAX;
const PER_METRE: f64 = 2.0;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct VoxelPos {
x: u32,
y: u32,
z: u32,
}
impl VoxelPos {
#[must_use]
pub const fn new(x: u32, y: u32, z: u32) -> Self {
Self { x, y, z }
}
#[must_use]
pub fn from_metres(metres: [f64; 3]) -> Option<Self> {
let [x, y, z] = metres.map(index);
Some(Self::new(x?, y?, z?))
}
#[must_use]
pub fn to_metres(self) -> [f64; 3] {
self.to_array().map(|axis| f64::from(axis) / PER_METRE)
}
#[must_use]
pub const fn x(self) -> u32 {
self.x
}
#[must_use]
pub const fn y(self) -> u32 {
self.y
}
#[must_use]
pub const fn z(self) -> u32 {
self.z
}
#[must_use]
pub const fn to_array(self) -> [u32; 3] {
[self.x, self.y, self.z]
}
}
impl From<[u32; 3]> for VoxelPos {
fn from([x, y, z]: [u32; 3]) -> Self {
Self::new(x, y, z)
}
}
impl From<VoxelPos> for [u32; 3] {
fn from(at: VoxelPos) -> Self {
at.to_array()
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "the value is checked to lie in 0 to `Voxel::END` first"
)]
fn index(metres: f64) -> Option<u32> {
let voxels = (metres * PER_METRE).floor();
(voxels >= 0.0 && voxels < f64::from(abi::Voxel::END)).then_some(voxels as u32)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum VoxelMaterial {
#[default]
Air,
Terrain(u8),
Building(u8),
Unknown(u8),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Voxel {
material: VoxelMaterial,
fill: u8,
}
impl Voxel {
#[must_use]
pub const fn new(material: VoxelMaterial, fill: u8) -> Self {
Self { material, fill }
}
#[must_use]
pub const fn material(self) -> VoxelMaterial {
self.material
}
#[must_use]
pub const fn fill(self) -> u8 {
self.fill
}
#[must_use]
pub const fn is_air(self) -> bool {
matches!(self.material, VoxelMaterial::Air)
}
pub(crate) const fn from_abi(raw: abi::Voxel) -> Self {
let material = match raw.kind {
abi::Voxel::AIR => VoxelMaterial::Air,
abi::Voxel::TERRAIN => VoxelMaterial::Terrain(raw.material),
abi::Voxel::BUILDING => VoxelMaterial::Building(raw.material),
_ => VoxelMaterial::Unknown(raw.material),
};
Self::new(material, raw.fill)
}
#[cfg(any(test, feature = "test"))]
pub(crate) const fn to_abi(self) -> abi::Voxel {
let (kind, material) = match self.material {
VoxelMaterial::Air => (abi::Voxel::AIR, 0),
VoxelMaterial::Terrain(id) => (abi::Voxel::TERRAIN, id),
VoxelMaterial::Building(id) => (abi::Voxel::BUILDING, id),
VoxelMaterial::Unknown(id) => (u8::MAX, id),
};
abi::Voxel {
kind,
material,
fill: self.fill,
reserved: 0,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct VoxelBox {
min: VoxelPos,
size: [u32; 3],
voxels: Vec<Voxel>,
}
impl VoxelBox {
#[must_use]
pub const fn min(&self) -> VoxelPos {
self.min
}
#[must_use]
pub const fn size(&self) -> [u32; 3] {
self.size
}
#[must_use]
pub fn get(&self, at: VoxelPos) -> Option<Voxel> {
let mut index = 0usize;
for axis in (0..3).rev() {
let offset = at.to_array()[axis].checked_sub(self.min.to_array()[axis])?;
if offset >= self.size[axis] {
return None;
}
index = index * self.size[axis] as usize + offset as usize;
}
self.voxels.get(index).copied()
}
#[must_use]
pub fn as_slice(&self) -> &[Voxel] {
&self.voxels
}
#[must_use]
pub fn iter(&self) -> Iter<'_> {
Iter {
block: self,
next: 0,
}
}
}
impl<'a> IntoIterator for &'a VoxelBox {
type Item = (VoxelPos, Voxel);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Iter<'a> {
self.iter()
}
}
#[derive(Clone, Debug)]
pub struct Iter<'a> {
block: &'a VoxelBox,
next: usize,
}
impl Iterator for Iter<'_> {
type Item = (VoxelPos, Voxel);
fn next(&mut self) -> Option<Self::Item> {
let voxel = *self.block.voxels.get(self.next)?;
let [width, height, _] = self.block.size.map(|axis| axis as usize);
let [x, y, z] = [
self.next % width,
self.next / width % height,
self.next / (width * height),
]
.map(|offset| u32::try_from(offset).unwrap_or(u32::MAX));
let [min_x, min_y, min_z] = self.block.min.to_array();
self.next += 1;
Some((VoxelPos::new(min_x + x, min_y + y, min_z + z), voxel))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let left = self.block.voxels.len() - self.next;
(left, Some(left))
}
}
impl ExactSizeIterator for Iter<'_> {}
#[derive(Clone, Copy, Debug)]
pub struct Voxels {
host: Host,
}
impl Voxels {
pub fn read(&self, min: VoxelPos, size: [u32; 3]) -> Result<VoxelBox, WorldError> {
let count = abi::Voxel::read_count(min.to_array(), size).ok_or(WorldError::Refused)?;
let mut raw =
vec![abi::Voxel::default(); usize::try_from(count).map_err(|_| WorldError::Refused)?];
let corner = min.to_array();
let status = (self.host.api().voxel_read)(
self.host.handle(),
&raw const corner,
&raw const size,
raw.as_mut_ptr(),
count,
);
WorldError::from_status(status)?;
Ok(VoxelBox {
min,
size,
voxels: raw.into_iter().map(Voxel::from_abi).collect(),
})
}
pub fn sample(&self, at: VoxelPos) -> Result<Voxel, WorldError> {
let mut raw = abi::Voxel::default();
let corner = at.to_array();
let size = [1u32; 3];
let status = (self.host.api().voxel_read)(
self.host.handle(),
&raw const corner,
&raw const size,
&raw mut raw,
1,
);
WorldError::from_status(status).map(|()| Voxel::from_abi(raw))
}
}
impl Load<'_> {
#[must_use]
pub fn voxels(&mut self) -> Voxels {
Voxels { host: self.host() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_point_lies_in_the_voxel_below_it_on_every_axis() {
assert_eq!(
VoxelPos::from_metres([0.0, 0.49, 0.5]),
Some(VoxelPos::new(0, 0, 1))
);
assert_eq!(VoxelPos::from_metres([-0.1, 0.0, 0.0]), None);
assert_eq!(VoxelPos::from_metres([f64::NAN, 0.0, 0.0]), None);
assert_eq!(VoxelPos::from_metres([0.0, 1e12, 0.0]), None);
}
#[test]
fn every_material_survives_the_abi() {
for material in [
VoxelMaterial::Air,
VoxelMaterial::Terrain(9),
VoxelMaterial::Building(0x83),
VoxelMaterial::Unknown(4),
] {
let voxel = Voxel::new(material, 0x40);
assert_eq!(Voxel::from_abi(voxel.to_abi()), voxel);
}
}
#[test]
fn a_box_finds_each_voxel_where_iter_puts_it() {
let size = [3, 2, 4];
let voxels = (0..24)
.map(|i| Voxel::new(VoxelMaterial::Terrain(i), i))
.collect();
let block = VoxelBox {
min: VoxelPos::new(10, 20, 30),
size,
voxels,
};
assert_eq!(block.iter().len(), 24);
for (at, voxel) in &block {
assert_eq!(block.get(at), Some(voxel), "{at:?}");
}
assert_eq!(
block.get(VoxelPos::new(11, 21, 32)).map(Voxel::fill),
Some(1 + 3 + 2 * 6)
);
assert_eq!(block.get(VoxelPos::new(13, 20, 30)), None);
assert_eq!(block.get(VoxelPos::new(9, 20, 30)), None);
}
}