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
56fn compute_vis_dir(occ: &impl Fn(i64, i64, i64) -> bool, x: i64, y: i64, z: i64) -> (u8, u8) {
63 let mut vis = 0u8;
64 if !occ(x - 1, y, z) {
65 vis |= VIS_NEG_X;
66 }
67 if !occ(x + 1, y, z) {
68 vis |= VIS_POS_X;
69 }
70 if !occ(x, y - 1, z) {
71 vis |= VIS_NEG_Y;
72 }
73 if !occ(x, y + 1, z) {
74 vis |= VIS_POS_Y;
75 }
76 if !occ(x, y, z - 1) {
77 vis |= VIS_NEG_Z;
78 }
79 if !occ(x, y, z + 1) {
80 vis |= VIS_POS_Z;
81 }
82
83 let mut n = [0.0f32; 3];
84 for dz in -1..=1 {
85 for dy in -1..=1 {
86 for dx in -1..=1 {
87 if (dx | dy | dz) != 0 && !occ(x + dx, y + dy, z + dz) {
88 n[0] += dx as f32;
89 n[1] += dy as f32;
90 n[2] += dz as f32;
91 }
92 }
93 }
94 }
95 (vis, crate::equivec::nearest_dir(n))
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct Voxel {
101 pub col: u32,
104 pub z: u16,
106 pub vis: u8,
109 pub dir: u8,
111}
112
113#[derive(Debug, Clone)]
116pub struct Kv6 {
117 pub xsiz: u32,
118 pub ysiz: u32,
119 pub zsiz: u32,
120 pub xpiv: f32,
121 pub ypiv: f32,
122 pub zpiv: f32,
123 pub voxels: Vec<Voxel>,
125 pub xlen: Vec<u32>,
128 pub ylen: Vec<Vec<u16>>,
131 pub palette: Option<[Rgb6; 256]>,
133}
134
135impl Kv6 {
136 #[must_use]
158 pub fn from_fn<F: Fn(u32, u32, u32) -> Option<u32>>(
159 xsiz: u32,
160 ysiz: u32,
161 zsiz: u32,
162 fill: F,
163 ) -> Kv6 {
164 Self::build_inner(xsiz, ysiz, zsiz, fill, false)
165 }
166
167 #[must_use]
181 pub fn from_fn_shaded<F: Fn(u32, u32, u32) -> Option<u32>>(
182 xsiz: u32,
183 ysiz: u32,
184 zsiz: u32,
185 fill: F,
186 ) -> Kv6 {
187 Self::build_inner(xsiz, ysiz, zsiz, fill, true)
188 }
189
190 #[allow(
194 clippy::cast_possible_truncation,
195 clippy::cast_sign_loss,
196 clippy::cast_precision_loss
197 )]
198 fn build_inner<F: Fn(u32, u32, u32) -> Option<u32>>(
199 xsiz: u32,
200 ysiz: u32,
201 zsiz: u32,
202 fill: F,
203 shaded: bool,
204 ) -> Kv6 {
205 let occupied = |x: i64, y: i64, z: i64| -> bool {
206 x >= 0
207 && y >= 0
208 && z >= 0
209 && (x as u32) < xsiz
210 && (y as u32) < ysiz
211 && (z as u32) < zsiz
212 && fill(x as u32, y as u32, z as u32).is_some()
213 };
214
215 let mut voxels: Vec<Voxel> = Vec::new();
216 let mut xlen: Vec<u32> = Vec::with_capacity(xsiz as usize);
217 let mut ylen: Vec<Vec<u16>> = Vec::with_capacity(xsiz as usize);
218
219 for x in 0..xsiz {
220 let mut col_counts: Vec<u16> = Vec::with_capacity(ysiz as usize);
221 for y in 0..ysiz {
222 let before = voxels.len();
223 for z in 0..zsiz {
224 let Some(col) = fill(x, y, z) else { continue };
225 let (xi, yi, zi) = (i64::from(x), i64::from(y), i64::from(z));
226 let exposed = !occupied(xi - 1, yi, zi)
227 || !occupied(xi + 1, yi, zi)
228 || !occupied(xi, yi - 1, zi)
229 || !occupied(xi, yi + 1, zi)
230 || !occupied(xi, yi, zi - 1)
231 || !occupied(xi, yi, zi + 1);
232 if exposed {
233 let (vis, dir) = if shaded {
234 compute_vis_dir(&occupied, xi, yi, zi)
235 } else {
236 (63, 0)
237 };
238 voxels.push(Voxel {
239 col,
240 z: z as u16,
241 vis,
242 dir,
243 });
244 }
245 }
246 col_counts.push((voxels.len() - before) as u16);
247 }
248 xlen.push(col_counts.iter().map(|&c| u32::from(c)).sum());
249 ylen.push(col_counts);
250 }
251
252 Kv6 {
253 xsiz,
254 ysiz,
255 zsiz,
256 xpiv: xsiz as f32 * 0.5,
257 ypiv: ysiz as f32 * 0.5,
258 zpiv: zsiz as f32 * 0.5,
259 voxels,
260 xlen,
261 ylen,
262 palette: None,
263 }
264 }
265
266 #[allow(clippy::cast_possible_wrap)]
274 pub fn recompute_surface(&mut self, occupied: impl Fn(i32, i32, i32) -> bool) {
275 let xsiz = self.xsiz;
276 let ysiz = self.ysiz;
277 let zsiz = self.zsiz;
278 let occ = |x: i64, y: i64, z: i64| -> bool {
279 x >= 0
280 && y >= 0
281 && z >= 0
282 && (x as u32) < xsiz
283 && (y as u32) < ysiz
284 && (z as u32) < zsiz
285 && occupied(x as i32, y as i32, z as i32)
286 };
287 let mut vi = 0usize;
288 for x in 0..xsiz as usize {
289 for y in 0..ysiz as usize {
290 let len = self.ylen[x][y] as usize;
291 for _ in 0..len {
292 let z = i64::from(self.voxels[vi].z);
293 let (vis, dir) = compute_vis_dir(&occ, x as i64, y as i64, z);
294 self.voxels[vi].vis = vis;
295 self.voxels[vi].dir = dir;
296 vi += 1;
297 }
298 }
299 }
300 }
301
302 fn surface_color_map(&self) -> HashMap<(u32, u32, u32), u32> {
308 let mut map = HashMap::with_capacity(self.voxels.len());
309 let mut vi = 0usize;
310 for x in 0..self.xsiz as usize {
311 for y in 0..self.ysiz as usize {
312 let len = self.ylen[x][y] as usize;
313 for _ in 0..len {
314 let v = self.voxels[vi];
315 #[allow(clippy::cast_lossless)]
316 map.insert((x as u32, y as u32, u32::from(v.z)), v.col);
317 vi += 1;
318 }
319 }
320 }
321 map
322 }
323
324 pub fn carve_sphere_with_colfunc<S, C>(
355 &mut self,
356 centre: [i32; 3],
357 radius: u32,
358 solid: S,
359 colfunc: C,
360 ) where
361 S: Fn(i32, i32, i32) -> bool,
362 C: Fn(i32, i32, i32) -> u32,
363 {
364 let orig = self.surface_color_map();
365 let (xpiv, ypiv, zpiv) = (self.xpiv, self.ypiv, self.zpiv);
367 let palette = self.palette;
368
369 #[allow(clippy::cast_possible_wrap)]
370 let r = radius as i32;
371 let r_sq = r * r;
372 let (cx, cy, cz) = (centre[0], centre[1], centre[2]);
373 let inside = |x: i32, y: i32, z: i32| {
374 let (dx, dy, dz) = (x - cx, y - cy, z - cz);
375 dx * dx + dy * dy + dz * dz <= r_sq
376 };
377
378 let rebuilt = Kv6::from_fn_shaded(self.xsiz, self.ysiz, self.zsiz, |x, y, z| {
379 #[allow(clippy::cast_possible_wrap)]
380 let (xi, yi, zi) = (x as i32, y as i32, z as i32);
381 if inside(xi, yi, zi) || !solid(xi, yi, zi) {
382 return None;
383 }
384 Some(
387 orig.get(&(x, y, z))
388 .copied()
389 .unwrap_or_else(|| colfunc(xi, yi, zi)),
390 )
391 });
392
393 self.voxels = rebuilt.voxels;
394 self.xlen = rebuilt.xlen;
395 self.ylen = rebuilt.ylen;
396 self.xpiv = xpiv;
397 self.ypiv = ypiv;
398 self.zpiv = zpiv;
399 self.palette = palette;
400 }
401
402 #[must_use]
405 pub fn solid_box(xsiz: u32, ysiz: u32, zsiz: u32, col: u32) -> Kv6 {
406 Kv6::from_fn(xsiz, ysiz, zsiz, |_, _, _| Some(col))
407 }
408
409 #[must_use]
411 pub fn solid_cube(n: u32, col: u32) -> Kv6 {
412 Kv6::solid_box(n, n, n, col)
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum ParseError {
419 TooSmall { got: usize },
421 BadMagic { got: [u8; 4] },
423 Truncated { at: usize, need: usize },
426}
427
428impl fmt::Display for ParseError {
429 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430 match *self {
431 Self::TooSmall { got } => write!(
432 f,
433 "kv6 file too small ({got} bytes; need at least 32 byte header)"
434 ),
435 Self::BadMagic { got } => write!(
436 f,
437 "kv6 bad magic: got [{:#04x},{:#04x},{:#04x},{:#04x}], expected b\"Kvxl\"",
438 got[0], got[1], got[2], got[3]
439 ),
440 Self::Truncated { at, need } => {
441 write!(f, "kv6 truncated: need {need} bytes at offset {at}")
442 }
443 }
444 }
445}
446
447impl std::error::Error for ParseError {}
448
449impl From<OutOfBounds> for ParseError {
450 fn from(e: OutOfBounds) -> Self {
451 Self::Truncated {
452 at: e.at,
453 need: e.need,
454 }
455 }
456}
457
458const HEADER_LEN: usize = 32;
459const MAGIC: &[u8; 4] = b"Kvxl";
460const PALETTE_MAGIC: &[u8; 4] = b"SPal";
461const PALETTE_LEN: usize = 768;
462
463pub fn parse(bytes: &[u8]) -> Result<Kv6, ParseError> {
493 if bytes.len() < HEADER_LEN {
494 return Err(ParseError::TooSmall { got: bytes.len() });
495 }
496
497 let mut cur = Cursor::new(bytes);
498 let magic = cur.read_bytes(4)?;
499 if magic != MAGIC {
500 return Err(ParseError::BadMagic {
501 got: [magic[0], magic[1], magic[2], magic[3]],
502 });
503 }
504 let xsiz = cur.read_u32()?;
505 let ysiz = cur.read_u32()?;
506 let zsiz = cur.read_u32()?;
507 let xpiv = cur.read_f32()?;
508 let ypiv = cur.read_f32()?;
509 let zpiv = cur.read_f32()?;
510 let numvoxs = cur.read_u32()?;
511
512 let mut voxels = Vec::with_capacity(numvoxs as usize);
513 for _ in 0..numvoxs {
514 let col = cur.read_u32()?;
515 let z = cur.read_u16()?;
516 let vis = cur.read_u8()?;
517 let dir = cur.read_u8()?;
518 voxels.push(Voxel { col, z, vis, dir });
519 }
520
521 let mut xlen = Vec::with_capacity(xsiz as usize);
522 for _ in 0..xsiz {
523 xlen.push(cur.read_u32()?);
524 }
525
526 let mut ylen = Vec::with_capacity(xsiz as usize);
527 for _ in 0..xsiz {
528 let mut row = Vec::with_capacity(ysiz as usize);
529 for _ in 0..ysiz {
530 row.push(cur.read_u16()?);
531 }
532 ylen.push(row);
533 }
534
535 let palette =
537 if cur.remaining() >= 4 + PALETTE_LEN && cur.peek(4) == Some(PALETTE_MAGIC.as_slice()) {
538 cur.read_bytes(4)?;
539 let mut pal = [Rgb6::default(); 256];
540 for entry in &mut pal {
541 entry.r = cur.read_u8()?;
542 entry.g = cur.read_u8()?;
543 entry.b = cur.read_u8()?;
544 }
545 Some(pal)
546 } else {
547 None
548 };
549
550 Ok(Kv6 {
551 xsiz,
552 ysiz,
553 zsiz,
554 xpiv,
555 ypiv,
556 zpiv,
557 voxels,
558 xlen,
559 ylen,
560 palette,
561 })
562}
563
564#[must_use]
574pub fn serialize(kv6: &Kv6) -> Vec<u8> {
575 let pal_bytes = if kv6.palette.is_some() {
576 4 + PALETTE_LEN
577 } else {
578 0
579 };
580 let body_bytes = kv6.voxels.len() * 8
581 + kv6.xlen.len() * 4
582 + kv6.ylen.iter().map(|row| row.len() * 2).sum::<usize>();
583 let mut out = Vec::with_capacity(HEADER_LEN + body_bytes + pal_bytes);
584
585 out.extend_from_slice(MAGIC);
586 out.extend_from_slice(&kv6.xsiz.to_le_bytes());
587 out.extend_from_slice(&kv6.ysiz.to_le_bytes());
588 out.extend_from_slice(&kv6.zsiz.to_le_bytes());
589 out.extend_from_slice(&kv6.xpiv.to_le_bytes());
590 out.extend_from_slice(&kv6.ypiv.to_le_bytes());
591 out.extend_from_slice(&kv6.zpiv.to_le_bytes());
592 let numvoxs =
593 u32::try_from(kv6.voxels.len()).expect("kv6 numvoxs must fit in u32 (file format limit)");
594 out.extend_from_slice(&numvoxs.to_le_bytes());
595
596 for v in &kv6.voxels {
597 out.extend_from_slice(&v.col.to_le_bytes());
598 out.extend_from_slice(&v.z.to_le_bytes());
599 out.push(v.vis);
600 out.push(v.dir);
601 }
602 for v in &kv6.xlen {
603 out.extend_from_slice(&v.to_le_bytes());
604 }
605 for row in &kv6.ylen {
606 for v in row {
607 out.extend_from_slice(&v.to_le_bytes());
608 }
609 }
610 if let Some(pal) = &kv6.palette {
611 out.extend_from_slice(PALETTE_MAGIC);
612 for e in pal {
613 out.push(e.r);
614 out.push(e.g);
615 out.push(e.b);
616 }
617 }
618
619 out
620}
621
622#[cfg(test)]
625mod tests {
626 use super::*;
627
628 const COCO_KV6: &[u8] = include_bytes!("../../../assets/coco.kv6");
630
631 #[test]
632 fn solid_cube_builder_is_surface_only_and_consistent() {
633 let cube = Kv6::solid_cube(4, 0x8012_3456);
634 assert_eq!((cube.xsiz, cube.ysiz, cube.zsiz), (4, 4, 4));
635 assert!((cube.xpiv - 2.0).abs() < f32::EPSILON);
637
638 assert_eq!(cube.voxels.len(), 64 - 8);
641 assert!(cube
642 .voxels
643 .iter()
644 .all(|v| v.vis == 63 && v.col == 0x8012_3456));
645
646 assert_eq!(cube.xlen.len(), 4);
648 assert_eq!(cube.ylen.len(), 4);
649 assert!(cube.ylen.iter().all(|row| row.len() == 4));
650 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
651 let ylen_sum: usize = cube
652 .ylen
653 .iter()
654 .flat_map(|r| r.iter())
655 .map(|&n| n as usize)
656 .sum();
657 assert_eq!(xlen_sum, cube.voxels.len());
658 assert_eq!(ylen_sum, cube.voxels.len());
659 }
660
661 fn color_at(kv6: &Kv6, tx: u32, ty: u32, tz: u32) -> Option<u32> {
664 let mut vi = 0usize;
665 for x in 0..kv6.xsiz {
666 for y in 0..kv6.ysiz {
667 let len = kv6.ylen[x as usize][y as usize] as usize;
668 for _ in 0..len {
669 let v = kv6.voxels[vi];
670 if x == tx && y == ty && u32::from(v.z) == tz {
671 return Some(v.col);
672 }
673 vi += 1;
674 }
675 }
676 }
677 None
678 }
679
680 #[test]
681 fn carve_sphere_exposes_interior_with_colfunc() {
682 const BASE: u32 = 0x8011_2233;
683 let mut cube = Kv6::from_fn_shaded(16, 16, 16, |_, _, _| Some(BASE));
685 cube.xpiv = 1.0;
687 cube.ypiv = 2.0;
688 cube.zpiv = 3.0;
689
690 let encode = |x: i32, y: i32, z: i32| ((x << 16) | (y << 8) | z) as u32;
693 cube.carve_sphere_with_colfunc([8, 8, 8], 4, |_, _, _| true, encode);
694
695 assert_eq!(color_at(&cube, 8, 8, 8), None);
697 assert_eq!(color_at(&cube, 8, 8, 3), Some(encode(8, 8, 3)));
701 assert_eq!(color_at(&cube, 0, 8, 8), Some(BASE));
703
704 assert!((cube.xpiv - 1.0).abs() < f32::EPSILON);
706 assert!((cube.ypiv - 2.0).abs() < f32::EPSILON);
707 assert!((cube.zpiv - 3.0).abs() < f32::EPSILON);
708
709 let xlen_sum: usize = cube.xlen.iter().map(|&n| n as usize).sum();
711 assert_eq!(xlen_sum, cube.voxels.len());
712 }
713
714 #[test]
715 fn carve_sphere_respects_caller_solid_predicate() {
716 const BASE: u32 = 0x80AA_BBCC;
717 let solid = |x: i32, _y: i32, _z: i32| (0..8).contains(&x);
721 #[allow(clippy::cast_sign_loss)]
722 let mut m =
723 Kv6::from_fn_shaded(16, 16, 16, |x, _, _| solid(x as i32, 0, 0).then_some(BASE));
724 m.carve_sphere_with_colfunc([4, 8, 8], 3, solid, |_, _, _| 0x8000_FF00);
725 assert_eq!(color_at(&m, 12, 8, 8), None);
727 assert_eq!(color_at(&m, 4, 8, 8), None);
729 }
730
731 #[test]
732 fn built_cube_round_trips_through_serialize_parse() {
733 let cube = Kv6::solid_cube(5, 0x80AB_CDEF);
734 let bytes = serialize(&cube);
735 let back = parse(&bytes).expect("parse built cube");
736 assert_eq!(back.xsiz, cube.xsiz);
737 assert_eq!(back.voxels.len(), cube.voxels.len());
738 assert_eq!(
739 serialize(&back),
740 bytes,
741 "serialize is stable across round-trip"
742 );
743 }
744
745 #[test]
746 fn from_fn_skips_air_and_keeps_z_order() {
747 let kv6 = Kv6::from_fn(1, 1, 2, |_, _, _| Some(0x8000_FF00));
750 assert_eq!(kv6.voxels.len(), 2);
751 assert_eq!(kv6.voxels[0].z, 0);
752 assert_eq!(kv6.voxels[1].z, 1);
753 assert_eq!(kv6.xlen, vec![2]);
754 assert_eq!(kv6.ylen, vec![vec![2]]);
755 }
756
757 #[test]
758 fn parse_coco_header() {
759 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
760 assert_eq!(kv6.xsiz, 9);
761 assert_eq!(kv6.ysiz, 11);
762 assert_eq!(kv6.zsiz, 9);
763 assert!((kv6.xpiv - 2.0).abs() < f32::EPSILON);
765 assert!((kv6.ypiv - 3.0).abs() < f32::EPSILON);
766 assert!((kv6.zpiv - 9.0).abs() < f32::EPSILON);
767 assert_eq!(kv6.voxels.len(), 148);
768 }
769
770 #[test]
771 fn coco_voxel_counts_consistent() {
772 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
773 assert_eq!(kv6.xlen.len(), kv6.xsiz as usize);
774 assert_eq!(kv6.ylen.len(), kv6.xsiz as usize);
775 for row in &kv6.ylen {
776 assert_eq!(row.len(), kv6.ysiz as usize);
777 }
778 let xlen_sum: u64 = kv6.xlen.iter().map(|&n| u64::from(n)).sum();
779 let ylen_sum: u64 = kv6
780 .ylen
781 .iter()
782 .flat_map(|row| row.iter().map(|&n| u64::from(n)))
783 .sum();
784 let nv = kv6.voxels.len() as u64;
785 assert_eq!(xlen_sum, nv);
786 assert_eq!(ylen_sum, nv);
787 }
788
789 #[test]
790 fn from_fn_shaded_keeps_from_fn_geometry() {
791 let fill = |x: u32, y: u32, z: u32| {
794 let on_face = x == 0 || x == 4 || y == 0 || y == 4 || z == 0 || z == 4;
795 on_face.then_some(0x80_44_55_66u32)
796 };
797 let flat = Kv6::from_fn(5, 5, 5, fill);
798 let shaded = Kv6::from_fn_shaded(5, 5, 5, fill);
799 assert_eq!(flat.voxels.len(), shaded.voxels.len());
800 assert_eq!(flat.xlen, shaded.xlen);
801 assert_eq!(flat.ylen, shaded.ylen);
802 for (f, s) in flat.voxels.iter().zip(&shaded.voxels) {
803 assert_eq!((f.col, f.z), (s.col, s.z));
804 }
805 assert!(
807 shaded.voxels.iter().any(|v| v.dir != 0),
808 "from_fn_shaded left every dir flat"
809 );
810 assert!(flat.voxels.iter().all(|v| v.dir == 0 && v.vis == 63));
811 }
812
813 #[test]
814 fn from_fn_shaded_column_z_faces() {
815 let kv = Kv6::from_fn_shaded(1, 1, 2, |_, _, _| Some(0x80_80_80_80));
820 assert_eq!(kv.voxels.len(), 2);
821 let (lower, upper) = (&kv.voxels[0], &kv.voxels[1]); assert_eq!(lower.z, 0);
823 assert_eq!(upper.z, 1);
824 assert_eq!(lower.vis & VIS_POS_Z, 0, "lower +z should be internal");
825 assert_eq!(lower.vis & VIS_NEG_Z, VIS_NEG_Z, "lower -z exposed");
826 assert_eq!(upper.vis & VIS_NEG_Z, 0, "upper -z should be internal");
827 assert_eq!(upper.vis & VIS_POS_Z, VIS_POS_Z, "upper +z exposed");
828 let sides = VIS_NEG_X | VIS_POS_X | VIS_NEG_Y | VIS_POS_Y;
830 assert_eq!(lower.vis & sides, sides);
831 assert_eq!(upper.vis & sides, sides);
832 }
833
834 #[test]
844 fn coco_vis_matches_authored_all_faces() {
845 use std::collections::HashMap;
846 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
847 let mut pos: HashMap<(u32, u32, u32), u8> = HashMap::new();
848 let mut vi = 0usize;
849 for x in 0..kv6.xsiz {
850 for y in 0..kv6.ysiz {
851 let len = kv6.ylen[x as usize][y as usize] as usize;
852 for _ in 0..len {
853 pos.insert((x, y, u32::from(kv6.voxels[vi].z)), kv6.voxels[vi].vis);
854 vi += 1;
855 }
856 }
857 }
858 let mut checked = 0u32;
859 for (&(x, y, z), &vis) in &pos {
860 let mut chk = |present: bool, bit: u8, face: &str| {
861 if present {
862 assert_eq!(
863 vis & bit,
864 0,
865 "coco ({x},{y},{z}): {face} internal but bit set"
866 );
867 checked += 1;
868 }
869 };
870 chk(pos.contains_key(&(x + 1, y, z)), VIS_POS_X, "+x");
871 chk(x > 0 && pos.contains_key(&(x - 1, y, z)), VIS_NEG_X, "-x");
872 chk(pos.contains_key(&(x, y + 1, z)), VIS_POS_Y, "+y");
873 chk(y > 0 && pos.contains_key(&(x, y - 1, z)), VIS_NEG_Y, "-y");
874 chk(pos.contains_key(&(x, y, z + 1)), VIS_POS_Z, "+z");
875 chk(z > 0 && pos.contains_key(&(x, y, z - 1)), VIS_NEG_Z, "-z");
876 }
877 assert!(
878 checked > 100,
879 "expected many adjacent faces in coco, got {checked}"
880 );
881 }
882
883 #[test]
884 fn recompute_surface_matches_from_fn_shaded() {
885 let fill = |x: u32, y: u32, z: u32| {
888 let cx = x as f32 - 4.0;
889 let cy = y as f32 - 4.0;
890 let cz = z as f32 - 4.0;
891 (cx * cx + cy * cy + cz * cz <= 16.0).then_some(0x80_30_60_90u32)
892 };
893 let shaded = Kv6::from_fn_shaded(9, 9, 9, fill);
894 let mut edited = Kv6::from_fn(9, 9, 9, fill); edited.recompute_surface(|x, y, z| {
896 x >= 0 && y >= 0 && z >= 0 && fill(x as u32, y as u32, z as u32).is_some()
897 });
898 assert_eq!(edited.voxels.len(), shaded.voxels.len());
899 for (e, s) in edited.voxels.iter().zip(&shaded.voxels) {
900 assert_eq!((e.vis, e.dir), (s.vis, s.dir), "voxel z={}", e.z);
901 }
902 }
903
904 #[test]
905 fn from_fn_shaded_slab_top_normal_points_up() {
906 use crate::equivec::univec;
907 let kv = Kv6::from_fn_shaded(8, 8, 12, |_, _, z| {
911 (2..=9).contains(&z).then_some(0x80_aa_aa_aa)
912 });
913 let v = kv
914 .voxels
915 .iter()
916 .enumerate()
917 .find_map(|(i, v)| {
918 let mut acc = 0usize;
920 for x in 0..kv.xsiz as usize {
921 for y in 0..kv.ysiz as usize {
922 let len = kv.ylen[x][y] as usize;
923 if i < acc + len {
924 return (x == 4 && y == 4 && v.z == 2).then_some(*v);
925 }
926 acc += len;
927 }
928 }
929 None
930 })
931 .expect("centre top-face voxel present");
932 let n = univec()[v.dir as usize];
933 assert!(
934 n[2] < -0.5,
935 "top-face normal should point -z (up), got {n:?}"
936 );
937 }
938
939 #[test]
940 fn coco_palette_present_and_matches_kvx() {
941 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
942 let pal = kv6.palette.as_ref().expect("SPal trailer present");
943 assert_eq!((pal[0].r, pal[0].g, pal[0].b), (0x3f, 0x19, 0x19));
945 }
946
947 #[test]
948 fn coco_first_voxel_packed_colour() {
949 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
950 let v0 = kv6.voxels[0];
954 assert_eq!(v0.col, 0x80fc_a460);
955 assert_eq!(v0.col & 0x8000_0000, 0x8000_0000);
956 }
957
958 #[test]
959 fn coco_roundtrips_byte_equal() {
960 let kv6 = parse(COCO_KV6).expect("parse coco.kv6");
961 let out = serialize(&kv6);
962 assert_eq!(out.len(), COCO_KV6.len(), "length differs");
963 assert_eq!(out.as_slice(), COCO_KV6, "byte content differs");
964 }
965
966 #[test]
967 fn parse_truncated_header_fails() {
968 let r = parse(&[0u8; 16]);
969 assert!(matches!(r, Err(ParseError::TooSmall { .. })));
970 }
971
972 #[test]
973 fn parse_bad_magic_fails() {
974 let mut bad = COCO_KV6.to_vec();
975 bad[0] = b'X';
976 let r = parse(&bad);
977 assert!(matches!(r, Err(ParseError::BadMagic { .. })));
978 }
979}