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
8pub 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
43fn 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
56fn 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
65fn 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
71fn 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 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
102fn 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
134fn 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
142fn 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
157fn 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
168fn 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 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 #[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}