use crate::{
Error, PositionEncoding, Result, SampleEncoding, encode_hilbert, encode_varint, hilbert_bits,
pack_bits, packed_width,
};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
pub fn encode_voxj_object(
object: &VoxjCodecObject,
cell_counts: &[usize],
position: PositionEncoding,
sample: SampleEncoding,
) -> Result<VoxjSerdeObject> {
validate_object_shape(object)?;
let num_palettes = object.palette_refs.len();
let (voxel_positions, voxel_samples) = if object.positions.is_empty() {
(
VoxjSerdePositionBlock::RawJson(Vec::new()),
VoxjSerdeSampleBlock::RawJson(Vec::new()),
)
} else {
let (order, position_block) = encode_positions(object, position);
let channels = channels_in_order(&object.samples, &order, num_palettes);
let sample_block = encode_samples(&channels, sample, cell_counts, order.len());
(position_block, sample_block)
};
Ok(VoxjSerdeObject {
name: object.name.clone(),
palette_refs: object.palette_refs.clone(),
bounds: object.bounds,
voxel_positions,
voxel_samples,
})
}
fn validate_object_shape(object: &VoxjCodecObject) -> Result<()> {
if object.samples.len() != object.positions.len() {
return Err(Error::Invalid(format!(
"object \"{}\" has {} sample rows but {} positions",
object.name,
object.samples.len(),
object.positions.len()
)));
}
let num_palettes = object.palette_refs.len();
if let Some(row) = object.samples.iter().find(|row| row.len() != num_palettes) {
return Err(Error::Invalid(format!(
"object \"{}\" has a sample row of {} values but references {num_palettes} palettes",
object.name,
row.len()
)));
}
Ok(())
}
fn encode_positions(
object: &VoxjCodecObject,
encoding: PositionEncoding,
) -> (Vec<usize>, VoxjSerdePositionBlock) {
match encoding {
PositionEncoding::RawJson => raw_positions(&object.positions),
PositionEncoding::BitmapBase64 => bitmap_positions(&object.positions, object.bounds),
PositionEncoding::Hilbert => hilbert_positions(&object.positions, object.bounds),
}
}
fn raw_positions(positions: &[[u32; 3]]) -> (Vec<usize>, VoxjSerdePositionBlock) {
let order = (0..positions.len()).collect();
let block = VoxjSerdePositionBlock::RawJson(positions.to_vec());
(order, block)
}
fn cell_index(pos: [u32; 3], bounds: [u32; 3]) -> u64 {
let [x, y, z] = pos;
x as u64 * bounds[1] as u64 * bounds[2] as u64 + y as u64 * bounds[2] as u64 + z as u64
}
fn bitmap_positions(
positions: &[[u32; 3]],
bounds: [u32; 3],
) -> (Vec<usize>, VoxjSerdePositionBlock) {
let mut indexed: Vec<(u64, usize)> = positions
.iter()
.enumerate()
.map(|(i, &pos)| (cell_index(pos, bounds), i))
.collect();
indexed.sort_unstable();
let order = indexed.iter().map(|&(_, i)| i).collect();
let cells = bounds[0] as usize * bounds[1] as usize * bounds[2] as usize;
let mut bytes = vec![0u8; cells.div_ceil(8)];
for &(cell, _) in &indexed {
let c = cell as usize;
debug_assert!(c < cells, "voxel cell {c} outside {cells}-cell bounds");
bytes[c / 8] |= 1 << (7 - (c % 8));
}
let block = VoxjSerdePositionBlock::BitmapBase64(BASE64.encode(bytes));
(order, block)
}
fn hilbert_positions(
positions: &[[u32; 3]],
bounds: [u32; 3],
) -> (Vec<usize>, VoxjSerdePositionBlock) {
let bits = hilbert_bits(bounds);
let mut indexed: Vec<(u64, usize)> = positions
.iter()
.enumerate()
.map(|(i, &[x, y, z])| (encode_hilbert(x, y, z, bits), i))
.collect();
indexed.sort_unstable();
let order = indexed.iter().map(|&(_, i)| i).collect();
let mut prev = 0u64;
let deltas: Vec<u64> = indexed
.iter()
.map(|&(index, _)| {
let d = index - prev;
prev = index;
d
})
.collect();
let block = VoxjSerdePositionBlock::HilbertIndexDeltaVarintBase64(
BASE64.encode(encode_varint(&deltas)),
);
(order, block)
}
fn channels_in_order(samples: &[Vec<u32>], order: &[usize], num_palettes: usize) -> Vec<Vec<u32>> {
(0..num_palettes)
.map(|p| order.iter().map(|&i| samples[i][p]).collect())
.collect()
}
fn encode_samples(
channels: &[Vec<u32>],
encoding: SampleEncoding,
cell_counts: &[usize],
n: usize,
) -> VoxjSerdeSampleBlock {
match encoding {
SampleEncoding::RawJson => samples_raw(channels, n),
SampleEncoding::RleJson => samples_rle(channels),
SampleEncoding::PackedBase64 => samples_packed(channels, cell_counts),
}
}
fn samples_raw(channels: &[Vec<u32>], n: usize) -> VoxjSerdeSampleBlock {
let rows = (0..n)
.map(|k| channels.iter().map(|ch| ch[k]).collect())
.collect();
VoxjSerdeSampleBlock::RawJson(rows)
}
fn rle_encode(channel: &[u32]) -> Vec<u32> {
let mut out = Vec::new();
let mut iter = channel.iter().copied();
let Some(mut value) = iter.next() else {
return out;
};
let mut count = 1u32;
for v in iter {
if v == value {
count += 1;
} else {
out.push(value);
out.push(count);
value = v;
count = 1;
}
}
out.push(value);
out.push(count);
out
}
fn samples_rle(channels: &[Vec<u32>]) -> VoxjSerdeSampleBlock {
VoxjSerdeSampleBlock::RleJson(channels.iter().map(|ch| rle_encode(ch)).collect())
}
fn samples_packed(channels: &[Vec<u32>], cell_counts: &[usize]) -> VoxjSerdeSampleBlock {
let packed = channels
.iter()
.enumerate()
.map(|(p, ch)| {
let width = packed_width(cell_counts.get(p).copied().unwrap_or(1));
BASE64.encode(pack_bits(ch, width))
})
.collect();
VoxjSerdeSampleBlock::PackedBase64(packed)
}
#[cfg(test)]
mod tests {
use crate::{PositionEncoding, SampleEncoding, encode_voxj_object};
use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
fn assert_zero_palette_arity(object: &VoxjSerdeObject) {
match &object.voxel_samples {
VoxjSerdeSampleBlock::RawJson(rows) => assert_eq!(rows.len(), 3),
VoxjSerdeSampleBlock::RleJson(channels) => assert!(channels.is_empty()),
VoxjSerdeSampleBlock::PackedBase64(channels) => assert!(channels.is_empty()),
}
}
#[test]
fn zero_palette_object_keeps_sample_arity() {
assert_zero_palette_arity(
&encode_voxj_object(
&VoxjCodecObject {
name: "o".to_owned(),
palette_refs: Vec::new(),
bounds: [3, 1, 1],
positions: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
samples: vec![Vec::new(), Vec::new(), Vec::new()],
},
&[],
PositionEncoding::RawJson,
SampleEncoding::RawJson,
)
.unwrap(),
);
}
#[test]
fn fixed_encoding_uses_requested_blocks() {
let object = VoxjCodecObject {
name: "o".to_owned(),
palette_refs: vec![0],
bounds: [2, 1, 1],
positions: vec![[0, 0, 0], [1, 0, 0]],
samples: vec![vec![1], vec![2]],
};
let object = encode_voxj_object(
&object,
&[4],
PositionEncoding::BitmapBase64,
SampleEncoding::PackedBase64,
)
.unwrap();
assert!(matches!(
object.voxel_positions,
VoxjSerdePositionBlock::BitmapBase64(_)
));
assert!(matches!(
object.voxel_samples,
VoxjSerdeSampleBlock::PackedBase64(_)
));
}
#[test]
fn rejects_ragged_object() {
let wrong_row_arity = VoxjCodecObject {
name: "o".to_owned(),
palette_refs: vec![0, 1],
bounds: [1, 1, 1],
positions: vec![[0, 0, 0]],
samples: vec![vec![1]],
};
assert!(
encode_voxj_object(
&wrong_row_arity,
&[4, 4],
PositionEncoding::RawJson,
SampleEncoding::RawJson,
)
.is_err()
);
let wrong_sample_count = VoxjCodecObject {
name: "o".to_owned(),
palette_refs: vec![0],
bounds: [2, 1, 1],
positions: vec![[0, 0, 0], [1, 0, 0]],
samples: vec![vec![1]],
};
assert!(
encode_voxj_object(
&wrong_sample_count,
&[4],
PositionEncoding::RawJson,
SampleEncoding::RawJson,
)
.is_err()
);
}
}