Skip to main content

voxj_codec/
encode_voxj_object_smallest.rs

1use crate::{PositionEncoding, Result, 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`],
14/// trying every applicable non-raw encoding pairing, deflating each, and
15/// keeping the smallest. The canonical shipping form. `cell_counts[p]` is the
16/// cell count of the palette referenced by `object.palette_refs[p]`;
17/// [`voxj_palette_cell_counts`](crate::voxj_palette_cell_counts) computes it
18/// from the document's palettes.
19pub fn encode_voxj_object_smallest(
20    object: &VoxjCodecObject,
21    cell_counts: &[usize],
22) -> Result<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    let smallest = 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        .collect::<Result<Vec<_>>>()?
38        .into_iter()
39        .min_by_key(deflated_len)
40        .expect("at least one candidate");
41    Ok(smallest)
42}
43
44/// The applicable non-raw position encodings for `bounds`, falling back to raw
45/// only when neither bitmap nor Hilbert applies.
46fn candidate_positions(bounds: [u32; 3]) -> Vec<PositionEncoding> {
47    let mut positions = Vec::new();
48
49    let cells = bounds[0] as u64 * bounds[1] as u64 * bounds[2] as u64;
50    if cells <= MAX_BITMAP_CELLS {
51        positions.push(PositionEncoding::BitmapBase64);
52    }
53
54    if hilbert_bits(bounds) <= MAX_HILBERT_BITS {
55        positions.push(PositionEncoding::Hilbert);
56    }
57
58    if positions.is_empty() {
59        positions.push(PositionEncoding::RawJson);
60    }
61
62    positions
63}
64
65/// Deflated byte length of an object's two blocks serialized together.
66fn deflated_len(object: &VoxjSerdeObject) -> usize {
67    let Ok(json) = serde_json::to_vec(&(&object.voxel_positions, &object.voxel_samples)) else {
68        return usize::MAX;
69    };
70    let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
71    let _ = encoder.write_all(&json);
72    encoder.finish().map_or(usize::MAX, |v| v.len())
73}
74
75#[cfg(test)]
76mod tests {
77    use crate::encode_voxj_object_smallest;
78    use voxj::{VoxjCodecObject, VoxjSerdeSampleBlock};
79
80    /// An object with voxels but zero palettes still emits sample channels
81    /// whose arity matches the position block (rle/packed carry zero channels).
82    #[test]
83    fn zero_palette_object_keeps_sample_arity() {
84        let object = encode_voxj_object_smallest(
85            &VoxjCodecObject {
86                name: "o".to_owned(),
87                palette_refs: Vec::new(),
88                bounds: [3, 1, 1],
89                positions: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
90                samples: vec![Vec::new(), Vec::new(), Vec::new()],
91            },
92            &[],
93        )
94        .unwrap();
95        match &object.voxel_samples {
96            VoxjSerdeSampleBlock::RawJson(rows) => assert_eq!(rows.len(), 3),
97            VoxjSerdeSampleBlock::RleJson(channels) => assert!(channels.is_empty()),
98            VoxjSerdeSampleBlock::PackedBase64(channels) => assert!(channels.is_empty()),
99        }
100    }
101}