1use std::collections::{HashMap, VecDeque};
24
25use glam::{DVec3, IVec3, Vec3};
26use roxlap_formats::color::VoxColor;
27use roxlap_formats::kv6::Kv6;
28use roxlap_formats::vxl::Vxl;
29
30use crate::{grid_local_to_world, BakeMode, Grid, GridTransform, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
31
32pub const DEFAULT_ISLAND_BUDGET: usize = 4096;
36
37#[derive(Debug, Clone)]
40pub struct Island {
41 pub voxels: Vec<(IVec3, VoxColor)>,
45 pub bbox: (IVec3, IVec3),
47}
48
49impl Island {
50 pub fn extract(&self, grid: &mut Grid, bake: BakeMode) {
67 if self.voxels.is_empty() {
68 return;
69 }
70 let mut pending: Option<(IVec3, IVec3)> = None;
71 for &(v, _) in &self.voxels {
72 if let Some((lo, hi)) = pending {
73 if v.x == hi.x && v.y == hi.y && v.z == hi.z + 1 {
74 pending = Some((lo, v));
75 continue;
76 }
77 grid.set_rect(lo, hi, None);
78 }
79 pending = Some((v, v));
80 }
81 if let Some((lo, hi)) = pending {
82 grid.set_rect(lo, hi, None);
83 }
84 let (lo, hi) = self.bbox;
85 grid.bake_bbox(lo, hi, bake);
86 }
87
88 #[must_use]
98 #[allow(clippy::cast_possible_wrap)]
99 pub fn to_kv6(&self) -> Kv6 {
100 let (lo, hi) = self.bbox;
101 let dims = (hi - lo + IVec3::ONE).as_uvec3();
102 let map: HashMap<IVec3, u32> = self
103 .voxels
104 .iter()
105 .map(|&(v, c)| (v - lo, (c.0 & 0x00ff_ffff) | 0x8000_0000))
106 .collect();
107 Kv6::from_fn(dims.x, dims.y, dims.z, |x, y, z| {
108 map.get(&IVec3::new(x as i32, y as i32, z as i32))
109 .map(|&c| VoxColor(c))
110 })
111 }
112
113 #[must_use]
117 pub fn world_origin(&self, transform: &GridTransform) -> DVec3 {
118 let (chunk, in_chunk) = crate::voxel_split(self.bbox.0);
119 grid_local_to_world(chunk, in_chunk, Vec3::ZERO, transform)
120 }
121
122 #[must_use]
127 pub fn world_pivot(&self, transform: &GridTransform) -> DVec3 {
128 let (chunk, in_chunk) = crate::voxel_split(self.bbox.0);
129 let dims = (self.bbox.1 - self.bbox.0 + IVec3::ONE).as_vec3();
130 grid_local_to_world(chunk, in_chunk, dims * 0.5, transform)
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub enum FracturePattern {
139 Whole,
141 Chunks {
144 cell: u32,
146 },
147 Shards {
151 plates: u32,
153 },
154}
155
156struct SplitMix(u64);
160
161impl SplitMix {
162 fn next(&mut self) -> u64 {
163 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
164 let mut z = self.0;
165 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
166 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
167 z ^ (z >> 31)
168 }
169 #[allow(clippy::cast_precision_loss)]
171 fn unit(&mut self) -> f64 {
172 (self.next() >> 11) as f64 / (1u64 << 53) as f64
173 }
174 #[allow(clippy::cast_possible_truncation)]
176 fn below(&mut self, n: u32) -> i32 {
177 (self.next() % u64::from(n.max(1))) as i32
178 }
179}
180
181impl Island {
182 #[must_use]
192 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
193 pub fn split(&self, pattern: FracturePattern, seed: u64) -> Vec<Island> {
194 let assign: Vec<usize> = match pattern {
195 FracturePattern::Whole => return vec![self.clone()],
196 FracturePattern::Chunks { cell } => {
197 let cell = i32::try_from(cell.max(2)).unwrap_or(i32::MAX);
198 let mut rng = SplitMix(seed ^ 0xC0C0);
199 let mut cell_site: HashMap<IVec3, usize> = HashMap::new();
209 let mut sites: Vec<IVec3> = Vec::new();
210 for &(v, _) in &self.voxels {
211 let cc = IVec3::new(
212 v.x.div_euclid(cell),
213 v.y.div_euclid(cell),
214 v.z.div_euclid(cell),
215 );
216 if let std::collections::hash_map::Entry::Vacant(e) = cell_site.entry(cc) {
217 let jitter = IVec3::new(
218 rng.below(cell as u32),
219 rng.below(cell as u32),
220 rng.below(cell as u32),
221 );
222 e.insert(sites.len());
223 sites.push(cc * cell + jitter);
224 }
225 }
226 self.voxels
227 .iter()
228 .map(|&(v, _)| {
229 let mut best = 0usize;
230 let mut best_d = i64::MAX;
231 for (i, s) in sites.iter().enumerate() {
232 let d = (v - *s).as_i64vec3().length_squared();
233 if d < best_d {
234 best_d = d;
235 best = i;
236 }
237 }
238 best
239 })
240 .collect()
241 }
242 FracturePattern::Shards { plates } => {
243 let k = plates.clamp(2, 8);
244 let mut rng = SplitMix(seed ^ 0x5AAD);
245 let z = 2.0 * rng.unit() - 1.0;
249 let phi = std::f64::consts::TAU * rng.unit();
250 let r = (1.0 - z * z).max(0.0).sqrt();
251 let n = glam::DVec3::new(r * phi.cos(), r * phi.sin(), z);
252 let projs: Vec<f64> = self
253 .voxels
254 .iter()
255 .map(|&(v, _)| v.as_dvec3().dot(n))
256 .collect();
257 let (mut pmin, mut pmax) = (f64::MAX, f64::MIN);
258 for &p in &projs {
259 pmin = pmin.min(p);
260 pmax = pmax.max(p);
261 }
262 let range = (pmax - pmin).max(1e-9);
263 let step = range / f64::from(k);
264 let bounds: Vec<f64> = (1..k)
265 .map(|i| pmin + step * (f64::from(i) + 0.4 * (rng.unit() - 0.5)))
266 .collect();
267 projs
268 .iter()
269 .map(|&p| bounds.iter().filter(|&&b| p >= b).count())
270 .collect()
271 }
272 };
273 let mut order: Vec<usize> = Vec::new();
276 let mut groups: HashMap<usize, Vec<(IVec3, VoxColor)>> = HashMap::new();
277 for (i, &(v, c)) in self.voxels.iter().enumerate() {
278 let g = assign[i];
279 groups.entry(g).or_insert_with(|| {
280 order.push(g);
281 Vec::new()
282 });
283 groups.get_mut(&g).expect("just inserted").push((v, c));
284 }
285 order
286 .into_iter()
287 .map(|g| {
288 let voxels = groups.remove(&g).expect("grouped above");
289 let mut lo = IVec3::MAX;
290 let mut hi = IVec3::MIN;
291 for &(v, _) in &voxels {
292 lo = lo.min(v);
293 hi = hi.max(v);
294 }
295 Island {
296 voxels,
297 bbox: (lo, hi),
298 }
299 })
300 .collect()
301 }
302}
303
304#[derive(Clone, Copy)]
308struct Run {
309 top: i32,
310 bot: i32,
311 anchored: bool,
312}
313
314fn chunk_column_runs(vxl: &Vxl, x: u32, y: u32, mut f: impl FnMut(i32, i32, bool)) {
319 let idx = (y * vxl.vsid + x) as usize;
320 let slab = vxl.column_data(idx);
321 let mut top = i32::from(slab[1]);
322 let mut v = 0usize;
323 while slab[v] != 0 {
324 v += usize::from(slab[v]) * 4;
325 if slab[v + 3] >= slab[v + 1] {
326 continue;
329 }
330 let bot = i32::from(slab[v + 3]);
331 f(top, bot, false);
332 top = i32::from(slab[v + 1]);
333 }
334 #[allow(clippy::cast_possible_wrap)]
335 f(top, CHUNK_SIZE_Z as i32, true);
336}
337
338#[derive(Default)]
343struct ColHash(u64);
344
345impl std::hash::Hasher for ColHash {
346 fn finish(&self) -> u64 {
347 self.0
348 }
349 fn write(&mut self, bytes: &[u8]) {
350 for &b in bytes {
351 self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
352 }
353 }
354 #[allow(clippy::cast_sign_loss)]
355 fn write_i32(&mut self, i: i32) {
356 self.0 = (self.0 ^ u64::from(i as u32)).wrapping_mul(0xf135_7aea_2e62_a9c5);
358 self.0 = self.0.rotate_left(26);
359 }
360}
361
362type ColMap<V> = HashMap<(i32, i32), V, std::hash::BuildHasherDefault<ColHash>>;
363
364struct Flood<'a> {
372 grid: &'a Grid,
373 stacks: ColMap<Vec<i32>>,
376 columns: ColMap<(u32, u32)>,
378 runs: Vec<Run>,
380 visited: Vec<u32>,
383 last_chunk: Option<(IVec3, Option<&'a Vxl>)>,
386}
387
388impl<'a> Flood<'a> {
389 fn new(grid: &'a Grid) -> Self {
390 let mut stacks: ColMap<Vec<i32>> = ColMap::default();
391 for k in grid.chunks.keys() {
392 stacks.entry((k.x, k.y)).or_default().push(k.z);
393 }
394 for v in stacks.values_mut() {
395 v.sort_unstable();
396 }
397 Self {
398 grid,
399 stacks,
400 columns: ColMap::default(),
401 runs: Vec::new(),
402 visited: Vec::new(),
403 last_chunk: None,
404 }
405 }
406
407 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
418 fn column_range(&mut self, x: i32, y: i32) -> (u32, u32) {
419 if let Some(&r) = self.columns.get(&(x, y)) {
420 return r;
421 }
422 let Self {
423 grid,
424 stacks,
425 columns,
426 runs,
427 visited,
428 last_chunk,
429 } = self;
430 let start = runs.len();
431 let chx = x.div_euclid(CHUNK_SIZE_XY as i32);
432 let chy = y.div_euclid(CHUNK_SIZE_XY as i32);
433 let lx = x.rem_euclid(CHUNK_SIZE_XY as i32) as u32;
434 let ly = y.rem_euclid(CHUNK_SIZE_XY as i32) as u32;
435 let stack: &[i32] = stacks.get(&(chx, chy)).map_or(&[], Vec::as_slice);
436 for &chz in stack {
437 let idx = IVec3::new(chx, chy, chz);
438 let vxl = match *last_chunk {
439 Some((c, v)) if c == idx => v,
440 _ => {
441 let v = grid.chunk(idx);
442 *last_chunk = Some((idx, v));
443 v
444 }
445 };
446 let Some(vxl) = vxl else { continue };
447 let base = chz * CHUNK_SIZE_Z as i32;
448 chunk_column_runs(vxl, lx, ly, |top, bot, is_final| {
449 let (top_g, bot_g) = (base + top, base + bot);
450 let anchored = is_final;
451 if runs.len() > start {
452 if let Some(last) = runs.last_mut() {
453 if last.bot == top_g {
454 last.bot = bot_g;
455 last.anchored |= anchored;
456 return;
457 }
458 }
459 }
460 runs.push(Run {
461 top: top_g,
462 bot: bot_g,
463 anchored,
464 });
465 });
466 }
467 visited.resize(runs.len(), 0);
468 let r = (start as u32, (runs.len() - start) as u32);
469 columns.insert((x, y), r);
470 r
471 }
472}
473
474#[must_use]
486pub fn detect_islands(grid: &Grid, carve_lo: IVec3, carve_hi: IVec3, budget: usize) -> Vec<Island> {
487 let lo = carve_lo.min(carve_hi) - IVec3::ONE;
488 let hi = carve_lo.max(carve_hi) + IVec3::ONE;
489
490 let mut fl = Flood::new(grid);
491 let mut islands = Vec::new();
492 let mut traversal: u32 = 0;
493
494 for y in lo.y..=hi.y {
495 for x in lo.x..=hi.x {
496 let (start, len) = fl.column_range(x, y);
497 for i in start..start + len {
498 let seed = fl.runs[i as usize];
499 if seed.bot <= lo.z || seed.top > hi.z {
501 continue;
502 }
503 if fl.visited[i as usize] != 0 {
504 continue;
505 }
506 traversal += 1;
507 fl.visited[i as usize] = traversal;
508 if let Some(comp) = flood_component(&mut fl, (x, y), seed, budget, traversal) {
509 islands.push(build_island(grid, &comp));
510 }
511 }
512 }
513 }
514 islands
515}
516
517fn flood_component(
521 fl: &mut Flood<'_>,
522 seed_col: (i32, i32),
523 seed: Run,
524 budget: usize,
525 traversal: u32,
526) -> Option<Vec<(i32, i32, Run)>> {
527 let mut queue: VecDeque<(i32, i32, Run)> = VecDeque::new();
528 queue.push_back((seed_col.0, seed_col.1, seed));
529
530 let mut comp: Vec<(i32, i32, Run)> = Vec::new();
531 let mut count = 0usize;
532
533 while let Some((cx, cy, r)) = queue.pop_front() {
534 if r.anchored {
535 return None; }
537 count += usize::try_from(r.bot - r.top).unwrap_or(usize::MAX);
538 if count > budget {
539 return None; }
541 comp.push((cx, cy, r));
542 for (nx, ny) in [(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)] {
543 let (start, len) = fl.column_range(nx, ny);
544 for i in (start as usize)..(start + len) as usize {
545 let nr = fl.runs[i];
546 if nr.top >= r.bot || nr.bot <= r.top {
547 continue; }
549 let v = fl.visited[i];
550 if v == traversal {
551 continue;
552 }
553 if v != 0 {
554 return None;
560 }
561 fl.visited[i] = traversal;
562 queue.push_back((nx, ny, nr));
563 }
564 }
565 }
566 Some(comp)
567}
568
569fn build_island(grid: &Grid, comp: &[(i32, i32, Run)]) -> Island {
574 let mut voxels = Vec::new();
575 let mut lo = IVec3::MAX;
576 let mut hi = IVec3::MIN;
577 for &(x, y, r) in comp {
578 let mut last = VoxColor(0x8080_8080);
579 for z in r.top..r.bot {
580 let v = IVec3::new(x, y, z);
581 let c = grid.voxel_color(v).unwrap_or(last);
582 last = c;
583 voxels.push((v, c));
584 lo = lo.min(v);
585 hi = hi.max(v);
586 }
587 }
588 Island {
589 voxels,
590 bbox: (lo, hi),
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::GridTransform;
598 use roxlap_formats::color::VoxColor;
599
600 const STONE: VoxColor = VoxColor(0x80B0_8040);
601
602 fn grid() -> Grid {
603 Grid::new(GridTransform::identity())
604 }
605
606 fn pillar(g: &mut Grid, x: i32, y: i32, z0: i32) {
609 g.set_rect(IVec3::new(x, y, z0), IVec3::new(x, y, 255), Some(STONE));
610 }
611
612 fn island_positions(i: &Island) -> Vec<IVec3> {
613 let mut v: Vec<IVec3> = i.voxels.iter().map(|&(p, _)| p).collect();
614 v.sort_by_key(|p| (p.z, p.y, p.x));
615 v
616 }
617
618 #[test]
621 fn beam_cut_detaches_tip() {
622 let mut g = grid();
623 pillar(&mut g, 2, 2, 100);
624 g.set_rect(IVec3::new(3, 2, 100), IVec3::new(6, 2, 100), Some(STONE));
626
627 assert!(detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096).is_empty());
629
630 g.set_voxel(IVec3::new(3, 2, 100), None);
632 let islands = detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096);
633 assert_eq!(islands.len(), 1);
634 let isl = &islands[0];
635 assert_eq!(
636 island_positions(isl),
637 vec![
638 IVec3::new(4, 2, 100),
639 IVec3::new(5, 2, 100),
640 IVec3::new(6, 2, 100)
641 ]
642 );
643 assert!(isl.voxels.iter().all(|&(_, c)| c == STONE));
644 assert_eq!(isl.bbox, (IVec3::new(4, 2, 100), IVec3::new(6, 2, 100)));
645 }
646
647 #[test]
650 fn arch_needs_both_legs_cut() {
651 let mut g = grid();
652 pillar(&mut g, 2, 5, 101);
653 pillar(&mut g, 8, 5, 101);
654 g.set_rect(IVec3::new(2, 5, 100), IVec3::new(8, 5, 100), Some(STONE));
655
656 g.set_voxel(IVec3::new(2, 5, 101), None);
658 assert!(
659 detect_islands(&g, IVec3::new(2, 5, 101), IVec3::new(2, 5, 101), 4096).is_empty(),
660 "beam still hangs off the right leg"
661 );
662
663 g.set_voxel(IVec3::new(8, 5, 101), None);
665 let islands = detect_islands(&g, IVec3::new(8, 5, 101), IVec3::new(8, 5, 101), 4096);
666 assert_eq!(islands.len(), 1);
667 assert_eq!(islands[0].voxels.len(), 7, "beam x ∈ [2, 8] at z=100");
668 }
669
670 #[test]
673 fn island_crosses_chunk_border() {
674 let mut g = grid();
675 pillar(&mut g, 133, 5, 101);
676 g.set_rect(
677 IVec3::new(124, 5, 100),
678 IVec3::new(133, 5, 100),
679 Some(STONE),
680 );
681
682 g.set_voxel(IVec3::new(132, 5, 100), None);
683 let islands = detect_islands(&g, IVec3::new(132, 5, 100), IVec3::new(132, 5, 100), 4096);
684 assert_eq!(islands.len(), 1);
685 assert_eq!(islands[0].voxels.len(), 8, "x ∈ [124, 131] at z=100");
686 let (lo, hi) = islands[0].bbox;
687 assert!(lo.x < 128 && hi.x >= 128, "bbox spans the border");
688 }
689
690 #[test]
693 fn budget_exceeded_stays_supported() {
694 let mut g = grid();
695 pillar(&mut g, 10, 10, 101);
696 g.set_rect(IVec3::new(1, 1, 100), IVec3::new(20, 20, 100), Some(STONE));
697
698 g.set_voxel(IVec3::new(10, 10, 101), None);
699 let cut = (IVec3::new(10, 10, 101), IVec3::new(10, 10, 101));
700 assert!(
701 detect_islands(&g, cut.0, cut.1, 100).is_empty(),
702 "400-voxel plate exceeds a 100-voxel budget"
703 );
704 let islands = detect_islands(&g, cut.0, cut.1, 10_000);
705 assert_eq!(islands.len(), 1);
706 assert_eq!(islands[0].voxels.len(), 400);
707 }
708
709 #[test]
711 fn carve_in_air_finds_nothing() {
712 let mut g = grid();
713 pillar(&mut g, 2, 2, 100);
714 assert!(
715 detect_islands(&g, IVec3::new(50, 50, 50), IVec3::new(52, 52, 52), 4096).is_empty()
716 );
717 }
718
719 #[test]
723 fn two_islands_from_one_carve() {
724 let mut g = grid();
725 pillar(&mut g, 5, 5, 101);
726 g.set_voxel(IVec3::new(5, 5, 100), Some(STONE));
728 g.set_rect(IVec3::new(6, 5, 100), IVec3::new(8, 5, 100), Some(STONE));
730 g.set_rect(IVec3::new(2, 5, 100), IVec3::new(4, 5, 100), Some(STONE));
731
732 assert!(detect_islands(&g, IVec3::new(5, 5, 100), IVec3::new(5, 5, 100), 4096).is_empty());
734
735 g.set_voxel(IVec3::new(5, 5, 100), None);
737 let islands = detect_islands(&g, IVec3::new(5, 5, 100), IVec3::new(5, 5, 100), 4096);
738 assert_eq!(islands.len(), 2);
739 let mut sizes: Vec<usize> = islands.iter().map(|i| i.voxels.len()).collect();
740 sizes.sort_unstable();
741 assert_eq!(sizes, vec![3, 3]);
742 }
743
744 #[test]
752 fn stacked_chz_bedrock_is_anchored() {
753 let mut g = grid();
754 g.set_rect(IVec3::new(5, 5, 200), IVec3::new(5, 5, 400), Some(STONE));
757
758 g.set_voxel(IVec3::new(5, 5, 300), None);
759 let islands = detect_islands(&g, IVec3::new(5, 5, 300), IVec3::new(5, 5, 300), 100_000);
760 assert_eq!(islands.len(), 1, "only the under-cut segment falls");
761 assert_eq!(islands[0].voxels.len(), 100, "z ∈ [301, 400]");
762 assert_eq!(
763 islands[0].bbox,
764 (IVec3::new(5, 5, 301), IVec3::new(5, 5, 400))
765 );
766 }
767
768 #[test]
772 #[ignore = "manual perf probe — cargo test -p roxlap-scene --lib islands -- --ignored --nocapture"]
773 fn perf_probe() {
774 let mut g = grid();
777 for (x, y) in [(1, 1), (98, 1), (1, 98), (98, 98)] {
778 pillar(&mut g, x, y, 101);
779 }
780 g.set_rect(IVec3::new(0, 0, 100), IVec3::new(99, 99, 100), Some(STONE));
781 g.set_sphere(IVec3::new(50, 50, 100), 4, None);
782 let t = std::time::Instant::now();
783 let n = detect_islands(
784 &g,
785 IVec3::new(46, 46, 96),
786 IVec3::new(54, 54, 104),
787 DEFAULT_ISLAND_BUDGET,
788 )
789 .len();
790 let supported_exit = t.elapsed();
791 assert_eq!(n, 0);
792
793 let mut g = grid();
795 pillar(&mut g, 2, 2, 100);
796 g.set_rect(IVec3::new(3, 2, 100), IVec3::new(66, 2, 100), Some(STONE));
797 g.set_voxel(IVec3::new(3, 2, 100), None);
798 let t = std::time::Instant::now();
799 let islands = detect_islands(
800 &g,
801 IVec3::new(3, 2, 100),
802 IVec3::new(3, 2, 100),
803 DEFAULT_ISLAND_BUDGET,
804 );
805 let detach = t.elapsed();
806 assert_eq!(islands[0].voxels.len(), 63);
807
808 eprintln!("supported-exit (budget {DEFAULT_ISLAND_BUDGET}): {supported_exit:?}");
809 eprintln!("63-voxel beam detach: {detach:?}");
810 }
811
812 #[test]
818 fn extract_leaves_air_and_detection_clean() {
819 let mut g = grid();
820 pillar(&mut g, 2, 2, 100);
821 g.set_rect(IVec3::new(3, 2, 100), IVec3::new(6, 2, 100), Some(STONE));
822 g.set_voxel(IVec3::new(3, 2, 100), None);
823 let cut = (IVec3::new(3, 2, 100), IVec3::new(3, 2, 100));
824 let islands = detect_islands(&g, cut.0, cut.1, 4096);
825 let isl = islands[0].clone();
826
827 let v_before = g.chunk_version(IVec3::ZERO);
828 isl.extract(&mut g, BakeMode::Directional);
829
830 for &(v, _) in &isl.voxels {
831 assert!(!g.voxel_solid(v), "extracted voxel {v} must be air");
832 }
833 assert!(
834 g.voxel_solid(IVec3::new(2, 2, 100)),
835 "the supported pillar stays"
836 );
837 assert!(
838 detect_islands(&g, cut.0, cut.1, 4096).is_empty(),
839 "nothing left to detach"
840 );
841 assert!(
842 g.chunk_version(IVec3::ZERO) > v_before,
843 "renderers must see the extraction"
844 );
845 }
846
847 #[test]
851 fn extract_vertical_run_across_chz() {
852 let mut g = grid();
853 g.set_rect(IVec3::new(5, 5, 200), IVec3::new(5, 5, 400), Some(STONE));
854 g.set_voxel(IVec3::new(5, 5, 300), None);
855 let islands = detect_islands(&g, IVec3::new(5, 5, 300), IVec3::new(5, 5, 300), 100_000);
856 islands[0].extract(&mut g, BakeMode::Directional);
857 for z in 301..=400 {
858 assert!(!g.voxel_solid(IVec3::new(5, 5, z)), "z={z} must be air");
859 }
860 assert!(
861 g.voxel_solid(IVec3::new(5, 5, 299)),
862 "the pinned upper segment stays"
863 );
864 }
865
866 #[test]
871 fn to_kv6_matches_bbox_and_colours() {
872 const DIM_STONE: VoxColor = VoxColor(0x40B0_8040); let mut g = grid();
874 pillar(&mut g, 2, 2, 100);
875 for x in [3, 4, 6] {
878 g.set_voxel(IVec3::new(x, 2, 100), Some(STONE));
879 }
880 g.set_voxel(IVec3::new(5, 2, 100), Some(DIM_STONE));
881 g.set_voxel(IVec3::new(3, 2, 100), None);
882 let islands = detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096);
883 let isl = &islands[0];
884 assert!(
885 isl.voxels
886 .iter()
887 .any(|&(v, c)| v == IVec3::new(5, 2, 100) && c == DIM_STONE),
888 "the island records the raw (dim) grid colour"
889 );
890 let kv6 = isl.to_kv6();
891 assert_eq!((kv6.xsiz, kv6.ysiz, kv6.zsiz), (3, 1, 1));
892 assert_eq!(kv6.voxels.len(), 3, "thin beam: every voxel is surface");
893 assert!(
894 kv6.voxels.iter().all(|v| v.col == STONE.0),
895 "the model normalises every brightness byte to 0x80"
896 );
897 }
898
899 #[test]
902 fn extract_empty_island_is_noop() {
903 let mut g = grid();
904 pillar(&mut g, 2, 2, 100);
905 let v = g.chunk_version(IVec3::ZERO);
906 Island {
907 voxels: Vec::new(),
908 bbox: (IVec3::MAX, IVec3::MIN),
909 }
910 .extract(&mut g, BakeMode::Directional);
911 assert_eq!(
912 g.chunk_version(IVec3::ZERO),
913 v,
914 "no-op leaves versions alone"
915 );
916 }
917
918 #[test]
922 fn world_anchors_honour_scale() {
923 let isl = Island {
924 voxels: Vec::new(),
925 bbox: (IVec3::new(4, 2, 100), IVec3::new(6, 2, 100)),
926 };
927 let t = GridTransform::at_scale(DVec3::new(10.0, 20.0, 30.0), 2.0);
928 assert_eq!(isl.world_origin(&t), DVec3::new(18.0, 24.0, 230.0));
929 assert_eq!(isl.world_pivot(&t), DVec3::new(21.0, 25.0, 231.0));
930 }
931
932 fn box_island(dims: IVec3) -> Island {
936 let mut voxels = Vec::new();
937 for z in 0..dims.z {
938 for y in 0..dims.y {
939 for x in 0..dims.x {
940 voxels.push((IVec3::new(x, y, z), STONE));
941 }
942 }
943 }
944 Island {
945 voxels,
946 bbox: (IVec3::ZERO, dims - IVec3::ONE),
947 }
948 }
949
950 fn assert_disjoint_cover(original: &Island, frags: &[Island]) {
953 let mut seen = std::collections::HashSet::new();
954 for f in frags {
955 let (mut lo, mut hi) = (IVec3::MAX, IVec3::MIN);
956 for &(v, _) in &f.voxels {
957 assert!(seen.insert(v), "voxel {v} appears in two fragments");
958 lo = lo.min(v);
959 hi = hi.max(v);
960 }
961 assert_eq!(f.bbox, (lo, hi), "fragment bbox recomputed");
962 }
963 assert_eq!(
964 seen.len(),
965 original.voxels.len(),
966 "fragments cover every original voxel"
967 );
968 for &(v, _) in &original.voxels {
969 assert!(seen.contains(&v));
970 }
971 }
972
973 #[test]
977 fn chunks_split_is_disjoint_cover_and_deterministic() {
978 let isl = box_island(IVec3::new(12, 12, 6));
979 let frags = isl.split(FracturePattern::Chunks { cell: 4 }, 7);
980 assert!(frags.len() > 3, "a 12×12×6 box breaks into several cells");
981 assert_disjoint_cover(&isl, &frags);
982
983 let again = isl.split(FracturePattern::Chunks { cell: 4 }, 7);
984 assert_eq!(frags.len(), again.len());
985 for (a, b) in frags.iter().zip(&again) {
986 assert_eq!(a.voxels, b.voxels, "same seed ⇒ bit-identical split");
987 }
988 }
989
990 #[test]
994 fn shards_split_into_planar_plates() {
995 let isl = box_island(IVec3::new(12, 12, 12));
996 let frags = isl.split(FracturePattern::Shards { plates: 3 }, 11);
997 assert_eq!(frags.len(), 3, "three plates from a solid cube");
998 assert_disjoint_cover(&isl, &frags);
999 let total = isl.voxels.len() as f64;
1000 for f in &frags {
1001 let share = f.voxels.len() as f64 / total;
1002 assert!(
1003 (0.15..=0.55).contains(&share),
1004 "roughly equal plates (share {share:.2})"
1005 );
1006 let mut best = f64::MAX;
1010 let mut rng = SplitMix(99);
1011 for _ in 0..256 {
1012 let z = 2.0 * rng.unit() - 1.0;
1013 let phi = std::f64::consts::TAU * rng.unit();
1014 let r = (1.0 - z * z).max(0.0).sqrt();
1015 let d = glam::DVec3::new(r * phi.cos(), r * phi.sin(), z);
1016 let (mut pmin, mut pmax) = (f64::MAX, f64::MIN);
1017 for &(v, _) in &f.voxels {
1018 let p = v.as_dvec3().dot(d);
1019 pmin = pmin.min(p);
1020 pmax = pmax.max(p);
1021 }
1022 best = best.min(pmax - pmin);
1023 }
1024 assert!(
1025 best < 7.0,
1026 "each plate is thin along the slicing normal (got {best:.2})"
1027 );
1028 }
1029 }
1030
1031 #[test]
1036 fn chunks_split_scales_with_island_not_bbox() {
1037 let n = 200;
1038 let voxels: Vec<(IVec3, VoxColor)> = (0..n)
1039 .map(|i| (IVec3::new(i, i, i.rem_euclid(256)), STONE))
1040 .collect();
1041 let isl = Island {
1042 bbox: (IVec3::ZERO, IVec3::new(n - 1, n - 1, 199)),
1043 voxels,
1044 };
1045 let frags = isl.split(FracturePattern::Chunks { cell: 2 }, 3);
1046 assert!(frags.len() > 10, "a diagonal snake breaks into many bits");
1047 assert_disjoint_cover(&isl, &frags);
1048 }
1049
1050 #[test]
1052 fn whole_and_degenerate_splits_pass_through() {
1053 let isl = box_island(IVec3::new(3, 2, 1));
1054 let whole = isl.split(FracturePattern::Whole, 1);
1055 assert_eq!(whole.len(), 1);
1056 assert_eq!(whole[0].voxels, isl.voxels);
1057 let coarse = isl.split(FracturePattern::Chunks { cell: 64 }, 1);
1059 assert_eq!(coarse.len(), 1);
1060 assert_disjoint_cover(&isl, &coarse);
1061 }
1062
1063 #[test]
1067 fn matches_dense_oracle() {
1068 let mut s: u64 = 0x9E37_79B9_7F4A_7C15;
1070 let mut rnd = move |m: i32| {
1071 s ^= s << 13;
1072 s ^= s >> 7;
1073 s ^= s << 17;
1074 #[allow(clippy::cast_possible_truncation)]
1075 ((s >> 33) as i32).rem_euclid(m)
1076 };
1077
1078 let (nx, ny) = (24, 24);
1079 let mut g = grid();
1080 for _ in 0..4 {
1082 pillar(&mut g, rnd(nx), rnd(ny), 200);
1083 }
1084 for _ in 0..40 {
1088 let p = IVec3::new(rnd(nx), rnd(ny), 190 + rnd(30));
1089 let q = (p + IVec3::new(rnd(3), rnd(3), rnd(3))).min(IVec3::new(nx - 1, ny - 1, 255));
1090 g.set_rect(p, q, Some(STONE));
1091 }
1092 g.set_sphere(IVec3::new(nx / 2, ny / 2, 205), 6, None);
1094
1095 let solid = |v: IVec3| v.x >= 0 && v.x < nx && v.y >= 0 && v.y < ny && g.voxel_solid(v);
1098 let mut reached = std::collections::HashSet::new();
1099 let mut q = VecDeque::new();
1100 for x in 0..nx {
1101 for y in 0..ny {
1102 let v = IVec3::new(x, y, 255);
1103 assert!(g.voxel_solid(v), "bedrock plane is always solid");
1104 reached.insert(v);
1105 q.push_back(v);
1106 }
1107 }
1108 while let Some(v) = q.pop_front() {
1109 for d in [
1110 IVec3::X,
1111 IVec3::NEG_X,
1112 IVec3::Y,
1113 IVec3::NEG_Y,
1114 IVec3::Z,
1115 IVec3::NEG_Z,
1116 ] {
1117 let n = v + d;
1118 if n.z >= 0 && n.z < 256 && solid(n) && reached.insert(n) {
1119 q.push_back(n);
1120 }
1121 }
1122 }
1123 let mut oracle: Vec<IVec3> = Vec::new();
1124 for x in 0..nx {
1125 for y in 0..ny {
1126 for z in 0..256 {
1127 let v = IVec3::new(x, y, z);
1128 if solid(v) && !reached.contains(&v) {
1129 oracle.push(v);
1130 }
1131 }
1132 }
1133 }
1134 oracle.sort_by_key(|p| (p.z, p.y, p.x));
1135
1136 let islands = detect_islands(
1138 &g,
1139 IVec3::new(0, 0, 0),
1140 IVec3::new(nx - 1, ny - 1, 255),
1141 1_000_000,
1142 );
1143 let mut got: Vec<IVec3> = islands
1144 .iter()
1145 .flat_map(|i| i.voxels.iter().map(|&(p, _)| p))
1146 .collect();
1147 got.sort_by_key(|p| (p.z, p.y, p.x));
1148 assert_eq!(got, oracle, "span-BFS must agree with the dense oracle");
1149 }
1150}