Skip to main content

voxj_codec/
encode_voxj_object_smallest.rs

1use crate::{PositionEncoding, SampleEncoding, encode_voxj_object, hilbert_bits};
2use flate2::{Compression, write::DeflateEncoder};
3use std::io::Write;
4use voxj::{VoxjCodecObject, VoxjSerdeObject};
5
6/// Skip the dense bitmap candidate above this many cells to bound memory.
7const MAX_BITMAP_CELLS: u64 = 8_000_000;
8
9/// Hilbert positions are only valid for `bits <= 17` (every bounds dimension
10/// `<= 131072`); above that the format requires bitmap or raw instead.
11const MAX_HILBERT_BITS: u32 = 17;
12
13/// Encodes one [`VoxjCodecObject`]'s geometry into a [`VoxjSerdeObject`], trying every
14/// applicable non-raw encoding pairing, deflating each, and keeping the
15/// smallest. The canonical shipping form. `cell_counts[p]` is the cell count of
16/// the palette referenced by `object.palette_refs[p]`;
17/// [`voxj_palette_cell_counts`](crate::voxj_palette_cell_counts) computes it from the
18/// document's palettes.
19pub fn encode_voxj_object_smallest(
20    object: &VoxjCodecObject,
21    cell_counts: &[usize],
22) -> VoxjSerdeObject {
23    if object.positions.is_empty() {
24        return encode_voxj_object(
25            object,
26            cell_counts,
27            PositionEncoding::RawJson,
28            SampleEncoding::RawJson,
29        );
30    }
31    candidate_positions(object.bounds)
32        .into_iter()
33        .flat_map(|position| {
34            [SampleEncoding::RleJson, SampleEncoding::PackedBase64].map(|sample| (position, sample))
35        })
36        .map(|(position, sample)| encode_voxj_object(object, cell_counts, position, sample))
37        .min_by_key(deflated_len)
38        .expect("at least one candidate")
39}
40
41/// The applicable non-raw position encodings for `bounds`, falling back to raw
42/// only when neither bitmap nor Hilbert applies.
43fn candidate_positions(bounds: [u32; 3]) -> Vec<PositionEncoding> {
44    let mut positions = Vec::new();
45
46    let cells = bounds[0] as u64 * bounds[1] as u64 * bounds[2] as u64;
47    if cells <= MAX_BITMAP_CELLS {
48        positions.push(PositionEncoding::BitmapBase64);
49    }
50
51    if hilbert_bits(bounds) <= MAX_HILBERT_BITS {
52        positions.push(PositionEncoding::Hilbert);
53    }
54
55    if positions.is_empty() {
56        positions.push(PositionEncoding::RawJson);
57    }
58
59    positions
60}
61
62/// Deflated byte length of an object's two blocks serialized together.
63fn deflated_len(object: &VoxjSerdeObject) -> usize {
64    let Ok(json) = serde_json::to_vec(&(&object.voxel_positions, &object.voxel_samples)) else {
65        return usize::MAX;
66    };
67    let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
68    let _ = encoder.write_all(&json);
69    encoder.finish().map_or(usize::MAX, |v| v.len())
70}
71
72#[cfg(test)]
73mod tests {
74    use crate::encode_voxj_object_smallest;
75    use voxj::{VoxjCodecObject, VoxjSerdeSampleBlock};
76
77    /// An object with voxels but zero palettes still emits sample channels
78    /// whose arity matches the position block (rle/packed carry zero channels).
79    #[test]
80    fn zero_palette_object_keeps_sample_arity() {
81        let object = encode_voxj_object_smallest(
82            &VoxjCodecObject {
83                name: "o".to_owned(),
84                palette_refs: Vec::new(),
85                bounds: [3, 1, 1],
86                positions: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
87                samples: vec![Vec::new(), Vec::new(), Vec::new()],
88            },
89            &[],
90        );
91        match &object.voxel_samples {
92            VoxjSerdeSampleBlock::RawJson(rows) => assert_eq!(rows.len(), 3),
93            VoxjSerdeSampleBlock::RleJson(channels) => assert!(channels.is_empty()),
94            VoxjSerdeSampleBlock::PackedBase64(channels) => assert!(channels.is_empty()),
95        }
96    }
97}