use core::fmt;
use crate::bytes::{Cursor, OutOfBounds};
use crate::Rgb6;
const VIS_NEG_X: u8 = 0x01;
const VIS_POS_X: u8 = 0x02;
const VIS_NEG_Y: u8 = 0x04;
const VIS_POS_Y: u8 = 0x08;
const VIS_POS_Z: u8 = 0x20;
const VIS_NEG_Z: u8 = 0x10;
fn compute_vis_dir(occ: &impl Fn(i64, i64, i64) -> bool, x: i64, y: i64, z: i64) -> (u8, u8) {
let mut vis = 0u8;
if !occ(x - 1, y, z) {
vis |= VIS_NEG_X;
}
if !occ(x + 1, y, z) {
vis |= VIS_POS_X;
}
if !occ(x, y - 1, z) {
vis |= VIS_NEG_Y;
}
if !occ(x, y + 1, z) {
vis |= VIS_POS_Y;
}
if !occ(x, y, z - 1) {
vis |= VIS_NEG_Z;
}
if !occ(x, y, z + 1) {
vis |= VIS_POS_Z;
}
let mut n = [0.0f32; 3];
for dz in -1..=1 {
for dy in -1..=1 {
for dx in -1..=1 {
if (dx | dy | dz) != 0 && !occ(x + dx, y + dy, z + dz) {
n[0] += dx as f32;
n[1] += dy as f32;
n[2] += dz as f32;
}
}
}
}
(vis, crate::equivec::nearest_dir(n))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Voxel {
pub col: u32,
pub z: u16,
pub vis: u8,
pub dir: u8,
}
#[derive(Debug, Clone)]
pub struct Kv6 {
pub xsiz: u32,
pub ysiz: u32,
pub zsiz: u32,
pub xpiv: f32,
pub ypiv: f32,
pub zpiv: f32,
pub voxels: Vec<Voxel>,
pub xlen: Vec<u32>,
pub ylen: Vec<Vec<u16>>,
pub palette: Option<[Rgb6; 256]>,
}
impl Kv6 {
#[must_use]
pub fn from_fn<F: Fn(u32, u32, u32) -> Option<u32>>(
xsiz: u32,
ysiz: u32,
zsiz: u32,
fill: F,
) -> Kv6 {
Self::build_inner(xsiz, ysiz, zsiz, fill, false)
}
#[must_use]
pub fn from_fn_shaded<F: Fn(u32, u32, u32) -> Option<u32>>(
xsiz: u32,
ysiz: u32,
zsiz: u32,
fill: F,
) -> Kv6 {
Self::build_inner(xsiz, ysiz, zsiz, fill, true)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss
)]
fn build_inner<F: Fn(u32, u32, u32) -> Option<u32>>(
xsiz: u32,
ysiz: u32,
zsiz: u32,
fill: F,
shaded: bool,
) -> Kv6 {
let occupied = |x: i64, y: i64, z: i64| -> bool {
x >= 0
&& y >= 0
&& z >= 0
&& (x as u32) < xsiz
&& (y as u32) < ysiz
&& (z as u32) < zsiz
&& fill(x as u32, y as u32, z as u32).is_some()
};
let mut voxels: Vec<Voxel> = Vec::new();
let mut xlen: Vec<u32> = Vec::with_capacity(xsiz as usize);
let mut ylen: Vec<Vec<u16>> = Vec::with_capacity(xsiz as usize);
for x in 0..xsiz {
let mut col_counts: Vec<u16> = Vec::with_capacity(ysiz as usize);
for y in 0..ysiz {
let before = voxels.len();
for z in 0..zsiz {
let Some(col) = fill(x, y, z) else { continue };
let (xi, yi, zi) = (i64::from(x), i64::from(y), i64::from(z));
let exposed = !occupied(xi - 1, yi, zi)
|| !occupied(xi + 1, yi, zi)
|| !occupied(xi, yi - 1, zi)
|| !occupied(xi, yi + 1, zi)
|| !occupied(xi, yi, zi - 1)
|| !occupied(xi, yi, zi + 1);
if exposed {
let (vis, dir) = if shaded {
compute_vis_dir(&occupied, xi, yi, zi)
} else {
(63, 0)
};
voxels.push(Voxel {
col,
z: z as u16,
vis,
dir,
});
}
}
col_counts.push((voxels.len() - before) as u16);
}
xlen.push(col_counts.iter().map(|&c| u32::from(c)).sum());
ylen.push(col_counts);
}
Kv6 {
xsiz,
ysiz,
zsiz,
xpiv: xsiz as f32 * 0.5,
ypiv: ysiz as f32 * 0.5,
zpiv: zsiz as f32 * 0.5,
voxels,
xlen,
ylen,
palette: None,
}
}
#[allow(clippy::cast_possible_wrap)]
pub fn recompute_surface(&mut self, occupied: impl Fn(i32, i32, i32) -> bool) {
let xsiz = self.xsiz;
let ysiz = self.ysiz;
let zsiz = self.zsiz;
let occ = |x: i64, y: i64, z: i64| -> bool {
x >= 0
&& y >= 0
&& z >= 0
&& (x as u32) < xsiz
&& (y as u32) < ysiz
&& (z as u32) < zsiz
&& occupied(x as i32, y as i32, z as i32)
};
let mut vi = 0usize;
for x in 0..xsiz as usize {
for y in 0..ysiz as usize {
let len = self.ylen[x][y] as usize;
for _ in 0..len {
let z = i64::from(self.voxels[vi].z);
let (vis, dir) = compute_vis_dir(&occ, x as i64, y as i64, z);
self.voxels[vi].vis = vis;
self.voxels[vi].dir = dir;
vi += 1;
}
}
}
}
#[must_use]
pub fn solid_box(xsiz: u32, ysiz: u32, zsiz: u32, col: u32) -> Kv6 {
Kv6::from_fn(xsiz, ysiz, zsiz, |_, _, _| Some(col))
}
#[must_use]
pub fn solid_cube(n: u32, col: u32) -> Kv6 {
Kv6::solid_box(n, n, n, col)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
TooSmall { got: usize },
BadMagic { got: [u8; 4] },
Truncated { at: usize, need: usize },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::TooSmall { got } => write!(
f,
"kv6 file too small ({got} bytes; need at least 32 byte header)"
),
Self::BadMagic { got } => write!(
f,
"kv6 bad magic: got [{:#04x},{:#04x},{:#04x},{:#04x}], expected b\"Kvxl\"",
got[0], got[1], got[2], got[3]
),
Self::Truncated { at, need } => {
write!(f, "kv6 truncated: need {need} bytes at offset {at}")
}
}
}
}
impl std::error::Error for ParseError {}
impl From<OutOfBounds> for ParseError {
fn from(e: OutOfBounds) -> Self {
Self::Truncated {
at: e.at,
need: e.need,
}
}
}
const HEADER_LEN: usize = 32;
const MAGIC: &[u8; 4] = b"Kvxl";
const PALETTE_MAGIC: &[u8; 4] = b"SPal";
const PALETTE_LEN: usize = 768;
pub fn parse(bytes: &[u8]) -> Result<Kv6, ParseError> {
if bytes.len() < HEADER_LEN {
return Err(ParseError::TooSmall { got: bytes.len() });
}
let mut cur = Cursor::new(bytes);
let magic = cur.read_bytes(4)?;
if magic != MAGIC {
return Err(ParseError::BadMagic {
got: [magic[0], magic[1], magic[2], magic[3]],
});
}
let xsiz = cur.read_u32()?;
let ysiz = cur.read_u32()?;
let zsiz = cur.read_u32()?;
let xpiv = cur.read_f32()?;
let ypiv = cur.read_f32()?;
let zpiv = cur.read_f32()?;
let numvoxs = cur.read_u32()?;
let mut voxels = Vec::with_capacity(numvoxs as usize);
for _ in 0..numvoxs {
let col = cur.read_u32()?;
let z = cur.read_u16()?;
let vis = cur.read_u8()?;
let dir = cur.read_u8()?;
voxels.push(Voxel { col, z, vis, dir });
}
let mut xlen = Vec::with_capacity(xsiz as usize);
for _ in 0..xsiz {
xlen.push(cur.read_u32()?);
}
let mut ylen = Vec::with_capacity(xsiz as usize);
for _ in 0..xsiz {
let mut row = Vec::with_capacity(ysiz as usize);
for _ in 0..ysiz {
row.push(cur.read_u16()?);
}
ylen.push(row);
}
let palette =
if cur.remaining() >= 4 + PALETTE_LEN && cur.peek(4) == Some(PALETTE_MAGIC.as_slice()) {
cur.read_bytes(4)?;
let mut pal = [Rgb6::default(); 256];
for entry in &mut pal {
entry.r = cur.read_u8()?;
entry.g = cur.read_u8()?;
entry.b = cur.read_u8()?;
}
Some(pal)
} else {
None
};
Ok(Kv6 {
xsiz,
ysiz,
zsiz,
xpiv,
ypiv,
zpiv,
voxels,
xlen,
ylen,
palette,
})
}
#[must_use]
pub fn serialize(kv6: &Kv6) -> Vec<u8> {
let pal_bytes = if kv6.palette.is_some() {
4 + PALETTE_LEN
} else {
0
};
let body_bytes = kv6.voxels.len() * 8
+ kv6.xlen.len() * 4
+ kv6.ylen.iter().map(|row| row.len() * 2).sum::<usize>();
let mut out = Vec::with_capacity(HEADER_LEN + body_bytes + pal_bytes);
out.extend_from_slice(MAGIC);
out.extend_from_slice(&kv6.xsiz.to_le_bytes());
out.extend_from_slice(&kv6.ysiz.to_le_bytes());
out.extend_from_slice(&kv6.zsiz.to_le_bytes());
out.extend_from_slice(&kv6.xpiv.to_le_bytes());
out.extend_from_slice(&kv6.ypiv.to_le_bytes());
out.extend_from_slice(&kv6.zpiv.to_le_bytes());
let numvoxs =
u32::try_from(kv6.voxels.len()).expect("kv6 numvoxs must fit in u32 (file format limit)");
out.extend_from_slice(&numvoxs.to_le_bytes());
for v in &kv6.voxels {
out.extend_from_slice(&v.col.to_le_bytes());
out.extend_from_slice(&v.z.to_le_bytes());
out.push(v.vis);
out.push(v.dir);
}
for v in &kv6.xlen {
out.extend_from_slice(&v.to_le_bytes());
}
for row in &kv6.ylen {
for v in row {
out.extend_from_slice(&v.to_le_bytes());
}
}
if let Some(pal) = &kv6.palette {
out.extend_from_slice(PALETTE_MAGIC);
for e in pal {
out.push(e.r);
out.push(e.g);
out.push(e.b);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const COCO_KV6: &[u8] = include_bytes!("../../../assets/coco.kv6");
#[test]
fn solid_cube_builder_is_surface_only_and_consistent() {
let cube = Kv6::solid_cube(4, 0x8012_3456);
assert_eq!((cube.xsiz, cube.ysiz, cube.zsiz), (4, 4, 4));
assert!((cube.xpiv - 2.0).abs() < f32::EPSILON);
assert_eq!(cube.voxels.len(), 64 - 8);
assert!(cube
.voxels
.iter()
.all(|v| v.vis == 63 && v.col == 0x8012_3456));
assert_eq!(cube.xlen.len(), 4);
assert_eq!(cube.ylen.len(), 4);
assert!(cube.ylen.iter().all(|row| row.len() == 4));
let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
let ylen_sum: usize = cube
.ylen
.iter()
.flat_map(|r| r.iter())
.map(|&n| n as usize)
.sum();
assert_eq!(xlen_sum, cube.voxels.len());
assert_eq!(ylen_sum, cube.voxels.len());
}
#[test]
fn built_cube_round_trips_through_serialize_parse() {
let cube = Kv6::solid_cube(5, 0x80AB_CDEF);
let bytes = serialize(&cube);
let back = parse(&bytes).expect("parse built cube");
assert_eq!(back.xsiz, cube.xsiz);
assert_eq!(back.voxels.len(), cube.voxels.len());
assert_eq!(
serialize(&back),
bytes,
"serialize is stable across round-trip"
);
}
#[test]
fn from_fn_skips_air_and_keeps_z_order() {
let kv6 = Kv6::from_fn(1, 1, 2, |_, _, _| Some(0x8000_FF00));
assert_eq!(kv6.voxels.len(), 2);
assert_eq!(kv6.voxels[0].z, 0);
assert_eq!(kv6.voxels[1].z, 1);
assert_eq!(kv6.xlen, vec![2]);
assert_eq!(kv6.ylen, vec![vec![2]]);
}
#[test]
fn parse_coco_header() {
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
assert_eq!(kv6.xsiz, 9);
assert_eq!(kv6.ysiz, 11);
assert_eq!(kv6.zsiz, 9);
assert!((kv6.xpiv - 2.0).abs() < f32::EPSILON);
assert!((kv6.ypiv - 3.0).abs() < f32::EPSILON);
assert!((kv6.zpiv - 9.0).abs() < f32::EPSILON);
assert_eq!(kv6.voxels.len(), 148);
}
#[test]
fn coco_voxel_counts_consistent() {
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
assert_eq!(kv6.xlen.len(), kv6.xsiz as usize);
assert_eq!(kv6.ylen.len(), kv6.xsiz as usize);
for row in &kv6.ylen {
assert_eq!(row.len(), kv6.ysiz as usize);
}
let xlen_sum: u64 = kv6.xlen.iter().map(|&n| u64::from(n)).sum();
let ylen_sum: u64 = kv6
.ylen
.iter()
.flat_map(|row| row.iter().map(|&n| u64::from(n)))
.sum();
let nv = kv6.voxels.len() as u64;
assert_eq!(xlen_sum, nv);
assert_eq!(ylen_sum, nv);
}
#[test]
fn from_fn_shaded_keeps_from_fn_geometry() {
let fill = |x: u32, y: u32, z: u32| {
let on_face = x == 0 || x == 4 || y == 0 || y == 4 || z == 0 || z == 4;
on_face.then_some(0x80_44_55_66u32)
};
let flat = Kv6::from_fn(5, 5, 5, fill);
let shaded = Kv6::from_fn_shaded(5, 5, 5, fill);
assert_eq!(flat.voxels.len(), shaded.voxels.len());
assert_eq!(flat.xlen, shaded.xlen);
assert_eq!(flat.ylen, shaded.ylen);
for (f, s) in flat.voxels.iter().zip(&shaded.voxels) {
assert_eq!((f.col, f.z), (s.col, s.z));
}
assert!(
shaded.voxels.iter().any(|v| v.dir != 0),
"from_fn_shaded left every dir flat"
);
assert!(flat.voxels.iter().all(|v| v.dir == 0 && v.vis == 63));
}
#[test]
fn from_fn_shaded_column_z_faces() {
let kv = Kv6::from_fn_shaded(1, 1, 2, |_, _, _| Some(0x80_80_80_80));
assert_eq!(kv.voxels.len(), 2);
let (lower, upper) = (&kv.voxels[0], &kv.voxels[1]); assert_eq!(lower.z, 0);
assert_eq!(upper.z, 1);
assert_eq!(lower.vis & VIS_POS_Z, 0, "lower +z should be internal");
assert_eq!(lower.vis & VIS_NEG_Z, VIS_NEG_Z, "lower -z exposed");
assert_eq!(upper.vis & VIS_NEG_Z, 0, "upper -z should be internal");
assert_eq!(upper.vis & VIS_POS_Z, VIS_POS_Z, "upper +z exposed");
let sides = VIS_NEG_X | VIS_POS_X | VIS_NEG_Y | VIS_POS_Y;
assert_eq!(lower.vis & sides, sides);
assert_eq!(upper.vis & sides, sides);
}
#[test]
fn coco_vis_matches_authored_all_faces() {
use std::collections::HashMap;
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
let mut pos: HashMap<(u32, u32, u32), u8> = HashMap::new();
let mut vi = 0usize;
for x in 0..kv6.xsiz {
for y in 0..kv6.ysiz {
let len = kv6.ylen[x as usize][y as usize] as usize;
for _ in 0..len {
pos.insert((x, y, u32::from(kv6.voxels[vi].z)), kv6.voxels[vi].vis);
vi += 1;
}
}
}
let mut checked = 0u32;
for (&(x, y, z), &vis) in &pos {
let mut chk = |present: bool, bit: u8, face: &str| {
if present {
assert_eq!(
vis & bit,
0,
"coco ({x},{y},{z}): {face} internal but bit set"
);
checked += 1;
}
};
chk(pos.contains_key(&(x + 1, y, z)), VIS_POS_X, "+x");
chk(x > 0 && pos.contains_key(&(x - 1, y, z)), VIS_NEG_X, "-x");
chk(pos.contains_key(&(x, y + 1, z)), VIS_POS_Y, "+y");
chk(y > 0 && pos.contains_key(&(x, y - 1, z)), VIS_NEG_Y, "-y");
chk(pos.contains_key(&(x, y, z + 1)), VIS_POS_Z, "+z");
chk(z > 0 && pos.contains_key(&(x, y, z - 1)), VIS_NEG_Z, "-z");
}
assert!(
checked > 100,
"expected many adjacent faces in coco, got {checked}"
);
}
#[test]
fn recompute_surface_matches_from_fn_shaded() {
let fill = |x: u32, y: u32, z: u32| {
let cx = x as f32 - 4.0;
let cy = y as f32 - 4.0;
let cz = z as f32 - 4.0;
(cx * cx + cy * cy + cz * cz <= 16.0).then_some(0x80_30_60_90u32)
};
let shaded = Kv6::from_fn_shaded(9, 9, 9, fill);
let mut edited = Kv6::from_fn(9, 9, 9, fill); edited.recompute_surface(|x, y, z| {
x >= 0 && y >= 0 && z >= 0 && fill(x as u32, y as u32, z as u32).is_some()
});
assert_eq!(edited.voxels.len(), shaded.voxels.len());
for (e, s) in edited.voxels.iter().zip(&shaded.voxels) {
assert_eq!((e.vis, e.dir), (s.vis, s.dir), "voxel z={}", e.z);
}
}
#[test]
fn from_fn_shaded_slab_top_normal_points_up() {
use crate::equivec::univec;
let kv = Kv6::from_fn_shaded(8, 8, 12, |_, _, z| {
(2..=9).contains(&z).then_some(0x80_aa_aa_aa)
});
let v = kv
.voxels
.iter()
.enumerate()
.find_map(|(i, v)| {
let mut acc = 0usize;
for x in 0..kv.xsiz as usize {
for y in 0..kv.ysiz as usize {
let len = kv.ylen[x][y] as usize;
if i < acc + len {
return (x == 4 && y == 4 && v.z == 2).then_some(*v);
}
acc += len;
}
}
None
})
.expect("centre top-face voxel present");
let n = univec()[v.dir as usize];
assert!(
n[2] < -0.5,
"top-face normal should point -z (up), got {n:?}"
);
}
#[test]
fn coco_palette_present_and_matches_kvx() {
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
let pal = kv6.palette.as_ref().expect("SPal trailer present");
assert_eq!((pal[0].r, pal[0].g, pal[0].b), (0x3f, 0x19, 0x19));
}
#[test]
fn coco_first_voxel_packed_colour() {
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
let v0 = kv6.voxels[0];
assert_eq!(v0.col, 0x80fc_a460);
assert_eq!(v0.col & 0x8000_0000, 0x8000_0000);
}
#[test]
fn coco_roundtrips_byte_equal() {
let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
let out = serialize(&kv6);
assert_eq!(out.len(), COCO_KV6.len(), "length differs");
assert_eq!(out.as_slice(), COCO_KV6, "byte content differs");
}
#[test]
fn parse_truncated_header_fails() {
let r = parse(&[0u8; 16]);
assert!(matches!(r, Err(ParseError::TooSmall { .. })));
}
#[test]
fn parse_bad_magic_fails() {
let mut bad = COCO_KV6.to_vec();
bad[0] = b'X';
let r = parse(&bad);
assert!(matches!(r, Err(ParseError::BadMagic { .. })));
}
}