1use crate::{
2 Error, PositionEncoding, Result, SampleEncoding, encode_hilbert, encode_varint, hilbert_bits,
3 pack_bits, 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) -> Result<VoxjSerdeObject> {
20 validate_object_shape(object)?;
21 let num_palettes = object.palette_refs.len();
22
23 let (voxel_positions, voxel_samples) = if object.positions.is_empty() {
24 (
25 VoxjSerdePositionBlock::RawJson(Vec::new()),
26 VoxjSerdeSampleBlock::RawJson(Vec::new()),
27 )
28 } else {
29 let (order, position_block) = encode_positions(object, position);
30 let channels = channels_in_order(&object.samples, &order, num_palettes);
31 let sample_block = encode_samples(&channels, sample, cell_counts, order.len());
32 (position_block, sample_block)
33 };
34
35 Ok(VoxjSerdeObject {
36 name: object.name.clone(),
37 palette_refs: object.palette_refs.clone(),
38 bounds: object.bounds,
39 voxel_positions,
40 voxel_samples,
41 })
42}
43
44fn validate_object_shape(object: &VoxjCodecObject) -> Result<()> {
49 if object.samples.len() != object.positions.len() {
50 return Err(Error::Invalid(format!(
51 "object \"{}\" has {} sample rows but {} positions",
52 object.name,
53 object.samples.len(),
54 object.positions.len()
55 )));
56 }
57 let num_palettes = object.palette_refs.len();
58 if let Some(row) = object.samples.iter().find(|row| row.len() != num_palettes) {
59 return Err(Error::Invalid(format!(
60 "object \"{}\" has a sample row of {} values but references {num_palettes} palettes",
61 object.name,
62 row.len()
63 )));
64 }
65 Ok(())
66}
67
68fn encode_positions(
71 object: &VoxjCodecObject,
72 encoding: PositionEncoding,
73) -> (Vec<usize>, VoxjSerdePositionBlock) {
74 match encoding {
75 PositionEncoding::RawJson => raw_positions(&object.positions),
76 PositionEncoding::BitmapBase64 => bitmap_positions(&object.positions, object.bounds),
77 PositionEncoding::Hilbert => hilbert_positions(&object.positions, object.bounds),
78 }
79}
80
81fn raw_positions(positions: &[[u32; 3]]) -> (Vec<usize>, VoxjSerdePositionBlock) {
85 let order = (0..positions.len()).collect();
86 let block = VoxjSerdePositionBlock::RawJson(positions.to_vec());
87 (order, block)
88}
89
90fn cell_index(pos: [u32; 3], bounds: [u32; 3]) -> u64 {
92 let [x, y, z] = pos;
93 x as u64 * bounds[1] as u64 * bounds[2] as u64 + y as u64 * bounds[2] as u64 + z as u64
94}
95
96fn bitmap_positions(
101 positions: &[[u32; 3]],
102 bounds: [u32; 3],
103) -> (Vec<usize>, VoxjSerdePositionBlock) {
104 let mut indexed: Vec<(u64, usize)> = positions
105 .iter()
106 .enumerate()
107 .map(|(i, &pos)| (cell_index(pos, bounds), i))
108 .collect();
109 indexed.sort_unstable();
110
111 let order = indexed.iter().map(|&(_, i)| i).collect();
112
113 let cells = bounds[0] as usize * bounds[1] as usize * bounds[2] as usize;
117 let mut bytes = vec![0u8; cells.div_ceil(8)];
118 for &(cell, _) in &indexed {
119 let c = cell as usize;
120 debug_assert!(c < cells, "voxel cell {c} outside {cells}-cell bounds");
121 bytes[c / 8] |= 1 << (7 - (c % 8));
122 }
123 let block = VoxjSerdePositionBlock::BitmapBase64(BASE64.encode(bytes));
124 (order, block)
125}
126
127fn hilbert_positions(
132 positions: &[[u32; 3]],
133 bounds: [u32; 3],
134) -> (Vec<usize>, VoxjSerdePositionBlock) {
135 let bits = hilbert_bits(bounds);
136 let mut indexed: Vec<(u64, usize)> = positions
137 .iter()
138 .enumerate()
139 .map(|(i, &[x, y, z])| (encode_hilbert(x, y, z, bits), i))
140 .collect();
141 indexed.sort_unstable();
142
143 let order = indexed.iter().map(|&(_, i)| i).collect();
144 let mut prev = 0u64;
145 let deltas: Vec<u64> = indexed
146 .iter()
147 .map(|&(index, _)| {
148 let d = index - prev;
149 prev = index;
150 d
151 })
152 .collect();
153 let block = VoxjSerdePositionBlock::HilbertIndexDeltaVarintBase64(
154 BASE64.encode(encode_varint(&deltas)),
155 );
156 (order, block)
157}
158
159fn channels_in_order(samples: &[Vec<u32>], order: &[usize], num_palettes: usize) -> Vec<Vec<u32>> {
162 (0..num_palettes)
163 .map(|p| order.iter().map(|&i| samples[i][p]).collect())
164 .collect()
165}
166
167fn encode_samples(
170 channels: &[Vec<u32>],
171 encoding: SampleEncoding,
172 cell_counts: &[usize],
173 n: usize,
174) -> VoxjSerdeSampleBlock {
175 match encoding {
176 SampleEncoding::RawJson => samples_raw(channels, n),
177 SampleEncoding::RleJson => samples_rle(channels),
178 SampleEncoding::PackedBase64 => samples_packed(channels, cell_counts),
179 }
180}
181
182fn samples_raw(channels: &[Vec<u32>], n: usize) -> VoxjSerdeSampleBlock {
187 let rows = (0..n)
188 .map(|k| channels.iter().map(|ch| ch[k]).collect())
189 .collect();
190 VoxjSerdeSampleBlock::RawJson(rows)
191}
192
193fn rle_encode(channel: &[u32]) -> Vec<u32> {
195 let mut out = Vec::new();
196 let mut iter = channel.iter().copied();
197 let Some(mut value) = iter.next() else {
198 return out;
199 };
200 let mut count = 1u32;
201 for v in iter {
202 if v == value {
203 count += 1;
204 } else {
205 out.push(value);
206 out.push(count);
207 value = v;
208 count = 1;
209 }
210 }
211 out.push(value);
212 out.push(count);
213 out
214}
215
216fn samples_rle(channels: &[Vec<u32>]) -> VoxjSerdeSampleBlock {
217 VoxjSerdeSampleBlock::RleJson(channels.iter().map(|ch| rle_encode(ch)).collect())
218}
219
220fn samples_packed(channels: &[Vec<u32>], cell_counts: &[usize]) -> VoxjSerdeSampleBlock {
221 let packed = channels
222 .iter()
223 .enumerate()
224 .map(|(p, ch)| {
225 let width = packed_width(cell_counts.get(p).copied().unwrap_or(1));
226 BASE64.encode(pack_bits(ch, width))
227 })
228 .collect();
229 VoxjSerdeSampleBlock::PackedBase64(packed)
230}
231
232#[cfg(test)]
233mod tests {
234 use crate::{PositionEncoding, SampleEncoding, encode_voxj_object};
235 use voxj::{VoxjCodecObject, VoxjSerdeObject, VoxjSerdePositionBlock, VoxjSerdeSampleBlock};
236
237 fn assert_zero_palette_arity(object: &VoxjSerdeObject) {
241 match &object.voxel_samples {
242 VoxjSerdeSampleBlock::RawJson(rows) => assert_eq!(rows.len(), 3),
243 VoxjSerdeSampleBlock::RleJson(channels) => assert!(channels.is_empty()),
244 VoxjSerdeSampleBlock::PackedBase64(channels) => assert!(channels.is_empty()),
245 }
246 }
247
248 #[test]
249 fn zero_palette_object_keeps_sample_arity() {
250 assert_zero_palette_arity(
251 &encode_voxj_object(
252 &VoxjCodecObject {
253 name: "o".to_owned(),
254 palette_refs: Vec::new(),
255 bounds: [3, 1, 1],
256 positions: vec![[0, 0, 0], [1, 0, 0], [2, 0, 0]],
257 samples: vec![Vec::new(), Vec::new(), Vec::new()],
258 },
259 &[],
260 PositionEncoding::RawJson,
261 SampleEncoding::RawJson,
262 )
263 .unwrap(),
264 );
265 }
266
267 #[test]
269 fn fixed_encoding_uses_requested_blocks() {
270 let object = VoxjCodecObject {
271 name: "o".to_owned(),
272 palette_refs: vec![0],
273 bounds: [2, 1, 1],
274 positions: vec![[0, 0, 0], [1, 0, 0]],
275 samples: vec![vec![1], vec![2]],
276 };
277 let object = encode_voxj_object(
278 &object,
279 &[4],
280 PositionEncoding::BitmapBase64,
281 SampleEncoding::PackedBase64,
282 )
283 .unwrap();
284 assert!(matches!(
285 object.voxel_positions,
286 VoxjSerdePositionBlock::BitmapBase64(_)
287 ));
288 assert!(matches!(
289 object.voxel_samples,
290 VoxjSerdeSampleBlock::PackedBase64(_)
291 ));
292 }
293
294 #[test]
298 fn rejects_ragged_object() {
299 let wrong_row_arity = VoxjCodecObject {
300 name: "o".to_owned(),
301 palette_refs: vec![0, 1],
302 bounds: [1, 1, 1],
303 positions: vec![[0, 0, 0]],
304 samples: vec![vec![1]],
305 };
306 assert!(
307 encode_voxj_object(
308 &wrong_row_arity,
309 &[4, 4],
310 PositionEncoding::RawJson,
311 SampleEncoding::RawJson,
312 )
313 .is_err()
314 );
315
316 let wrong_sample_count = VoxjCodecObject {
317 name: "o".to_owned(),
318 palette_refs: vec![0],
319 bounds: [2, 1, 1],
320 positions: vec![[0, 0, 0], [1, 0, 0]],
321 samples: vec![vec![1]],
322 };
323 assert!(
324 encode_voxj_object(
325 &wrong_sample_count,
326 &[4],
327 PositionEncoding::RawJson,
328 SampleEncoding::RawJson,
329 )
330 .is_err()
331 );
332 }
333}