1use core::fmt;
34use std::collections::HashMap;
35
36use crate::bytes::{Cursor, OutOfBounds};
37use crate::Rgb6;
38
39const VIS_NEG_X: u8 = 0x01;
47const VIS_POS_X: u8 = 0x02;
48const VIS_NEG_Y: u8 = 0x04;
49const VIS_POS_Y: u8 = 0x08;
50const VIS_POS_Z: u8 = 0x20;
54const VIS_NEG_Z: u8 = 0x10;
55
56pub(crate) fn compute_vis_dir(
63 occ: &impl Fn(i64, i64, i64) -> bool,
64 x: i64,
65 y: i64,
66 z: i64,
67) -> (u8, u8) {
68 let mut vis = 0u8;
69 if !occ(x - 1, y, z) {
70 vis |= VIS_NEG_X;
71 }
72 if !occ(x + 1, y, z) {
73 vis |= VIS_POS_X;
74 }
75 if !occ(x, y - 1, z) {
76 vis |= VIS_NEG_Y;
77 }
78 if !occ(x, y + 1, z) {
79 vis |= VIS_POS_Y;
80 }
81 if !occ(x, y, z - 1) {
82 vis |= VIS_NEG_Z;
83 }
84 if !occ(x, y, z + 1) {
85 vis |= VIS_POS_Z;
86 }
87
88 let mut n = [0.0f32; 3];
89 for dz in -1..=1 {
90 for dy in -1..=1 {
91 for dx in -1..=1 {
92 if (dx | dy | dz) != 0 && !occ(x + dx, y + dy, z + dz) {
93 n[0] += dx as f32;
94 n[1] += dy as f32;
95 n[2] += dz as f32;
96 }
97 }
98 }
99 }
100 (vis, crate::equivec::nearest_dir(n))
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct Voxel {
106 pub col: u32,
109 pub z: u16,
111 pub vis: u8,
114 pub dir: u8,
116}
117
118#[derive(Debug, Clone)]
121pub struct Kv6 {
122 pub xsiz: u32,
123 pub ysiz: u32,
124 pub zsiz: u32,
125 pub xpiv: f32,
126 pub ypiv: f32,
127 pub zpiv: f32,
128 pub voxels: Vec<Voxel>,
130 pub xlen: Vec<u32>,
133 pub ylen: Vec<Vec<u16>>,
136 pub palette: Option<[Rgb6; 256]>,
138}
139
140impl Kv6 {
141 #[must_use]
163 pub fn from_fn<F: Fn(u32, u32, u32) -> Option<u32>>(
164 xsiz: u32,
165 ysiz: u32,
166 zsiz: u32,
167 fill: F,
168 ) -> Kv6 {
169 Self::build_inner(xsiz, ysiz, zsiz, fill, false, |_| false)
170 }
171
172 #[must_use]
188 pub fn from_fn_keep_interior<F, G>(
189 xsiz: u32,
190 ysiz: u32,
191 zsiz: u32,
192 fill: F,
193 keep_interior: G,
194 ) -> Kv6
195 where
196 F: Fn(u32, u32, u32) -> Option<u32>,
197 G: Fn(u32) -> bool,
198 {
199 Self::build_inner(xsiz, ysiz, zsiz, fill, false, keep_interior)
200 }
201
202 #[must_use]
216 pub fn from_fn_shaded<F: Fn(u32, u32, u32) -> Option<u32>>(
217 xsiz: u32,
218 ysiz: u32,
219 zsiz: u32,
220 fill: F,
221 ) -> Kv6 {
222 Self::build_inner(xsiz, ysiz, zsiz, fill, true, |_| false)
223 }
224
225 #[allow(
229 clippy::cast_possible_truncation,
230 clippy::cast_sign_loss,
231 clippy::cast_precision_loss
232 )]
233 fn build_inner<F, G>(
234 xsiz: u32,
235 ysiz: u32,
236 zsiz: u32,
237 fill: F,
238 shaded: bool,
239 keep_interior: G,
240 ) -> Kv6
241 where
242 F: Fn(u32, u32, u32) -> Option<u32>,
243 G: Fn(u32) -> bool,
244 {
245 let occupied = |x: i64, y: i64, z: i64| -> bool {
246 x >= 0
247 && y >= 0
248 && z >= 0
249 && (x as u32) < xsiz
250 && (y as u32) < ysiz
251 && (z as u32) < zsiz
252 && fill(x as u32, y as u32, z as u32).is_some()
253 };
254
255 let mut voxels: Vec<Voxel> = Vec::new();
256 let mut xlen: Vec<u32> = Vec::with_capacity(xsiz as usize);
257 let mut ylen: Vec<Vec<u16>> = Vec::with_capacity(xsiz as usize);
258
259 for x in 0..xsiz {
260 let mut col_counts: Vec<u16> = Vec::with_capacity(ysiz as usize);
261 for y in 0..ysiz {
262 let before = voxels.len();
263 for z in 0..zsiz {
264 let Some(col) = fill(x, y, z) else { continue };
265 let (xi, yi, zi) = (i64::from(x), i64::from(y), i64::from(z));
266 let exposed = !occupied(xi - 1, yi, zi)
267 || !occupied(xi + 1, yi, zi)
268 || !occupied(xi, yi - 1, zi)
269 || !occupied(xi, yi + 1, zi)
270 || !occupied(xi, yi, zi - 1)
271 || !occupied(xi, yi, zi + 1);
272 if exposed || keep_interior(col) {
273 let (vis, dir) = if shaded {
274 compute_vis_dir(&occupied, xi, yi, zi)
275 } else {
276 (63, 0)
277 };
278 voxels.push(Voxel {
279 col,
280 z: z as u16,
281 vis,
282 dir,
283 });
284 }
285 }
286 col_counts.push((voxels.len() - before) as u16);
287 }
288 xlen.push(col_counts.iter().map(|&c| u32::from(c)).sum());
289 ylen.push(col_counts);
290 }
291
292 Kv6 {
293 xsiz,
294 ysiz,
295 zsiz,
296 xpiv: xsiz as f32 * 0.5,
297 ypiv: ysiz as f32 * 0.5,
298 zpiv: zsiz as f32 * 0.5,
299 voxels,
300 xlen,
301 ylen,
302 palette: None,
303 }
304 }
305
306 #[allow(clippy::cast_possible_wrap)]
314 pub fn recompute_surface(&mut self, occupied: impl Fn(i32, i32, i32) -> bool) {
315 let xsiz = self.xsiz;
316 let ysiz = self.ysiz;
317 let zsiz = self.zsiz;
318 let occ = |x: i64, y: i64, z: i64| -> bool {
319 x >= 0
320 && y >= 0
321 && z >= 0
322 && (x as u32) < xsiz
323 && (y as u32) < ysiz
324 && (z as u32) < zsiz
325 && occupied(x as i32, y as i32, z as i32)
326 };
327 let mut vi = 0usize;
328 for x in 0..xsiz as usize {
329 for y in 0..ysiz as usize {
330 let len = self.ylen[x][y] as usize;
331 for _ in 0..len {
332 let z = i64::from(self.voxels[vi].z);
333 let (vis, dir) = compute_vis_dir(&occ, x as i64, y as i64, z);
334 self.voxels[vi].vis = vis;
335 self.voxels[vi].dir = dir;
336 vi += 1;
337 }
338 }
339 }
340 }
341
342 fn surface_color_map(&self) -> HashMap<(u32, u32, u32), u32> {
348 let mut map = HashMap::with_capacity(self.voxels.len());
349 let mut vi = 0usize;
350 for x in 0..self.xsiz as usize {
351 for y in 0..self.ysiz as usize {
352 let len = self.ylen[x][y] as usize;
353 for _ in 0..len {
354 let v = self.voxels[vi];
355 #[allow(clippy::cast_lossless)]
356 map.insert((x as u32, y as u32, u32::from(v.z)), v.col);
357 vi += 1;
358 }
359 }
360 }
361 map
362 }
363
364 pub fn carve_sphere_with_colfunc<S, C>(
395 &mut self,
396 centre: [i32; 3],
397 radius: u32,
398 solid: S,
399 colfunc: C,
400 ) where
401 S: Fn(i32, i32, i32) -> bool,
402 C: Fn(i32, i32, i32) -> u32,
403 {
404 let orig = self.surface_color_map();
405 let (xpiv, ypiv, zpiv) = (self.xpiv, self.ypiv, self.zpiv);
407 let palette = self.palette;
408
409 #[allow(clippy::cast_possible_wrap)]
410 let r = radius as i32;
411 let r_sq = r * r;
412 let (cx, cy, cz) = (centre[0], centre[1], centre[2]);
413 let inside = |x: i32, y: i32, z: i32| {
414 let (dx, dy, dz) = (x - cx, y - cy, z - cz);
415 dx * dx + dy * dy + dz * dz <= r_sq
416 };
417
418 let rebuilt = Kv6::from_fn_shaded(self.xsiz, self.ysiz, self.zsiz, |x, y, z| {
419 #[allow(clippy::cast_possible_wrap)]
420 let (xi, yi, zi) = (x as i32, y as i32, z as i32);
421 if inside(xi, yi, zi) || !solid(xi, yi, zi) {
422 return None;
423 }
424 Some(
427 orig.get(&(x, y, z))
428 .copied()
429 .unwrap_or_else(|| colfunc(xi, yi, zi)),
430 )
431 });
432
433 self.voxels = rebuilt.voxels;
434 self.xlen = rebuilt.xlen;
435 self.ylen = rebuilt.ylen;
436 self.xpiv = xpiv;
437 self.ypiv = ypiv;
438 self.zpiv = zpiv;
439 self.palette = palette;
440 }
441
442 #[must_use]
445 pub fn solid_box(xsiz: u32, ysiz: u32, zsiz: u32, col: u32) -> Kv6 {
446 Kv6::from_fn(xsiz, ysiz, zsiz, |_, _, _| Some(col))
447 }
448
449 #[must_use]
451 pub fn solid_cube(n: u32, col: u32) -> Kv6 {
452 Kv6::solid_box(n, n, n, col)
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
458pub enum ParseError {
459 TooSmall { got: usize },
461 BadMagic { got: [u8; 4] },
463 Truncated { at: usize, need: usize },
466}
467
468impl fmt::Display for ParseError {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 match *self {
471 Self::TooSmall { got } => write!(
472 f,
473 "kv6 file too small ({got} bytes; need at least 32 byte header)"
474 ),
475 Self::BadMagic { got } => write!(
476 f,
477 "kv6 bad magic: got [{:#04x},{:#04x},{:#04x},{:#04x}], expected b\"Kvxl\"",
478 got[0], got[1], got[2], got[3]
479 ),
480 Self::Truncated { at, need } => {
481 write!(f, "kv6 truncated: need {need} bytes at offset {at}")
482 }
483 }
484 }
485}
486
487impl std::error::Error for ParseError {}
488
489impl From<OutOfBounds> for ParseError {
490 fn from(e: OutOfBounds) -> Self {
491 Self::Truncated {
492 at: e.at,
493 need: e.need,
494 }
495 }
496}
497
498const HEADER_LEN: usize = 32;
499const MAGIC: &[u8; 4] = b"Kvxl";
500const PALETTE_MAGIC: &[u8; 4] = b"SPal";
501const PALETTE_LEN: usize = 768;
502
503pub fn parse(bytes: &[u8]) -> Result<Kv6, ParseError> {
533 if bytes.len() < HEADER_LEN {
534 return Err(ParseError::TooSmall { got: bytes.len() });
535 }
536
537 let mut cur = Cursor::new(bytes);
538 let magic = cur.read_bytes(4)?;
539 if magic != MAGIC {
540 return Err(ParseError::BadMagic {
541 got: [magic[0], magic[1], magic[2], magic[3]],
542 });
543 }
544 let xsiz = cur.read_u32()?;
545 let ysiz = cur.read_u32()?;
546 let zsiz = cur.read_u32()?;
547 let xpiv = cur.read_f32()?;
548 let ypiv = cur.read_f32()?;
549 let zpiv = cur.read_f32()?;
550 let numvoxs = cur.read_u32()?;
551
552 let mut voxels = Vec::with_capacity(numvoxs as usize);
553 for _ in 0..numvoxs {
554 let col = cur.read_u32()?;
555 let z = cur.read_u16()?;
556 let vis = cur.read_u8()?;
557 let dir = cur.read_u8()?;
558 voxels.push(Voxel { col, z, vis, dir });
559 }
560
561 let mut xlen = Vec::with_capacity(xsiz as usize);
562 for _ in 0..xsiz {
563 xlen.push(cur.read_u32()?);
564 }
565
566 let mut ylen = Vec::with_capacity(xsiz as usize);
567 for _ in 0..xsiz {
568 let mut row = Vec::with_capacity(ysiz as usize);
569 for _ in 0..ysiz {
570 row.push(cur.read_u16()?);
571 }
572 ylen.push(row);
573 }
574
575 let palette =
577 if cur.remaining() >= 4 + PALETTE_LEN && cur.peek(4) == Some(PALETTE_MAGIC.as_slice()) {
578 cur.read_bytes(4)?;
579 let mut pal = [Rgb6::default(); 256];
580 for entry in &mut pal {
581 entry.r = cur.read_u8()?;
582 entry.g = cur.read_u8()?;
583 entry.b = cur.read_u8()?;
584 }
585 Some(pal)
586 } else {
587 None
588 };
589
590 Ok(Kv6 {
591 xsiz,
592 ysiz,
593 zsiz,
594 xpiv,
595 ypiv,
596 zpiv,
597 voxels,
598 xlen,
599 ylen,
600 palette,
601 })
602}
603
604#[must_use]
614pub fn serialize(kv6: &Kv6) -> Vec<u8> {
615 let pal_bytes = if kv6.palette.is_some() {
616 4 + PALETTE_LEN
617 } else {
618 0
619 };
620 let body_bytes = kv6.voxels.len() * 8
621 + kv6.xlen.len() * 4
622 + kv6.ylen.iter().map(|row| row.len() * 2).sum::<usize>();
623 let mut out = Vec::with_capacity(HEADER_LEN + body_bytes + pal_bytes);
624
625 out.extend_from_slice(MAGIC);
626 out.extend_from_slice(&kv6.xsiz.to_le_bytes());
627 out.extend_from_slice(&kv6.ysiz.to_le_bytes());
628 out.extend_from_slice(&kv6.zsiz.to_le_bytes());
629 out.extend_from_slice(&kv6.xpiv.to_le_bytes());
630 out.extend_from_slice(&kv6.ypiv.to_le_bytes());
631 out.extend_from_slice(&kv6.zpiv.to_le_bytes());
632 let numvoxs =
633 u32::try_from(kv6.voxels.len()).expect("kv6 numvoxs must fit in u32 (file format limit)");
634 out.extend_from_slice(&numvoxs.to_le_bytes());
635
636 for v in &kv6.voxels {
637 out.extend_from_slice(&v.col.to_le_bytes());
638 out.extend_from_slice(&v.z.to_le_bytes());
639 out.push(v.vis);
640 out.push(v.dir);
641 }
642 for v in &kv6.xlen {
643 out.extend_from_slice(&v.to_le_bytes());
644 }
645 for row in &kv6.ylen {
646 for v in row {
647 out.extend_from_slice(&v.to_le_bytes());
648 }
649 }
650 if let Some(pal) = &kv6.palette {
651 out.extend_from_slice(PALETTE_MAGIC);
652 for e in pal {
653 out.push(e.r);
654 out.push(e.g);
655 out.push(e.b);
656 }
657 }
658
659 out
660}
661
662#[cfg(test)]
665mod tests {
666 use super::*;
667
668 const COCO_KV6: &[u8] = include_bytes!("../../../assets/coco.kv6");
670
671 #[test]
672 fn solid_cube_builder_is_surface_only_and_consistent() {
673 let cube = Kv6::solid_cube(4, 0x8012_3456);
674 assert_eq!((cube.xsiz, cube.ysiz, cube.zsiz), (4, 4, 4));
675 assert!((cube.xpiv - 2.0).abs() < f32::EPSILON);
677
678 assert_eq!(cube.voxels.len(), 64 - 8);
681 assert!(cube
682 .voxels
683 .iter()
684 .all(|v| v.vis == 63 && v.col == 0x8012_3456));
685
686 assert_eq!(cube.xlen.len(), 4);
688 assert_eq!(cube.ylen.len(), 4);
689 assert!(cube.ylen.iter().all(|row| row.len() == 4));
690 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
691 let ylen_sum: usize = cube
692 .ylen
693 .iter()
694 .flat_map(|r| r.iter())
695 .map(|&n| n as usize)
696 .sum();
697 assert_eq!(xlen_sum, cube.voxels.len());
698 assert_eq!(ylen_sum, cube.voxels.len());
699 }
700
701 #[test]
705 fn from_fn_keep_interior_retains_matching_interiors() {
706 let col = 0x8012_3456;
707 let shell = Kv6::from_fn(4, 4, 4, |_, _, _| Some(col));
710 assert_eq!(shell.voxels.len(), 64 - 8, "from_fn is surface-only");
711
712 let filled = Kv6::from_fn_keep_interior(4, 4, 4, |_, _, _| Some(col), |c| c == col);
713 assert_eq!(filled.voxels.len(), 64, "keep_interior retains all 64");
714 assert_eq!(color_at(&shell, 1, 1, 1), None);
717 assert_eq!(color_at(&filled, 1, 1, 1), Some(col));
718
719 let culled = Kv6::from_fn_keep_interior(4, 4, 4, |_, _, _| Some(col), |_| false);
721 assert_eq!(
722 culled.voxels.len(),
723 64 - 8,
724 "predicate=false ⇒ surface-only"
725 );
726 }
727
728 fn color_at(kv6: &Kv6, tx: u32, ty: u32, tz: u32) -> Option<u32> {
731 let mut vi = 0usize;
732 for x in 0..kv6.xsiz {
733 for y in 0..kv6.ysiz {
734 let len = kv6.ylen[x as usize][y as usize] as usize;
735 for _ in 0..len {
736 let v = kv6.voxels[vi];
737 if x == tx && y == ty && u32::from(v.z) == tz {
738 return Some(v.col);
739 }
740 vi += 1;
741 }
742 }
743 }
744 None
745 }
746
747 #[test]
748 fn carve_sphere_exposes_interior_with_colfunc() {
749 const BASE: u32 = 0x8011_2233;
750 let mut cube = Kv6::from_fn_shaded(16, 16, 16, |_, _, _| Some(BASE));
752 cube.xpiv = 1.0;
754 cube.ypiv = 2.0;
755 cube.zpiv = 3.0;
756
757 let encode = |x: i32, y: i32, z: i32| ((x << 16) | (y << 8) | z) as u32;
760 cube.carve_sphere_with_colfunc([8, 8, 8], 4, |_, _, _| true, encode);
761
762 assert_eq!(color_at(&cube, 8, 8, 8), None);
764 assert_eq!(color_at(&cube, 8, 8, 3), Some(encode(8, 8, 3)));
768 assert_eq!(color_at(&cube, 0, 8, 8), Some(BASE));
770
771 assert!((cube.xpiv - 1.0).abs() < f32::EPSILON);
773 assert!((cube.ypiv - 2.0).abs() < f32::EPSILON);
774 assert!((cube.zpiv - 3.0).abs() < f32::EPSILON);
775
776 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
778 assert_eq!(xlen_sum, cube.voxels.len());
779 }
780
781 #[test]
782 fn carve_sphere_respects_caller_solid_predicate() {
783 const BASE: u32 = 0x80AA_BBCC;
784 let solid = |x: i32, _y: i32, _z: i32| (0..8).contains(&x);
788 #[allow(clippy::cast_sign_loss)]
789 let mut m =
790 Kv6::from_fn_shaded(16, 16, 16, |x, _, _| solid(x as i32, 0, 0).then_some(BASE));
791 m.carve_sphere_with_colfunc([4, 8, 8], 3, solid, |_, _, _| 0x8000_FF00);
792 assert_eq!(color_at(&m, 12, 8, 8), None);
794 assert_eq!(color_at(&m, 4, 8, 8), None);
796 }
797
798 #[test]
799 fn built_cube_round_trips_through_serialize_parse() {
800 let cube = Kv6::solid_cube(5, 0x80AB_CDEF);
801 let bytes = serialize(&cube);
802 let back = parse(&bytes).expect("parse built cube");
803 assert_eq!(back.xsiz, cube.xsiz);
804 assert_eq!(back.voxels.len(), cube.voxels.len());
805 assert_eq!(
806 serialize(&back),
807 bytes,
808 "serialize is stable across round-trip"
809 );
810 }
811
812 #[test]
813 fn from_fn_skips_air_and_keeps_z_order() {
814 let kv6 = Kv6::from_fn(1, 1, 2, |_, _, _| Some(0x8000_FF00));
817 assert_eq!(kv6.voxels.len(), 2);
818 assert_eq!(kv6.voxels[0].z, 0);
819 assert_eq!(kv6.voxels[1].z, 1);
820 assert_eq!(kv6.xlen, vec![2]);
821 assert_eq!(kv6.ylen, vec![vec![2]]);
822 }
823
824 #[test]
825 fn parse_coco_header() {
826 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
827 assert_eq!(kv6.xsiz, 9);
828 assert_eq!(kv6.ysiz, 11);
829 assert_eq!(kv6.zsiz, 9);
830 assert!((kv6.xpiv - 2.0).abs() < f32::EPSILON);
832 assert!((kv6.ypiv - 3.0).abs() < f32::EPSILON);
833 assert!((kv6.zpiv - 9.0).abs() < f32::EPSILON);
834 assert_eq!(kv6.voxels.len(), 148);
835 }
836
837 #[test]
838 fn coco_voxel_counts_consistent() {
839 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
840 assert_eq!(kv6.xlen.len(), kv6.xsiz as usize);
841 assert_eq!(kv6.ylen.len(), kv6.xsiz as usize);
842 for row in &kv6.ylen {
843 assert_eq!(row.len(), kv6.ysiz as usize);
844 }
845 let xlen_sum: u64 = kv6.xlen.iter().map(|&n| u64::from(n)).sum();
846 let ylen_sum: u64 = kv6
847 .ylen
848 .iter()
849 .flat_map(|row| row.iter().map(|&n| u64::from(n)))
850 .sum();
851 let nv = kv6.voxels.len() as u64;
852 assert_eq!(xlen_sum, nv);
853 assert_eq!(ylen_sum, nv);
854 }
855
856 #[test]
857 fn from_fn_shaded_keeps_from_fn_geometry() {
858 let fill = |x: u32, y: u32, z: u32| {
861 let on_face = x == 0 || x == 4 || y == 0 || y == 4 || z == 0 || z == 4;
862 on_face.then_some(0x80_44_55_66u32)
863 };
864 let flat = Kv6::from_fn(5, 5, 5, fill);
865 let shaded = Kv6::from_fn_shaded(5, 5, 5, fill);
866 assert_eq!(flat.voxels.len(), shaded.voxels.len());
867 assert_eq!(flat.xlen, shaded.xlen);
868 assert_eq!(flat.ylen, shaded.ylen);
869 for (f, s) in flat.voxels.iter().zip(&shaded.voxels) {
870 assert_eq!((f.col, f.z), (s.col, s.z));
871 }
872 assert!(
874 shaded.voxels.iter().any(|v| v.dir != 0),
875 "from_fn_shaded left every dir flat"
876 );
877 assert!(flat.voxels.iter().all(|v| v.dir == 0 && v.vis == 63));
878 }
879
880 #[test]
881 fn from_fn_shaded_column_z_faces() {
882 let kv = Kv6::from_fn_shaded(1, 1, 2, |_, _, _| Some(0x80_80_80_80));
887 assert_eq!(kv.voxels.len(), 2);
888 let (lower, upper) = (&kv.voxels[0], &kv.voxels[1]); assert_eq!(lower.z, 0);
890 assert_eq!(upper.z, 1);
891 assert_eq!(lower.vis & VIS_POS_Z, 0, "lower +z should be internal");
892 assert_eq!(lower.vis & VIS_NEG_Z, VIS_NEG_Z, "lower -z exposed");
893 assert_eq!(upper.vis & VIS_NEG_Z, 0, "upper -z should be internal");
894 assert_eq!(upper.vis & VIS_POS_Z, VIS_POS_Z, "upper +z exposed");
895 let sides = VIS_NEG_X | VIS_POS_X | VIS_NEG_Y | VIS_POS_Y;
897 assert_eq!(lower.vis & sides, sides);
898 assert_eq!(upper.vis & sides, sides);
899 }
900
901 #[test]
911 fn coco_vis_matches_authored_all_faces() {
912 use std::collections::HashMap;
913 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
914 let mut pos: HashMap<(u32, u32, u32), u8> = HashMap::new();
915 let mut vi = 0usize;
916 for x in 0..kv6.xsiz {
917 for y in 0..kv6.ysiz {
918 let len = kv6.ylen[x as usize][y as usize] as usize;
919 for _ in 0..len {
920 pos.insert((x, y, u32::from(kv6.voxels[vi].z)), kv6.voxels[vi].vis);
921 vi += 1;
922 }
923 }
924 }
925 let mut checked = 0u32;
926 for (&(x, y, z), &vis) in &pos {
927 let mut chk = |present: bool, bit: u8, face: &str| {
928 if present {
929 assert_eq!(
930 vis & bit,
931 0,
932 "coco ({x},{y},{z}): {face} internal but bit set"
933 );
934 checked += 1;
935 }
936 };
937 chk(pos.contains_key(&(x + 1, y, z)), VIS_POS_X, "+x");
938 chk(x > 0 && pos.contains_key(&(x - 1, y, z)), VIS_NEG_X, "-x");
939 chk(pos.contains_key(&(x, y + 1, z)), VIS_POS_Y, "+y");
940 chk(y > 0 && pos.contains_key(&(x, y - 1, z)), VIS_NEG_Y, "-y");
941 chk(pos.contains_key(&(x, y, z + 1)), VIS_POS_Z, "+z");
942 chk(z > 0 && pos.contains_key(&(x, y, z - 1)), VIS_NEG_Z, "-z");
943 }
944 assert!(
945 checked > 100,
946 "expected many adjacent faces in coco, got {checked}"
947 );
948 }
949
950 #[test]
951 fn recompute_surface_matches_from_fn_shaded() {
952 let fill = |x: u32, y: u32, z: u32| {
955 let cx = x as f32 - 4.0;
956 let cy = y as f32 - 4.0;
957 let cz = z as f32 - 4.0;
958 (cx * cx + cy * cy + cz * cz <= 16.0).then_some(0x80_30_60_90u32)
959 };
960 let shaded = Kv6::from_fn_shaded(9, 9, 9, fill);
961 let mut edited = Kv6::from_fn(9, 9, 9, fill); edited.recompute_surface(|x, y, z| {
963 x >= 0 && y >= 0 && z >= 0 && fill(x as u32, y as u32, z as u32).is_some()
964 });
965 assert_eq!(edited.voxels.len(), shaded.voxels.len());
966 for (e, s) in edited.voxels.iter().zip(&shaded.voxels) {
967 assert_eq!((e.vis, e.dir), (s.vis, s.dir), "voxel z={}", e.z);
968 }
969 }
970
971 #[test]
972 fn from_fn_shaded_slab_top_normal_points_up() {
973 use crate::equivec::univec;
974 let kv = Kv6::from_fn_shaded(8, 8, 12, |_, _, z| {
978 (2..=9).contains(&z).then_some(0x80_aa_aa_aa)
979 });
980 let v = kv
981 .voxels
982 .iter()
983 .enumerate()
984 .find_map(|(i, v)| {
985 let mut acc = 0usize;
987 for x in 0..kv.xsiz as usize {
988 for y in 0..kv.ysiz as usize {
989 let len = kv.ylen[x][y] as usize;
990 if i < acc + len {
991 return (x == 4 && y == 4 && v.z == 2).then_some(*v);
992 }
993 acc += len;
994 }
995 }
996 None
997 })
998 .expect("centre top-face voxel present");
999 let n = univec()[v.dir as usize];
1000 assert!(
1001 n[2] < -0.5,
1002 "top-face normal should point -z (up), got {n:?}"
1003 );
1004 }
1005
1006 #[test]
1007 fn coco_palette_present_and_matches_kvx() {
1008 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
1009 let pal = kv6.palette.as_ref().expect("SPal trailer present");
1010 assert_eq!((pal[0].r, pal[0].g, pal[0].b), (0x3f, 0x19, 0x19));
1012 }
1013
1014 #[test]
1015 fn coco_first_voxel_packed_colour() {
1016 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
1017 let v0 = kv6.voxels[0];
1021 assert_eq!(v0.col, 0x80fc_a460);
1022 assert_eq!(v0.col & 0x8000_0000, 0x8000_0000);
1023 }
1024
1025 #[test]
1026 fn coco_roundtrips_byte_equal() {
1027 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
1028 let out = serialize(&kv6);
1029 assert_eq!(out.len(), COCO_KV6.len(), "length differs");
1030 assert_eq!(out.as_slice(), COCO_KV6, "byte content differs");
1031 }
1032
1033 #[test]
1034 fn parse_truncated_header_fails() {
1035 let r = parse(&[0u8; 16]);
1036 assert!(matches!(r, Err(ParseError::TooSmall { .. })));
1037 }
1038
1039 #[test]
1040 fn parse_bad_magic_fails() {
1041 let mut bad = COCO_KV6.to_vec();
1042 bad[0] = b'X';
1043 let r = parse(&bad);
1044 assert!(matches!(r, Err(ParseError::BadMagic { .. })));
1045 }
1046}