use std::{
fs::File,
io::{
Cursor,
Read,
Seek,
},
path::Path,
str::from_utf8,
};
use byteorder::{
ReadBytesExt,
LE,
};
use thiserror::Error;
use crate::{
chunk::{
read_main_chunk,
Chunk,
ChunkId,
},
data::{
VoxBuffer,
VoxData,
},
types::{
Palette,
Size,
Version,
Voxel,
},
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Expected file header to start with b'VOX ', but got: {got:?}.")]
InvalidMagic { got: [u8; 4] },
#[error("Unsupported file version: {version}")]
UnsupportedFileVersion { version: Version },
#[error("Expected MAIN chunk, but read chunk with ID: {0:?}", got.id())]
ExpectedMainChunk { got: Chunk },
#[error("Found {} SIZE chunks, {} XYZI chunks.", .size_chunks.len(), .xyzi_chunks.len())]
InvalidNumberOfSizeAndXyziChunks {
size_chunks: Vec<Chunk>,
xyzi_chunks: Vec<Chunk>,
},
#[error("Found multiple RGBA chunks (at {} and {}).", .chunks[0].offset(), chunks[1].offset())]
MultipleRgbaChunks { chunks: [Chunk; 2] },
#[error("Invalid material type: {material_type}")]
InvalidMaterial { material_type: u8 },
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("Failed to decode UTF-8 string")]
Utf8(#[from] std::string::FromUtf8Error),
}
pub fn read_vox_into<R: Read + Seek, B: VoxBuffer>(
mut reader: R,
buffer: &mut B,
) -> Result<(), Error> {
let (main_chunk, version) = read_main_chunk(&mut reader)?;
buffer.set_version(version);
log::trace!("main chunk: {:#?}", main_chunk);
let mut size_chunks = vec![];
let mut xyzi_chunks = vec![];
let mut rgba_chunk = None;
let mut transform_chunks = vec![];
let mut group_chunks = vec![];
let mut shape_chunks = vec![];
let mut layer_chunks = vec![];
for r in main_chunk.children(&mut reader) {
let chunk = r?;
match chunk.id() {
ChunkId::Size => size_chunks.push(chunk),
ChunkId::Xyzi => xyzi_chunks.push(chunk),
ChunkId::Rgba => {
if rgba_chunk.is_some() {
return Err(Error::MultipleRgbaChunks {
chunks: [rgba_chunk.take().unwrap(), chunk],
});
}
rgba_chunk = Some(chunk);
}
ChunkId::NTrn => transform_chunks.push(chunk),
ChunkId::NGrp => group_chunks.push(chunk),
ChunkId::NShp => shape_chunks.push(chunk),
ChunkId::Layr => layer_chunks.push(chunk),
ChunkId::Unsupported(raw) => {
let str_opt = from_utf8(&raw).ok();
log::debug!("Skipping unsupported chunk: {:?} ({:?})", raw, str_opt);
}
id => log::trace!("Skipping unimplemented chunk: {:?}", id),
}
}
if let Some(rgba_chunk) = rgba_chunk {
log::trace!("read RGBA chunk");
let palette = Palette::read(rgba_chunk.content(&mut reader)?)?;
buffer.set_palette(palette);
}
else {
log::trace!("no RGBA chunk found");
}
if xyzi_chunks.len() != size_chunks.len() {
return Err(Error::InvalidNumberOfSizeAndXyziChunks {
size_chunks,
xyzi_chunks,
});
}
let num_models = size_chunks.len();
log::trace!("num_models = {}", num_models);
buffer.set_num_models(num_models);
for (size_chunk, xyzi_chunk) in size_chunks.into_iter().zip(xyzi_chunks) {
let model_size = Size::read(size_chunk.content(&mut reader)?)?;
log::trace!("model_size = {:?}", model_size);
buffer.set_model_size(model_size);
let mut reader = xyzi_chunk.content(&mut reader)?;
let num_voxels = reader.read_u32::<LE>()?;
log::trace!("num_voxels = {}", num_voxels);
for _ in 0..num_voxels {
let voxel = Voxel::read(&mut reader)?;
log::trace!("voxel = {:?}", voxel);
buffer.set_voxel(voxel);
}
}
Ok(())
}
pub fn from_reader<R: Read + Seek>(reader: R) -> Result<VoxData, Error> {
let mut buffer = VoxData::default();
read_vox_into(reader, &mut buffer)?;
Ok(buffer)
}
pub fn from_slice(slice: &[u8]) -> Result<VoxData, Error> {
from_reader(Cursor::new(slice))
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<VoxData, Error> {
from_reader(File::open(path)?)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::from_slice;
use crate::types::{
Color,
ColorIndex,
Model,
Point,
Vector,
Voxel,
};
fn glider() -> Vec<Voxel> {
vec![
Voxel::new([0, 0, 1], 79),
Voxel::new([1, 0, 0], 79),
Voxel::new([2, 0, 0], 79),
Voxel::new([2, 0, 1], 69),
Voxel::new([2, 0, 2], 69),
]
}
fn glider2() -> Vec<Voxel> {
vec![
Voxel::new([0, 2, 0], 79),
Voxel::new([1, 1, 0], 79),
Voxel::new([2, 1, 0], 79),
Voxel::new([1, 0, 0], 79),
Voxel::new([0, 0, 0], 79),
]
}
fn assert_voxels(model: &Model, expected: &[Voxel]) {
let voxels = model
.voxels
.iter()
.map(|voxel| (voxel.point, voxel.color_index))
.collect::<HashMap<Point, ColorIndex>>();
for expected_voxel in expected {
let voxel = voxels.get(&Vector::from(expected_voxel.point)).copied();
assert_eq!(
voxel,
Some(expected_voxel.color_index),
"Expected right at {:?}",
expected_voxel.point
);
}
}
#[test]
fn it_reads_files_without_models() {
let vox = from_slice(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../test_files/test_no_models.vox"
)))
.unwrap();
assert!(vox.models.is_empty());
}
#[test]
fn it_reads_a_single_model() {
let vox = from_slice(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../test_files/test_single_model_default_palette.vox"
)))
.unwrap();
assert_eq!(vox.models.len(), 1);
assert!(vox.palette.is_default());
let model = &vox.models[0];
assert_eq!(model.size, Vector::new(3, 1, 3));
assert_voxels(model, &glider());
}
#[test]
fn it_reads_multiple_models() {
let vox = from_slice(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../test_files/test_multiple_models.vox"
)))
.unwrap();
assert_eq!(vox.models.len(), 2);
let model2 = &vox.models[0];
assert_eq!(model2.size, Vector::new(3, 3, 1));
assert_voxels(model2, &glider2());
let model1 = &vox.models[1];
assert_eq!(model1.size, Vector::new(3, 1, 3));
assert_voxels(model1, &glider());
}
#[test]
fn it_reads_a_custom_palette() {
let vox = from_slice(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../test_files/test_custom_palette.vox"
)))
.unwrap();
assert!(!vox.palette.is_default());
assert_eq!(vox.palette[79.into()], Color::light_blue());
assert_eq!(vox.palette[69.into()], Color::new(108, 0, 204, 255));
}
#[test]
fn color_indices_work_as_expected() {
let vox = from_slice(include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../test_files/test_single_model_default_palette.vox"
)))
.unwrap();
let color_index = vox
.models
.get(0)
.unwrap()
.voxels
.first()
.unwrap()
.color_index;
assert_eq!(vox.palette[color_index], Color::light_blue());
}
}