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)
170 }
171
172 #[must_use]
186 pub fn from_fn_shaded<F: Fn(u32, u32, u32) -> Option<u32>>(
187 xsiz: u32,
188 ysiz: u32,
189 zsiz: u32,
190 fill: F,
191 ) -> Kv6 {
192 Self::build_inner(xsiz, ysiz, zsiz, fill, true)
193 }
194
195 #[allow(
199 clippy::cast_possible_truncation,
200 clippy::cast_sign_loss,
201 clippy::cast_precision_loss
202 )]
203 fn build_inner<F: Fn(u32, u32, u32) -> Option<u32>>(
204 xsiz: u32,
205 ysiz: u32,
206 zsiz: u32,
207 fill: F,
208 shaded: bool,
209 ) -> Kv6 {
210 let occupied = |x: i64, y: i64, z: i64| -> bool {
211 x >= 0
212 && y >= 0
213 && z >= 0
214 && (x as u32) < xsiz
215 && (y as u32) < ysiz
216 && (z as u32) < zsiz
217 && fill(x as u32, y as u32, z as u32).is_some()
218 };
219
220 let mut voxels: Vec<Voxel> = Vec::new();
221 let mut xlen: Vec<u32> = Vec::with_capacity(xsiz as usize);
222 let mut ylen: Vec<Vec<u16>> = Vec::with_capacity(xsiz as usize);
223
224 for x in 0..xsiz {
225 let mut col_counts: Vec<u16> = Vec::with_capacity(ysiz as usize);
226 for y in 0..ysiz {
227 let before = voxels.len();
228 for z in 0..zsiz {
229 let Some(col) = fill(x, y, z) else { continue };
230 let (xi, yi, zi) = (i64::from(x), i64::from(y), i64::from(z));
231 let exposed = !occupied(xi - 1, yi, zi)
232 || !occupied(xi + 1, yi, zi)
233 || !occupied(xi, yi - 1, zi)
234 || !occupied(xi, yi + 1, zi)
235 || !occupied(xi, yi, zi - 1)
236 || !occupied(xi, yi, zi + 1);
237 if exposed {
238 let (vis, dir) = if shaded {
239 compute_vis_dir(&occupied, xi, yi, zi)
240 } else {
241 (63, 0)
242 };
243 voxels.push(Voxel {
244 col,
245 z: z as u16,
246 vis,
247 dir,
248 });
249 }
250 }
251 col_counts.push((voxels.len() - before) as u16);
252 }
253 xlen.push(col_counts.iter().map(|&c| u32::from(c)).sum());
254 ylen.push(col_counts);
255 }
256
257 Kv6 {
258 xsiz,
259 ysiz,
260 zsiz,
261 xpiv: xsiz as f32 * 0.5,
262 ypiv: ysiz as f32 * 0.5,
263 zpiv: zsiz as f32 * 0.5,
264 voxels,
265 xlen,
266 ylen,
267 palette: None,
268 }
269 }
270
271 #[allow(clippy::cast_possible_wrap)]
279 pub fn recompute_surface(&mut self, occupied: impl Fn(i32, i32, i32) -> bool) {
280 let xsiz = self.xsiz;
281 let ysiz = self.ysiz;
282 let zsiz = self.zsiz;
283 let occ = |x: i64, y: i64, z: i64| -> bool {
284 x >= 0
285 && y >= 0
286 && z >= 0
287 && (x as u32) < xsiz
288 && (y as u32) < ysiz
289 && (z as u32) < zsiz
290 && occupied(x as i32, y as i32, z as i32)
291 };
292 let mut vi = 0usize;
293 for x in 0..xsiz as usize {
294 for y in 0..ysiz as usize {
295 let len = self.ylen[x][y] as usize;
296 for _ in 0..len {
297 let z = i64::from(self.voxels[vi].z);
298 let (vis, dir) = compute_vis_dir(&occ, x as i64, y as i64, z);
299 self.voxels[vi].vis = vis;
300 self.voxels[vi].dir = dir;
301 vi += 1;
302 }
303 }
304 }
305 }
306
307 fn surface_color_map(&self) -> HashMap<(u32, u32, u32), u32> {
313 let mut map = HashMap::with_capacity(self.voxels.len());
314 let mut vi = 0usize;
315 for x in 0..self.xsiz as usize {
316 for y in 0..self.ysiz as usize {
317 let len = self.ylen[x][y] as usize;
318 for _ in 0..len {
319 let v = self.voxels[vi];
320 #[allow(clippy::cast_lossless)]
321 map.insert((x as u32, y as u32, u32::from(v.z)), v.col);
322 vi += 1;
323 }
324 }
325 }
326 map
327 }
328
329 pub fn carve_sphere_with_colfunc<S, C>(
360 &mut self,
361 centre: [i32; 3],
362 radius: u32,
363 solid: S,
364 colfunc: C,
365 ) where
366 S: Fn(i32, i32, i32) -> bool,
367 C: Fn(i32, i32, i32) -> u32,
368 {
369 let orig = self.surface_color_map();
370 let (xpiv, ypiv, zpiv) = (self.xpiv, self.ypiv, self.zpiv);
372 let palette = self.palette;
373
374 #[allow(clippy::cast_possible_wrap)]
375 let r = radius as i32;
376 let r_sq = r * r;
377 let (cx, cy, cz) = (centre[0], centre[1], centre[2]);
378 let inside = |x: i32, y: i32, z: i32| {
379 let (dx, dy, dz) = (x - cx, y - cy, z - cz);
380 dx * dx + dy * dy + dz * dz <= r_sq
381 };
382
383 let rebuilt = Kv6::from_fn_shaded(self.xsiz, self.ysiz, self.zsiz, |x, y, z| {
384 #[allow(clippy::cast_possible_wrap)]
385 let (xi, yi, zi) = (x as i32, y as i32, z as i32);
386 if inside(xi, yi, zi) || !solid(xi, yi, zi) {
387 return None;
388 }
389 Some(
392 orig.get(&(x, y, z))
393 .copied()
394 .unwrap_or_else(|| colfunc(xi, yi, zi)),
395 )
396 });
397
398 self.voxels = rebuilt.voxels;
399 self.xlen = rebuilt.xlen;
400 self.ylen = rebuilt.ylen;
401 self.xpiv = xpiv;
402 self.ypiv = ypiv;
403 self.zpiv = zpiv;
404 self.palette = palette;
405 }
406
407 #[must_use]
410 pub fn solid_box(xsiz: u32, ysiz: u32, zsiz: u32, col: u32) -> Kv6 {
411 Kv6::from_fn(xsiz, ysiz, zsiz, |_, _, _| Some(col))
412 }
413
414 #[must_use]
416 pub fn solid_cube(n: u32, col: u32) -> Kv6 {
417 Kv6::solid_box(n, n, n, col)
418 }
419}
420
421#[derive(Debug, Clone, PartialEq, Eq)]
423pub enum ParseError {
424 TooSmall { got: usize },
426 BadMagic { got: [u8; 4] },
428 Truncated { at: usize, need: usize },
431}
432
433impl fmt::Display for ParseError {
434 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435 match *self {
436 Self::TooSmall { got } => write!(
437 f,
438 "kv6 file too small ({got} bytes; need at least 32 byte header)"
439 ),
440 Self::BadMagic { got } => write!(
441 f,
442 "kv6 bad magic: got [{:#04x},{:#04x},{:#04x},{:#04x}], expected b\"Kvxl\"",
443 got[0], got[1], got[2], got[3]
444 ),
445 Self::Truncated { at, need } => {
446 write!(f, "kv6 truncated: need {need} bytes at offset {at}")
447 }
448 }
449 }
450}
451
452impl std::error::Error for ParseError {}
453
454impl From<OutOfBounds> for ParseError {
455 fn from(e: OutOfBounds) -> Self {
456 Self::Truncated {
457 at: e.at,
458 need: e.need,
459 }
460 }
461}
462
463const HEADER_LEN: usize = 32;
464const MAGIC: &[u8; 4] = b"Kvxl";
465const PALETTE_MAGIC: &[u8; 4] = b"SPal";
466const PALETTE_LEN: usize = 768;
467
468pub fn parse(bytes: &[u8]) -> Result<Kv6, ParseError> {
498 if bytes.len() < HEADER_LEN {
499 return Err(ParseError::TooSmall { got: bytes.len() });
500 }
501
502 let mut cur = Cursor::new(bytes);
503 let magic = cur.read_bytes(4)?;
504 if magic != MAGIC {
505 return Err(ParseError::BadMagic {
506 got: [magic[0], magic[1], magic[2], magic[3]],
507 });
508 }
509 let xsiz = cur.read_u32()?;
510 let ysiz = cur.read_u32()?;
511 let zsiz = cur.read_u32()?;
512 let xpiv = cur.read_f32()?;
513 let ypiv = cur.read_f32()?;
514 let zpiv = cur.read_f32()?;
515 let numvoxs = cur.read_u32()?;
516
517 let mut voxels = Vec::with_capacity(numvoxs as usize);
518 for _ in 0..numvoxs {
519 let col = cur.read_u32()?;
520 let z = cur.read_u16()?;
521 let vis = cur.read_u8()?;
522 let dir = cur.read_u8()?;
523 voxels.push(Voxel { col, z, vis, dir });
524 }
525
526 let mut xlen = Vec::with_capacity(xsiz as usize);
527 for _ in 0..xsiz {
528 xlen.push(cur.read_u32()?);
529 }
530
531 let mut ylen = Vec::with_capacity(xsiz as usize);
532 for _ in 0..xsiz {
533 let mut row = Vec::with_capacity(ysiz as usize);
534 for _ in 0..ysiz {
535 row.push(cur.read_u16()?);
536 }
537 ylen.push(row);
538 }
539
540 let palette =
542 if cur.remaining() >= 4 + PALETTE_LEN && cur.peek(4) == Some(PALETTE_MAGIC.as_slice()) {
543 cur.read_bytes(4)?;
544 let mut pal = [Rgb6::default(); 256];
545 for entry in &mut pal {
546 entry.r = cur.read_u8()?;
547 entry.g = cur.read_u8()?;
548 entry.b = cur.read_u8()?;
549 }
550 Some(pal)
551 } else {
552 None
553 };
554
555 Ok(Kv6 {
556 xsiz,
557 ysiz,
558 zsiz,
559 xpiv,
560 ypiv,
561 zpiv,
562 voxels,
563 xlen,
564 ylen,
565 palette,
566 })
567}
568
569#[must_use]
579pub fn serialize(kv6: &Kv6) -> Vec<u8> {
580 let pal_bytes = if kv6.palette.is_some() {
581 4 + PALETTE_LEN
582 } else {
583 0
584 };
585 let body_bytes = kv6.voxels.len() * 8
586 + kv6.xlen.len() * 4
587 + kv6.ylen.iter().map(|row| row.len() * 2).sum::<usize>();
588 let mut out = Vec::with_capacity(HEADER_LEN + body_bytes + pal_bytes);
589
590 out.extend_from_slice(MAGIC);
591 out.extend_from_slice(&kv6.xsiz.to_le_bytes());
592 out.extend_from_slice(&kv6.ysiz.to_le_bytes());
593 out.extend_from_slice(&kv6.zsiz.to_le_bytes());
594 out.extend_from_slice(&kv6.xpiv.to_le_bytes());
595 out.extend_from_slice(&kv6.ypiv.to_le_bytes());
596 out.extend_from_slice(&kv6.zpiv.to_le_bytes());
597 let numvoxs =
598 u32::try_from(kv6.voxels.len()).expect("kv6 numvoxs must fit in u32 (file format limit)");
599 out.extend_from_slice(&numvoxs.to_le_bytes());
600
601 for v in &kv6.voxels {
602 out.extend_from_slice(&v.col.to_le_bytes());
603 out.extend_from_slice(&v.z.to_le_bytes());
604 out.push(v.vis);
605 out.push(v.dir);
606 }
607 for v in &kv6.xlen {
608 out.extend_from_slice(&v.to_le_bytes());
609 }
610 for row in &kv6.ylen {
611 for v in row {
612 out.extend_from_slice(&v.to_le_bytes());
613 }
614 }
615 if let Some(pal) = &kv6.palette {
616 out.extend_from_slice(PALETTE_MAGIC);
617 for e in pal {
618 out.push(e.r);
619 out.push(e.g);
620 out.push(e.b);
621 }
622 }
623
624 out
625}
626
627#[cfg(test)]
630mod tests {
631 use super::*;
632
633 const COCO_KV6: &[u8] = include_bytes!("../../../assets/coco.kv6");
635
636 #[test]
637 fn solid_cube_builder_is_surface_only_and_consistent() {
638 let cube = Kv6::solid_cube(4, 0x8012_3456);
639 assert_eq!((cube.xsiz, cube.ysiz, cube.zsiz), (4, 4, 4));
640 assert!((cube.xpiv - 2.0).abs() < f32::EPSILON);
642
643 assert_eq!(cube.voxels.len(), 64 - 8);
646 assert!(cube
647 .voxels
648 .iter()
649 .all(|v| v.vis == 63 && v.col == 0x8012_3456));
650
651 assert_eq!(cube.xlen.len(), 4);
653 assert_eq!(cube.ylen.len(), 4);
654 assert!(cube.ylen.iter().all(|row| row.len() == 4));
655 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
656 let ylen_sum: usize = cube
657 .ylen
658 .iter()
659 .flat_map(|r| r.iter())
660 .map(|&n| n as usize)
661 .sum();
662 assert_eq!(xlen_sum, cube.voxels.len());
663 assert_eq!(ylen_sum, cube.voxels.len());
664 }
665
666 fn color_at(kv6: &Kv6, tx: u32, ty: u32, tz: u32) -> Option<u32> {
669 let mut vi = 0usize;
670 for x in 0..kv6.xsiz {
671 for y in 0..kv6.ysiz {
672 let len = kv6.ylen[x as usize][y as usize] as usize;
673 for _ in 0..len {
674 let v = kv6.voxels[vi];
675 if x == tx && y == ty && u32::from(v.z) == tz {
676 return Some(v.col);
677 }
678 vi += 1;
679 }
680 }
681 }
682 None
683 }
684
685 #[test]
686 fn carve_sphere_exposes_interior_with_colfunc() {
687 const BASE: u32 = 0x8011_2233;
688 let mut cube = Kv6::from_fn_shaded(16, 16, 16, |_, _, _| Some(BASE));
690 cube.xpiv = 1.0;
692 cube.ypiv = 2.0;
693 cube.zpiv = 3.0;
694
695 let encode = |x: i32, y: i32, z: i32| ((x << 16) | (y << 8) | z) as u32;
698 cube.carve_sphere_with_colfunc([8, 8, 8], 4, |_, _, _| true, encode);
699
700 assert_eq!(color_at(&cube, 8, 8, 8), None);
702 assert_eq!(color_at(&cube, 8, 8, 3), Some(encode(8, 8, 3)));
706 assert_eq!(color_at(&cube, 0, 8, 8), Some(BASE));
708
709 assert!((cube.xpiv - 1.0).abs() < f32::EPSILON);
711 assert!((cube.ypiv - 2.0).abs() < f32::EPSILON);
712 assert!((cube.zpiv - 3.0).abs() < f32::EPSILON);
713
714 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
716 assert_eq!(xlen_sum, cube.voxels.len());
717 }
718
719 #[test]
720 fn carve_sphere_respects_caller_solid_predicate() {
721 const BASE: u32 = 0x80AA_BBCC;
722 let solid = |x: i32, _y: i32, _z: i32| (0..8).contains(&x);
726 #[allow(clippy::cast_sign_loss)]
727 let mut m =
728 Kv6::from_fn_shaded(16, 16, 16, |x, _, _| solid(x as i32, 0, 0).then_some(BASE));
729 m.carve_sphere_with_colfunc([4, 8, 8], 3, solid, |_, _, _| 0x8000_FF00);
730 assert_eq!(color_at(&m, 12, 8, 8), None);
732 assert_eq!(color_at(&m, 4, 8, 8), None);
734 }
735
736 #[test]
737 fn built_cube_round_trips_through_serialize_parse() {
738 let cube = Kv6::solid_cube(5, 0x80AB_CDEF);
739 let bytes = serialize(&cube);
740 let back = parse(&bytes).expect("parse built cube");
741 assert_eq!(back.xsiz, cube.xsiz);
742 assert_eq!(back.voxels.len(), cube.voxels.len());
743 assert_eq!(
744 serialize(&back),
745 bytes,
746 "serialize is stable across round-trip"
747 );
748 }
749
750 #[test]
751 fn from_fn_skips_air_and_keeps_z_order() {
752 let kv6 = Kv6::from_fn(1, 1, 2, |_, _, _| Some(0x8000_FF00));
755 assert_eq!(kv6.voxels.len(), 2);
756 assert_eq!(kv6.voxels[0].z, 0);
757 assert_eq!(kv6.voxels[1].z, 1);
758 assert_eq!(kv6.xlen, vec![2]);
759 assert_eq!(kv6.ylen, vec![vec![2]]);
760 }
761
762 #[test]
763 fn parse_coco_header() {
764 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
765 assert_eq!(kv6.xsiz, 9);
766 assert_eq!(kv6.ysiz, 11);
767 assert_eq!(kv6.zsiz, 9);
768 assert!((kv6.xpiv - 2.0).abs() < f32::EPSILON);
770 assert!((kv6.ypiv - 3.0).abs() < f32::EPSILON);
771 assert!((kv6.zpiv - 9.0).abs() < f32::EPSILON);
772 assert_eq!(kv6.voxels.len(), 148);
773 }
774
775 #[test]
776 fn coco_voxel_counts_consistent() {
777 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
778 assert_eq!(kv6.xlen.len(), kv6.xsiz as usize);
779 assert_eq!(kv6.ylen.len(), kv6.xsiz as usize);
780 for row in &kv6.ylen {
781 assert_eq!(row.len(), kv6.ysiz as usize);
782 }
783 let xlen_sum: u64 = kv6.xlen.iter().map(|&n| u64::from(n)).sum();
784 let ylen_sum: u64 = kv6
785 .ylen
786 .iter()
787 .flat_map(|row| row.iter().map(|&n| u64::from(n)))
788 .sum();
789 let nv = kv6.voxels.len() as u64;
790 assert_eq!(xlen_sum, nv);
791 assert_eq!(ylen_sum, nv);
792 }
793
794 #[test]
795 fn from_fn_shaded_keeps_from_fn_geometry() {
796 let fill = |x: u32, y: u32, z: u32| {
799 let on_face = x == 0 || x == 4 || y == 0 || y == 4 || z == 0 || z == 4;
800 on_face.then_some(0x80_44_55_66u32)
801 };
802 let flat = Kv6::from_fn(5, 5, 5, fill);
803 let shaded = Kv6::from_fn_shaded(5, 5, 5, fill);
804 assert_eq!(flat.voxels.len(), shaded.voxels.len());
805 assert_eq!(flat.xlen, shaded.xlen);
806 assert_eq!(flat.ylen, shaded.ylen);
807 for (f, s) in flat.voxels.iter().zip(&shaded.voxels) {
808 assert_eq!((f.col, f.z), (s.col, s.z));
809 }
810 assert!(
812 shaded.voxels.iter().any(|v| v.dir != 0),
813 "from_fn_shaded left every dir flat"
814 );
815 assert!(flat.voxels.iter().all(|v| v.dir == 0 && v.vis == 63));
816 }
817
818 #[test]
819 fn from_fn_shaded_column_z_faces() {
820 let kv = Kv6::from_fn_shaded(1, 1, 2, |_, _, _| Some(0x80_80_80_80));
825 assert_eq!(kv.voxels.len(), 2);
826 let (lower, upper) = (&kv.voxels[0], &kv.voxels[1]); assert_eq!(lower.z, 0);
828 assert_eq!(upper.z, 1);
829 assert_eq!(lower.vis & VIS_POS_Z, 0, "lower +z should be internal");
830 assert_eq!(lower.vis & VIS_NEG_Z, VIS_NEG_Z, "lower -z exposed");
831 assert_eq!(upper.vis & VIS_NEG_Z, 0, "upper -z should be internal");
832 assert_eq!(upper.vis & VIS_POS_Z, VIS_POS_Z, "upper +z exposed");
833 let sides = VIS_NEG_X | VIS_POS_X | VIS_NEG_Y | VIS_POS_Y;
835 assert_eq!(lower.vis & sides, sides);
836 assert_eq!(upper.vis & sides, sides);
837 }
838
839 #[test]
849 fn coco_vis_matches_authored_all_faces() {
850 use std::collections::HashMap;
851 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
852 let mut pos: HashMap<(u32, u32, u32), u8> = HashMap::new();
853 let mut vi = 0usize;
854 for x in 0..kv6.xsiz {
855 for y in 0..kv6.ysiz {
856 let len = kv6.ylen[x as usize][y as usize] as usize;
857 for _ in 0..len {
858 pos.insert((x, y, u32::from(kv6.voxels[vi].z)), kv6.voxels[vi].vis);
859 vi += 1;
860 }
861 }
862 }
863 let mut checked = 0u32;
864 for (&(x, y, z), &vis) in &pos {
865 let mut chk = |present: bool, bit: u8, face: &str| {
866 if present {
867 assert_eq!(
868 vis & bit,
869 0,
870 "coco ({x},{y},{z}): {face} internal but bit set"
871 );
872 checked += 1;
873 }
874 };
875 chk(pos.contains_key(&(x + 1, y, z)), VIS_POS_X, "+x");
876 chk(x > 0 && pos.contains_key(&(x - 1, y, z)), VIS_NEG_X, "-x");
877 chk(pos.contains_key(&(x, y + 1, z)), VIS_POS_Y, "+y");
878 chk(y > 0 && pos.contains_key(&(x, y - 1, z)), VIS_NEG_Y, "-y");
879 chk(pos.contains_key(&(x, y, z + 1)), VIS_POS_Z, "+z");
880 chk(z > 0 && pos.contains_key(&(x, y, z - 1)), VIS_NEG_Z, "-z");
881 }
882 assert!(
883 checked > 100,
884 "expected many adjacent faces in coco, got {checked}"
885 );
886 }
887
888 #[test]
889 fn recompute_surface_matches_from_fn_shaded() {
890 let fill = |x: u32, y: u32, z: u32| {
893 let cx = x as f32 - 4.0;
894 let cy = y as f32 - 4.0;
895 let cz = z as f32 - 4.0;
896 (cx * cx + cy * cy + cz * cz <= 16.0).then_some(0x80_30_60_90u32)
897 };
898 let shaded = Kv6::from_fn_shaded(9, 9, 9, fill);
899 let mut edited = Kv6::from_fn(9, 9, 9, fill); edited.recompute_surface(|x, y, z| {
901 x >= 0 && y >= 0 && z >= 0 && fill(x as u32, y as u32, z as u32).is_some()
902 });
903 assert_eq!(edited.voxels.len(), shaded.voxels.len());
904 for (e, s) in edited.voxels.iter().zip(&shaded.voxels) {
905 assert_eq!((e.vis, e.dir), (s.vis, s.dir), "voxel z={}", e.z);
906 }
907 }
908
909 #[test]
910 fn from_fn_shaded_slab_top_normal_points_up() {
911 use crate::equivec::univec;
912 let kv = Kv6::from_fn_shaded(8, 8, 12, |_, _, z| {
916 (2..=9).contains(&z).then_some(0x80_aa_aa_aa)
917 });
918 let v = kv
919 .voxels
920 .iter()
921 .enumerate()
922 .find_map(|(i, v)| {
923 let mut acc = 0usize;
925 for x in 0..kv.xsiz as usize {
926 for y in 0..kv.ysiz as usize {
927 let len = kv.ylen[x][y] as usize;
928 if i < acc + len {
929 return (x == 4 && y == 4 && v.z == 2).then_some(*v);
930 }
931 acc += len;
932 }
933 }
934 None
935 })
936 .expect("centre top-face voxel present");
937 let n = univec()[v.dir as usize];
938 assert!(
939 n[2] < -0.5,
940 "top-face normal should point -z (up), got {n:?}"
941 );
942 }
943
944 #[test]
945 fn coco_palette_present_and_matches_kvx() {
946 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
947 let pal = kv6.palette.as_ref().expect("SPal trailer present");
948 assert_eq!((pal[0].r, pal[0].g, pal[0].b), (0x3f, 0x19, 0x19));
950 }
951
952 #[test]
953 fn coco_first_voxel_packed_colour() {
954 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
955 let v0 = kv6.voxels[0];
959 assert_eq!(v0.col, 0x80fc_a460);
960 assert_eq!(v0.col & 0x8000_0000, 0x8000_0000);
961 }
962
963 #[test]
964 fn coco_roundtrips_byte_equal() {
965 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
966 let out = serialize(&kv6);
967 assert_eq!(out.len(), COCO_KV6.len(), "length differs");
968 assert_eq!(out.as_slice(), COCO_KV6, "byte content differs");
969 }
970
971 #[test]
972 fn parse_truncated_header_fails() {
973 let r = parse(&[0u8; 16]);
974 assert!(matches!(r, Err(ParseError::TooSmall { .. })));
975 }
976
977 #[test]
978 fn parse_bad_magic_fails() {
979 let mut bad = COCO_KV6.to_vec();
980 bad[0] = b'X';
981 let r = parse(&bad);
982 assert!(matches!(r, Err(ParseError::BadMagic { .. })));
983 }
984}