Skip to main content

voxj_codec/
encode_voxj_object.rs

1use crate::{
2    PositionEncoding, SampleEncoding, encode_hilbert, encode_varint, hilbert_bits, pack_bits,
3    packed_width,
4};
5use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
6use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
7
8/// Encodes one [`VoxjCodecObject`]'s geometry into a [`VoxjSerdeObject`] with the
9/// given fixed position and sample encodings. `cell_counts[p]` is the cell count
10/// of the palette referenced by `object.palette_refs[p]`, used to derive the bit
11/// width of `packed-base64` samples;
12/// [`voxj_palette_cell_counts`](crate::voxj_palette_cell_counts) computes it from the
13/// document's palettes.
14pub fn encode_voxj_object(
15    object: &VoxjCodecObject,
16    cell_counts: &[usize],
17    position: PositionEncoding,
18    sample: SampleEncoding,
19) -> VoxjSerdeObject {
20    let num_palettes = object.palette_refs.len();
21
22    let (voxel_positions, voxel_samples) = if object.positions.is_empty() {
23        (
24            VoxjSerdePositionBlock::RawJson(Vec::new()),
25            VoxjSerdeSampleBlock::RawJson(Vec::new()),
26        )
27    } else {
28        let (order, position_block) = encode_positions(object, position);
29        let channels = channels_in_order(&object.samples, &order, num_palettes);
30        let sample_block = encode_samples(&channels, sample, cell_counts, order.len());
31        (position_block, sample_block)
32    };
33
34    VoxjSerdeObject {
35        name: object.name.clone(),
36        palette_refs: object.palette_refs.clone(),
37        bounds: object.bounds,
38        voxel_positions,
39        voxel_samples,
40    }
41}
42
43/// Encodes the voxel positions with `encoding`, returning the canonical voxel
44/// order and the block.
45fn encode_positions(
46    object: &VoxjCodecObject,
47    encoding: PositionEncoding,
48) -> (Vec<usize>, VoxjSerdePositionBlock) {
49    match encoding {
50        PositionEncoding::RawJson => raw_positions(&object.positions),
51        PositionEncoding::BitmapBase64 => bitmap_positions(&object.positions, object.bounds),
52        PositionEncoding::Hilbert => hilbert_positions(&object.positions, object.bounds),
53    }
54}
55
56/// Listing order `0..n` paired with the raw block. The raw encoding never
57/// reorders voxels, so positions pass through unchanged and the order is the
58/// identity permutation.
59fn raw_positions(positions: &[[u32; 3]]) -> (Vec<usize>, VoxjSerdePositionBlock) {
60    let order = (0..positions.len()).collect();
61    let block = VoxjSerdePositionBlock::RawJson(positions.to_vec());
62    (order, block)
63}
64
65/// Raster cell index `k = x*Y*Z + y*Z + z`.
66fn cell_index(pos: [u32; 3], bounds: [u32; 3]) -> u64 {
67    let [x, y, z] = pos;
68    x as u64 * bounds[1] as u64 * bounds[2] as u64 + y as u64 * bounds[2] as u64 + z as u64
69}
70
71/// Voxel order ascending by raster cell index, paired with a dense occupancy
72/// bitmap: bit `k` (MSB-first, 8 per byte) is set when raster cell `k` holds a
73/// voxel. Each cell index is computed exactly once, by sorting `(cell, voxel)`
74/// pairs, and shared between the order permutation and the packed bits.
75fn bitmap_positions(
76    positions: &[[u32; 3]],
77    bounds: [u32; 3],
78) -> (Vec<usize>, VoxjSerdePositionBlock) {
79    let mut indexed: Vec<(u64, usize)> = positions
80        .iter()
81        .enumerate()
82        .map(|(i, &pos)| (cell_index(pos, bounds), i))
83        .collect();
84    indexed.sort_unstable();
85
86    let order = indexed.iter().map(|&(_, i)| i).collect();
87
88    // Pack the bits directly instead of filling a one-u32-per-cell occupancy
89    // buffer and packing it afterward. Every position lies within bounds, so
90    // its cell index is < cells.
91    let cells = bounds[0] as usize * bounds[1] as usize * bounds[2] as usize;
92    let mut bytes = vec![0u8; cells.div_ceil(8)];
93    for &(cell, _) in &indexed {
94        let c = cell as usize;
95        debug_assert!(c < cells, "voxel cell {c} outside {cells}-cell bounds");
96        bytes[c / 8] |= 1 << (7 - (c % 8));
97    }
98    let block = VoxjSerdePositionBlock::BitmapBase64(BASE64.encode(bytes));
99    (order, block)
100}
101
102/// Voxel order ascending by Hilbert index, paired with the delta-varint
103/// position block. Each voxel's Hilbert index is computed exactly once and
104/// shared between the order permutation and the encoded deltas. Sorting
105/// `(index, original_voxel)` pairs yields both in a single pass.
106fn hilbert_positions(
107    positions: &[[u32; 3]],
108    bounds: [u32; 3],
109) -> (Vec<usize>, VoxjSerdePositionBlock) {
110    let bits = hilbert_bits(bounds);
111    let mut indexed: Vec<(u64, usize)> = positions
112        .iter()
113        .enumerate()
114        .map(|(i, &[x, y, z])| (encode_hilbert(x, y, z, bits), i))
115        .collect();
116    indexed.sort_unstable();
117
118    let order = indexed.iter().map(|&(_, i)| i).collect();
119    let mut prev = 0u64;
120    let deltas: Vec<u64> = indexed
121        .iter()
122        .map(|&(index, _)| {
123            let d = index - prev;
124            prev = index;
125            d
126        })
127        .collect();
128    let block = VoxjSerdePositionBlock::HilbertIndexDeltaVarintBase64(
129        BASE64.encode(encode_varint(&deltas)),
130    );
131    (order, block)
132}
133
134/// Reorders `samples[voxel][palette]` into one channel per palette, in the
135/// position block's voxel order.
136fn channels_in_order(samples: &[Vec<u32>], order: &[usize], num_palettes: usize) -> Vec<Vec<u32>> {
137    (0..num_palettes)
138        .map(|p| order.iter().map(|&i| samples[i][p]).collect())
139        .collect()
140}
141
142/// Encodes the per-palette sample `channels` (already in the position block's
143/// voxel order) with `encoding`. `n` is the voxel count.
144fn encode_samples(
145    channels: &[Vec<u32>],
146    encoding: SampleEncoding,
147    cell_counts: &[usize],
148    n: usize,
149) -> VoxjSerdeSampleBlock {
150    match encoding {
151        SampleEncoding::RawJson => samples_raw(channels, n),
152        SampleEncoding::RleJson => samples_rle(channels),
153        SampleEncoding::PackedBase64 => samples_packed(channels, cell_counts),
154    }
155}
156
157/// Builds one row per voxel, each holding that voxel's cell index per palette.
158/// `n` is the voxel count, sourced independently of `channels` so an object
159/// with voxels but zero palettes still emits `n` empty rows (matching the
160/// position block's voxel count).
161fn samples_raw(channels: &[Vec<u32>], n: usize) -> VoxjSerdeSampleBlock {
162    let rows = (0..n)
163        .map(|k| channels.iter().map(|ch| ch[k]).collect())
164        .collect();
165    VoxjSerdeSampleBlock::RawJson(rows)
166}
167
168/// Flat run-length encoding: `[value1, count1, value2, count2, ...]`.
169fn rle_encode(channel: &[u32]) -> Vec<u32> {
170    let mut out = Vec::new();
171    let mut iter = channel.iter().copied();
172    let Some(mut value) = iter.next() else {
173        return out;
174    };
175    let mut count = 1u32;
176    for v in iter {
177        if v == value {
178            count += 1;
179        } else {
180            out.push(value);
181            out.push(count);
182            value = v;
183            count = 1;
184        }
185    }
186    out.push(value);
187    out.push(count);
188    out
189}
190
191fn samples_rle(channels: &[Vec<u32>]) -> VoxjSerdeSampleBlock {
192    VoxjSerdeSampleBlock::RleJson(channels.iter().map(|ch| rle_encode(ch)).collect())
193}
194
195fn samples_packed(channels: &[Vec<u32>], cell_counts: &[usize]) -> VoxjSerdeSampleBlock {
196    let packed = channels
197        .iter()
198        .enumerate()
199        .map(|(p, ch)| {
200            let width = packed_width(cell_counts.get(p).copied().unwrap_or(1));
201            BASE64.encode(pack_bits(ch, width))
202        })
203        .collect();
204    VoxjSerdeSampleBlock::PackedBase64(packed)
205}
206
207#[cfg(test)]
208mod tests {
209    use crate::{PositionEncoding, SampleEncoding, encode_voxj_object};
210    use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
211
212    /// An object with voxels but zero palettes must still emit a sample block
213    /// whose arity matches the position block: raw-json carries one (empty) row
214    /// per voxel, and rle/packed carry zero channels.
215    fn assert_zero_palette_arity(object: &VoxjSerdeObject) {
216        match &object.voxel_samples {
217            VoxjSerdeSampleBlock::RawJson(rows) => assert_eq!(rows.len(), 3),
218            VoxjSerdeSampleBlock::RleJson(channels) => assert!(channels.is_empty()),
219            VoxjSerdeSampleBlock::PackedBase64(channels) => assert!(channels.is_empty()),
220        }
221    }
222
223    #[test]
224    fn zero_palette_object_keeps_sample_arity() {
225        assert_zero_palette_arity(&encode_voxj_object(
226            &VoxjCodecObject {
227                name: "o".to_owned(),
228                palette_refs: Vec::new(),
229                bounds: [3, 1, 1],
230                positions: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
231                samples: vec![Vec::new(), Vec::new(), Vec::new()],
232            },
233            &[],
234            PositionEncoding::RawJson,
235            SampleEncoding::RawJson,
236        ));
237    }
238
239    /// A fixed encoding produces exactly the requested blocks.
240    #[test]
241    fn fixed_encoding_uses_requested_blocks() {
242        let object = VoxjCodecObject {
243            name: "o".to_owned(),
244            palette_refs: vec![0],
245            bounds: [2, 1, 1],
246            positions: vec![[0, 0, 0], [1, 0, 0]],
247            samples: vec![vec![1], vec![2]],
248        };
249        let object = encode_voxj_object(
250            &object,
251            &[4],
252            PositionEncoding::BitmapBase64,
253            SampleEncoding::PackedBase64,
254        );
255        assert!(matches!(
256            object.voxel_positions,
257            VoxjSerdePositionBlock::BitmapBase64(_)
258        ));
259        assert!(matches!(
260            object.voxel_samples,
261            VoxjSerdeSampleBlock::PackedBase64(_)
262        ));
263    }
264}