use std::collections::{HashMap, VecDeque};
use glam::{DVec3, IVec3, Vec3};
use roxlap_formats::color::VoxColor;
use roxlap_formats::kv6::Kv6;
use roxlap_formats::vxl::Vxl;
use crate::{grid_local_to_world, BakeMode, Grid, GridTransform, CHUNK_SIZE_XY, CHUNK_SIZE_Z};
pub const DEFAULT_ISLAND_BUDGET: usize = 4096;
#[derive(Debug, Clone)]
pub struct Island {
pub voxels: Vec<(IVec3, VoxColor)>,
pub bbox: (IVec3, IVec3),
}
impl Island {
pub fn extract(&self, grid: &mut Grid, bake: BakeMode) {
if self.voxels.is_empty() {
return;
}
let mut pending: Option<(IVec3, IVec3)> = None;
for &(v, _) in &self.voxels {
if let Some((lo, hi)) = pending {
if v.x == hi.x && v.y == hi.y && v.z == hi.z + 1 {
pending = Some((lo, v));
continue;
}
grid.set_rect(lo, hi, None);
}
pending = Some((v, v));
}
if let Some((lo, hi)) = pending {
grid.set_rect(lo, hi, None);
}
let (lo, hi) = self.bbox;
grid.bake_bbox(lo, hi, bake);
}
#[must_use]
#[allow(clippy::cast_possible_wrap)]
pub fn to_kv6(&self) -> Kv6 {
let (lo, hi) = self.bbox;
let dims = (hi - lo + IVec3::ONE).as_uvec3();
let map: HashMap<IVec3, u32> = self
.voxels
.iter()
.map(|&(v, c)| (v - lo, (c.0 & 0x00ff_ffff) | 0x8000_0000))
.collect();
Kv6::from_fn(dims.x, dims.y, dims.z, |x, y, z| {
map.get(&IVec3::new(x as i32, y as i32, z as i32))
.map(|&c| VoxColor(c))
})
}
#[must_use]
pub fn world_origin(&self, transform: &GridTransform) -> DVec3 {
let (chunk, in_chunk) = crate::voxel_split(self.bbox.0);
grid_local_to_world(chunk, in_chunk, Vec3::ZERO, transform)
}
#[must_use]
pub fn world_pivot(&self, transform: &GridTransform) -> DVec3 {
let (chunk, in_chunk) = crate::voxel_split(self.bbox.0);
let dims = (self.bbox.1 - self.bbox.0 + IVec3::ONE).as_vec3();
grid_local_to_world(chunk, in_chunk, dims * 0.5, transform)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FracturePattern {
Whole,
Chunks {
cell: u32,
},
Shards {
plates: u32,
},
}
struct SplitMix(u64);
impl SplitMix {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[allow(clippy::cast_precision_loss)]
fn unit(&mut self) -> f64 {
(self.next() >> 11) as f64 / (1u64 << 53) as f64
}
#[allow(clippy::cast_possible_truncation)]
fn below(&mut self, n: u32) -> i32 {
(self.next() % u64::from(n.max(1))) as i32
}
}
impl Island {
#[must_use]
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
pub fn split(&self, pattern: FracturePattern, seed: u64) -> Vec<Island> {
let assign: Vec<usize> = match pattern {
FracturePattern::Whole => return vec![self.clone()],
FracturePattern::Chunks { cell } => {
let cell = i32::try_from(cell.max(2)).unwrap_or(i32::MAX);
let mut rng = SplitMix(seed ^ 0xC0C0);
let mut cell_site: HashMap<IVec3, usize> = HashMap::new();
let mut sites: Vec<IVec3> = Vec::new();
for &(v, _) in &self.voxels {
let cc = IVec3::new(
v.x.div_euclid(cell),
v.y.div_euclid(cell),
v.z.div_euclid(cell),
);
if let std::collections::hash_map::Entry::Vacant(e) = cell_site.entry(cc) {
let jitter = IVec3::new(
rng.below(cell as u32),
rng.below(cell as u32),
rng.below(cell as u32),
);
e.insert(sites.len());
sites.push(cc * cell + jitter);
}
}
self.voxels
.iter()
.map(|&(v, _)| {
let mut best = 0usize;
let mut best_d = i64::MAX;
for (i, s) in sites.iter().enumerate() {
let d = (v - *s).as_i64vec3().length_squared();
if d < best_d {
best_d = d;
best = i;
}
}
best
})
.collect()
}
FracturePattern::Shards { plates } => {
let k = plates.clamp(2, 8);
let mut rng = SplitMix(seed ^ 0x5AAD);
let z = 2.0 * rng.unit() - 1.0;
let phi = std::f64::consts::TAU * rng.unit();
let r = (1.0 - z * z).max(0.0).sqrt();
let n = glam::DVec3::new(r * phi.cos(), r * phi.sin(), z);
let projs: Vec<f64> = self
.voxels
.iter()
.map(|&(v, _)| v.as_dvec3().dot(n))
.collect();
let (mut pmin, mut pmax) = (f64::MAX, f64::MIN);
for &p in &projs {
pmin = pmin.min(p);
pmax = pmax.max(p);
}
let range = (pmax - pmin).max(1e-9);
let step = range / f64::from(k);
let bounds: Vec<f64> = (1..k)
.map(|i| pmin + step * (f64::from(i) + 0.4 * (rng.unit() - 0.5)))
.collect();
projs
.iter()
.map(|&p| bounds.iter().filter(|&&b| p >= b).count())
.collect()
}
};
let mut order: Vec<usize> = Vec::new();
let mut groups: HashMap<usize, Vec<(IVec3, VoxColor)>> = HashMap::new();
for (i, &(v, c)) in self.voxels.iter().enumerate() {
let g = assign[i];
groups.entry(g).or_insert_with(|| {
order.push(g);
Vec::new()
});
groups.get_mut(&g).expect("just inserted").push((v, c));
}
order
.into_iter()
.map(|g| {
let voxels = groups.remove(&g).expect("grouped above");
let mut lo = IVec3::MAX;
let mut hi = IVec3::MIN;
for &(v, _) in &voxels {
lo = lo.min(v);
hi = hi.max(v);
}
Island {
voxels,
bbox: (lo, hi),
}
})
.collect()
}
}
#[derive(Clone, Copy)]
struct Run {
top: i32,
bot: i32,
anchored: bool,
}
fn chunk_column_runs(vxl: &Vxl, x: u32, y: u32, mut f: impl FnMut(i32, i32, bool)) {
let idx = (y * vxl.vsid + x) as usize;
let slab = vxl.column_data(idx);
let mut top = i32::from(slab[1]);
let mut v = 0usize;
while slab[v] != 0 {
v += usize::from(slab[v]) * 4;
if slab[v + 3] >= slab[v + 1] {
continue;
}
let bot = i32::from(slab[v + 3]);
f(top, bot, false);
top = i32::from(slab[v + 1]);
}
#[allow(clippy::cast_possible_wrap)]
f(top, CHUNK_SIZE_Z as i32, true);
}
#[derive(Default)]
struct ColHash(u64);
impl std::hash::Hasher for ColHash {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3);
}
}
#[allow(clippy::cast_sign_loss)]
fn write_i32(&mut self, i: i32) {
self.0 = (self.0 ^ u64::from(i as u32)).wrapping_mul(0xf135_7aea_2e62_a9c5);
self.0 = self.0.rotate_left(26);
}
}
type ColMap<V> = HashMap<(i32, i32), V, std::hash::BuildHasherDefault<ColHash>>;
struct Flood<'a> {
grid: &'a Grid,
stacks: ColMap<Vec<i32>>,
columns: ColMap<(u32, u32)>,
runs: Vec<Run>,
visited: Vec<u32>,
last_chunk: Option<(IVec3, Option<&'a Vxl>)>,
}
impl<'a> Flood<'a> {
fn new(grid: &'a Grid) -> Self {
let mut stacks: ColMap<Vec<i32>> = ColMap::default();
for k in grid.chunks.keys() {
stacks.entry((k.x, k.y)).or_default().push(k.z);
}
for v in stacks.values_mut() {
v.sort_unstable();
}
Self {
grid,
stacks,
columns: ColMap::default(),
runs: Vec::new(),
visited: Vec::new(),
last_chunk: None,
}
}
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
fn column_range(&mut self, x: i32, y: i32) -> (u32, u32) {
if let Some(&r) = self.columns.get(&(x, y)) {
return r;
}
let Self {
grid,
stacks,
columns,
runs,
visited,
last_chunk,
} = self;
let start = runs.len();
let chx = x.div_euclid(CHUNK_SIZE_XY as i32);
let chy = y.div_euclid(CHUNK_SIZE_XY as i32);
let lx = x.rem_euclid(CHUNK_SIZE_XY as i32) as u32;
let ly = y.rem_euclid(CHUNK_SIZE_XY as i32) as u32;
let stack: &[i32] = stacks.get(&(chx, chy)).map_or(&[], Vec::as_slice);
for &chz in stack {
let idx = IVec3::new(chx, chy, chz);
let vxl = match *last_chunk {
Some((c, v)) if c == idx => v,
_ => {
let v = grid.chunk(idx);
*last_chunk = Some((idx, v));
v
}
};
let Some(vxl) = vxl else { continue };
let base = chz * CHUNK_SIZE_Z as i32;
chunk_column_runs(vxl, lx, ly, |top, bot, is_final| {
let (top_g, bot_g) = (base + top, base + bot);
let anchored = is_final;
if runs.len() > start {
if let Some(last) = runs.last_mut() {
if last.bot == top_g {
last.bot = bot_g;
last.anchored |= anchored;
return;
}
}
}
runs.push(Run {
top: top_g,
bot: bot_g,
anchored,
});
});
}
visited.resize(runs.len(), 0);
let r = (start as u32, (runs.len() - start) as u32);
columns.insert((x, y), r);
r
}
}
#[must_use]
pub fn detect_islands(grid: &Grid, carve_lo: IVec3, carve_hi: IVec3, budget: usize) -> Vec<Island> {
let lo = carve_lo.min(carve_hi) - IVec3::ONE;
let hi = carve_lo.max(carve_hi) + IVec3::ONE;
let mut fl = Flood::new(grid);
let mut islands = Vec::new();
let mut traversal: u32 = 0;
for y in lo.y..=hi.y {
for x in lo.x..=hi.x {
let (start, len) = fl.column_range(x, y);
for i in start..start + len {
let seed = fl.runs[i as usize];
if seed.bot <= lo.z || seed.top > hi.z {
continue;
}
if fl.visited[i as usize] != 0 {
continue;
}
traversal += 1;
fl.visited[i as usize] = traversal;
if let Some(comp) = flood_component(&mut fl, (x, y), seed, budget, traversal) {
islands.push(build_island(grid, &comp));
}
}
}
}
islands
}
fn flood_component(
fl: &mut Flood<'_>,
seed_col: (i32, i32),
seed: Run,
budget: usize,
traversal: u32,
) -> Option<Vec<(i32, i32, Run)>> {
let mut queue: VecDeque<(i32, i32, Run)> = VecDeque::new();
queue.push_back((seed_col.0, seed_col.1, seed));
let mut comp: Vec<(i32, i32, Run)> = Vec::new();
let mut count = 0usize;
while let Some((cx, cy, r)) = queue.pop_front() {
if r.anchored {
return None; }
count += usize::try_from(r.bot - r.top).unwrap_or(usize::MAX);
if count > budget {
return None; }
comp.push((cx, cy, r));
for (nx, ny) in [(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)] {
let (start, len) = fl.column_range(nx, ny);
for i in (start as usize)..(start + len) as usize {
let nr = fl.runs[i];
if nr.top >= r.bot || nr.bot <= r.top {
continue; }
let v = fl.visited[i];
if v == traversal {
continue;
}
if v != 0 {
return None;
}
fl.visited[i] = traversal;
queue.push_back((nx, ny, nr));
}
}
}
Some(comp)
}
fn build_island(grid: &Grid, comp: &[(i32, i32, Run)]) -> Island {
let mut voxels = Vec::new();
let mut lo = IVec3::MAX;
let mut hi = IVec3::MIN;
for &(x, y, r) in comp {
let mut last = VoxColor(0x8080_8080);
for z in r.top..r.bot {
let v = IVec3::new(x, y, z);
let c = grid.voxel_color(v).unwrap_or(last);
last = c;
voxels.push((v, c));
lo = lo.min(v);
hi = hi.max(v);
}
}
Island {
voxels,
bbox: (lo, hi),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GridTransform;
use roxlap_formats::color::VoxColor;
const STONE: VoxColor = VoxColor(0x80B0_8040);
fn grid() -> Grid {
Grid::new(GridTransform::identity())
}
fn pillar(g: &mut Grid, x: i32, y: i32, z0: i32) {
g.set_rect(IVec3::new(x, y, z0), IVec3::new(x, y, 255), Some(STONE));
}
fn island_positions(i: &Island) -> Vec<IVec3> {
let mut v: Vec<IVec3> = i.voxels.iter().map(|&(p, _)| p).collect();
v.sort_by_key(|p| (p.z, p.y, p.x));
v
}
#[test]
fn beam_cut_detaches_tip() {
let mut g = grid();
pillar(&mut g, 2, 2, 100);
g.set_rect(IVec3::new(3, 2, 100), IVec3::new(6, 2, 100), Some(STONE));
assert!(detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096).is_empty());
g.set_voxel(IVec3::new(3, 2, 100), None);
let islands = detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096);
assert_eq!(islands.len(), 1);
let isl = &islands[0];
assert_eq!(
island_positions(isl),
vec![
IVec3::new(4, 2, 100),
IVec3::new(5, 2, 100),
IVec3::new(6, 2, 100)
]
);
assert!(isl.voxels.iter().all(|&(_, c)| c == STONE));
assert_eq!(isl.bbox, (IVec3::new(4, 2, 100), IVec3::new(6, 2, 100)));
}
#[test]
fn arch_needs_both_legs_cut() {
let mut g = grid();
pillar(&mut g, 2, 5, 101);
pillar(&mut g, 8, 5, 101);
g.set_rect(IVec3::new(2, 5, 100), IVec3::new(8, 5, 100), Some(STONE));
g.set_voxel(IVec3::new(2, 5, 101), None);
assert!(
detect_islands(&g, IVec3::new(2, 5, 101), IVec3::new(2, 5, 101), 4096).is_empty(),
"beam still hangs off the right leg"
);
g.set_voxel(IVec3::new(8, 5, 101), None);
let islands = detect_islands(&g, IVec3::new(8, 5, 101), IVec3::new(8, 5, 101), 4096);
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].voxels.len(), 7, "beam x ∈ [2, 8] at z=100");
}
#[test]
fn island_crosses_chunk_border() {
let mut g = grid();
pillar(&mut g, 133, 5, 101);
g.set_rect(
IVec3::new(124, 5, 100),
IVec3::new(133, 5, 100),
Some(STONE),
);
g.set_voxel(IVec3::new(132, 5, 100), None);
let islands = detect_islands(&g, IVec3::new(132, 5, 100), IVec3::new(132, 5, 100), 4096);
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].voxels.len(), 8, "x ∈ [124, 131] at z=100");
let (lo, hi) = islands[0].bbox;
assert!(lo.x < 128 && hi.x >= 128, "bbox spans the border");
}
#[test]
fn budget_exceeded_stays_supported() {
let mut g = grid();
pillar(&mut g, 10, 10, 101);
g.set_rect(IVec3::new(1, 1, 100), IVec3::new(20, 20, 100), Some(STONE));
g.set_voxel(IVec3::new(10, 10, 101), None);
let cut = (IVec3::new(10, 10, 101), IVec3::new(10, 10, 101));
assert!(
detect_islands(&g, cut.0, cut.1, 100).is_empty(),
"400-voxel plate exceeds a 100-voxel budget"
);
let islands = detect_islands(&g, cut.0, cut.1, 10_000);
assert_eq!(islands.len(), 1);
assert_eq!(islands[0].voxels.len(), 400);
}
#[test]
fn carve_in_air_finds_nothing() {
let mut g = grid();
pillar(&mut g, 2, 2, 100);
assert!(
detect_islands(&g, IVec3::new(50, 50, 50), IVec3::new(52, 52, 52), 4096).is_empty()
);
}
#[test]
fn two_islands_from_one_carve() {
let mut g = grid();
pillar(&mut g, 5, 5, 101);
g.set_voxel(IVec3::new(5, 5, 100), Some(STONE));
g.set_rect(IVec3::new(6, 5, 100), IVec3::new(8, 5, 100), Some(STONE));
g.set_rect(IVec3::new(2, 5, 100), IVec3::new(4, 5, 100), Some(STONE));
assert!(detect_islands(&g, IVec3::new(5, 5, 100), IVec3::new(5, 5, 100), 4096).is_empty());
g.set_voxel(IVec3::new(5, 5, 100), None);
let islands = detect_islands(&g, IVec3::new(5, 5, 100), IVec3::new(5, 5, 100), 4096);
assert_eq!(islands.len(), 2);
let mut sizes: Vec<usize> = islands.iter().map(|i| i.voxels.len()).collect();
sizes.sort_unstable();
assert_eq!(sizes, vec![3, 3]);
}
#[test]
fn stacked_chz_bedrock_is_anchored() {
let mut g = grid();
g.set_rect(IVec3::new(5, 5, 200), IVec3::new(5, 5, 400), Some(STONE));
g.set_voxel(IVec3::new(5, 5, 300), None);
let islands = detect_islands(&g, IVec3::new(5, 5, 300), IVec3::new(5, 5, 300), 100_000);
assert_eq!(islands.len(), 1, "only the under-cut segment falls");
assert_eq!(islands[0].voxels.len(), 100, "z ∈ [301, 400]");
assert_eq!(
islands[0].bbox,
(IVec3::new(5, 5, 301), IVec3::new(5, 5, 400))
);
}
#[test]
#[ignore = "manual perf probe — cargo test -p roxlap-scene --lib islands -- --ignored --nocapture"]
fn perf_probe() {
let mut g = grid();
for (x, y) in [(1, 1), (98, 1), (1, 98), (98, 98)] {
pillar(&mut g, x, y, 101);
}
g.set_rect(IVec3::new(0, 0, 100), IVec3::new(99, 99, 100), Some(STONE));
g.set_sphere(IVec3::new(50, 50, 100), 4, None);
let t = std::time::Instant::now();
let n = detect_islands(
&g,
IVec3::new(46, 46, 96),
IVec3::new(54, 54, 104),
DEFAULT_ISLAND_BUDGET,
)
.len();
let supported_exit = t.elapsed();
assert_eq!(n, 0);
let mut g = grid();
pillar(&mut g, 2, 2, 100);
g.set_rect(IVec3::new(3, 2, 100), IVec3::new(66, 2, 100), Some(STONE));
g.set_voxel(IVec3::new(3, 2, 100), None);
let t = std::time::Instant::now();
let islands = detect_islands(
&g,
IVec3::new(3, 2, 100),
IVec3::new(3, 2, 100),
DEFAULT_ISLAND_BUDGET,
);
let detach = t.elapsed();
assert_eq!(islands[0].voxels.len(), 63);
eprintln!("supported-exit (budget {DEFAULT_ISLAND_BUDGET}): {supported_exit:?}");
eprintln!("63-voxel beam detach: {detach:?}");
}
#[test]
fn extract_leaves_air_and_detection_clean() {
let mut g = grid();
pillar(&mut g, 2, 2, 100);
g.set_rect(IVec3::new(3, 2, 100), IVec3::new(6, 2, 100), Some(STONE));
g.set_voxel(IVec3::new(3, 2, 100), None);
let cut = (IVec3::new(3, 2, 100), IVec3::new(3, 2, 100));
let islands = detect_islands(&g, cut.0, cut.1, 4096);
let isl = islands[0].clone();
let v_before = g.chunk_version(IVec3::ZERO);
isl.extract(&mut g, BakeMode::Directional);
for &(v, _) in &isl.voxels {
assert!(!g.voxel_solid(v), "extracted voxel {v} must be air");
}
assert!(
g.voxel_solid(IVec3::new(2, 2, 100)),
"the supported pillar stays"
);
assert!(
detect_islands(&g, cut.0, cut.1, 4096).is_empty(),
"nothing left to detach"
);
assert!(
g.chunk_version(IVec3::ZERO) > v_before,
"renderers must see the extraction"
);
}
#[test]
fn extract_vertical_run_across_chz() {
let mut g = grid();
g.set_rect(IVec3::new(5, 5, 200), IVec3::new(5, 5, 400), Some(STONE));
g.set_voxel(IVec3::new(5, 5, 300), None);
let islands = detect_islands(&g, IVec3::new(5, 5, 300), IVec3::new(5, 5, 300), 100_000);
islands[0].extract(&mut g, BakeMode::Directional);
for z in 301..=400 {
assert!(!g.voxel_solid(IVec3::new(5, 5, z)), "z={z} must be air");
}
assert!(
g.voxel_solid(IVec3::new(5, 5, 299)),
"the pinned upper segment stays"
);
}
#[test]
fn to_kv6_matches_bbox_and_colours() {
const DIM_STONE: VoxColor = VoxColor(0x40B0_8040); let mut g = grid();
pillar(&mut g, 2, 2, 100);
for x in [3, 4, 6] {
g.set_voxel(IVec3::new(x, 2, 100), Some(STONE));
}
g.set_voxel(IVec3::new(5, 2, 100), Some(DIM_STONE));
g.set_voxel(IVec3::new(3, 2, 100), None);
let islands = detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096);
let isl = &islands[0];
assert!(
isl.voxels
.iter()
.any(|&(v, c)| v == IVec3::new(5, 2, 100) && c == DIM_STONE),
"the island records the raw (dim) grid colour"
);
let kv6 = isl.to_kv6();
assert_eq!((kv6.xsiz, kv6.ysiz, kv6.zsiz), (3, 1, 1));
assert_eq!(kv6.voxels.len(), 3, "thin beam: every voxel is surface");
assert!(
kv6.voxels.iter().all(|v| v.col == STONE.0),
"the model normalises every brightness byte to 0x80"
);
}
#[test]
fn extract_empty_island_is_noop() {
let mut g = grid();
pillar(&mut g, 2, 2, 100);
let v = g.chunk_version(IVec3::ZERO);
Island {
voxels: Vec::new(),
bbox: (IVec3::MAX, IVec3::MIN),
}
.extract(&mut g, BakeMode::Directional);
assert_eq!(
g.chunk_version(IVec3::ZERO),
v,
"no-op leaves versions alone"
);
}
#[test]
fn world_anchors_honour_scale() {
let isl = Island {
voxels: Vec::new(),
bbox: (IVec3::new(4, 2, 100), IVec3::new(6, 2, 100)),
};
let t = GridTransform::at_scale(DVec3::new(10.0, 20.0, 30.0), 2.0);
assert_eq!(isl.world_origin(&t), DVec3::new(18.0, 24.0, 230.0));
assert_eq!(isl.world_pivot(&t), DVec3::new(21.0, 25.0, 231.0));
}
fn box_island(dims: IVec3) -> Island {
let mut voxels = Vec::new();
for z in 0..dims.z {
for y in 0..dims.y {
for x in 0..dims.x {
voxels.push((IVec3::new(x, y, z), STONE));
}
}
}
Island {
voxels,
bbox: (IVec3::ZERO, dims - IVec3::ONE),
}
}
fn assert_disjoint_cover(original: &Island, frags: &[Island]) {
let mut seen = std::collections::HashSet::new();
for f in frags {
let (mut lo, mut hi) = (IVec3::MAX, IVec3::MIN);
for &(v, _) in &f.voxels {
assert!(seen.insert(v), "voxel {v} appears in two fragments");
lo = lo.min(v);
hi = hi.max(v);
}
assert_eq!(f.bbox, (lo, hi), "fragment bbox recomputed");
}
assert_eq!(
seen.len(),
original.voxels.len(),
"fragments cover every original voxel"
);
for &(v, _) in &original.voxels {
assert!(seen.contains(&v));
}
}
#[test]
fn chunks_split_is_disjoint_cover_and_deterministic() {
let isl = box_island(IVec3::new(12, 12, 6));
let frags = isl.split(FracturePattern::Chunks { cell: 4 }, 7);
assert!(frags.len() > 3, "a 12×12×6 box breaks into several cells");
assert_disjoint_cover(&isl, &frags);
let again = isl.split(FracturePattern::Chunks { cell: 4 }, 7);
assert_eq!(frags.len(), again.len());
for (a, b) in frags.iter().zip(&again) {
assert_eq!(a.voxels, b.voxels, "same seed ⇒ bit-identical split");
}
}
#[test]
fn shards_split_into_planar_plates() {
let isl = box_island(IVec3::new(12, 12, 12));
let frags = isl.split(FracturePattern::Shards { plates: 3 }, 11);
assert_eq!(frags.len(), 3, "three plates from a solid cube");
assert_disjoint_cover(&isl, &frags);
let total = isl.voxels.len() as f64;
for f in &frags {
let share = f.voxels.len() as f64 / total;
assert!(
(0.15..=0.55).contains(&share),
"roughly equal plates (share {share:.2})"
);
let mut best = f64::MAX;
let mut rng = SplitMix(99);
for _ in 0..256 {
let z = 2.0 * rng.unit() - 1.0;
let phi = std::f64::consts::TAU * rng.unit();
let r = (1.0 - z * z).max(0.0).sqrt();
let d = glam::DVec3::new(r * phi.cos(), r * phi.sin(), z);
let (mut pmin, mut pmax) = (f64::MAX, f64::MIN);
for &(v, _) in &f.voxels {
let p = v.as_dvec3().dot(d);
pmin = pmin.min(p);
pmax = pmax.max(p);
}
best = best.min(pmax - pmin);
}
assert!(
best < 7.0,
"each plate is thin along the slicing normal (got {best:.2})"
);
}
}
#[test]
fn chunks_split_scales_with_island_not_bbox() {
let n = 200;
let voxels: Vec<(IVec3, VoxColor)> = (0..n)
.map(|i| (IVec3::new(i, i, i.rem_euclid(256)), STONE))
.collect();
let isl = Island {
bbox: (IVec3::ZERO, IVec3::new(n - 1, n - 1, 199)),
voxels,
};
let frags = isl.split(FracturePattern::Chunks { cell: 2 }, 3);
assert!(frags.len() > 10, "a diagonal snake breaks into many bits");
assert_disjoint_cover(&isl, &frags);
}
#[test]
fn whole_and_degenerate_splits_pass_through() {
let isl = box_island(IVec3::new(3, 2, 1));
let whole = isl.split(FracturePattern::Whole, 1);
assert_eq!(whole.len(), 1);
assert_eq!(whole[0].voxels, isl.voxels);
let coarse = isl.split(FracturePattern::Chunks { cell: 64 }, 1);
assert_eq!(coarse.len(), 1);
assert_disjoint_cover(&isl, &coarse);
}
#[test]
fn matches_dense_oracle() {
let mut s: u64 = 0x9E37_79B9_7F4A_7C15;
let mut rnd = move |m: i32| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
#[allow(clippy::cast_possible_truncation)]
((s >> 33) as i32).rem_euclid(m)
};
let (nx, ny) = (24, 24);
let mut g = grid();
for _ in 0..4 {
pillar(&mut g, rnd(nx), rnd(ny), 200);
}
for _ in 0..40 {
let p = IVec3::new(rnd(nx), rnd(ny), 190 + rnd(30));
let q = (p + IVec3::new(rnd(3), rnd(3), rnd(3))).min(IVec3::new(nx - 1, ny - 1, 255));
g.set_rect(p, q, Some(STONE));
}
g.set_sphere(IVec3::new(nx / 2, ny / 2, 205), 6, None);
let solid = |v: IVec3| v.x >= 0 && v.x < nx && v.y >= 0 && v.y < ny && g.voxel_solid(v);
let mut reached = std::collections::HashSet::new();
let mut q = VecDeque::new();
for x in 0..nx {
for y in 0..ny {
let v = IVec3::new(x, y, 255);
assert!(g.voxel_solid(v), "bedrock plane is always solid");
reached.insert(v);
q.push_back(v);
}
}
while let Some(v) = q.pop_front() {
for d in [
IVec3::X,
IVec3::NEG_X,
IVec3::Y,
IVec3::NEG_Y,
IVec3::Z,
IVec3::NEG_Z,
] {
let n = v + d;
if n.z >= 0 && n.z < 256 && solid(n) && reached.insert(n) {
q.push_back(n);
}
}
}
let mut oracle: Vec<IVec3> = Vec::new();
for x in 0..nx {
for y in 0..ny {
for z in 0..256 {
let v = IVec3::new(x, y, z);
if solid(v) && !reached.contains(&v) {
oracle.push(v);
}
}
}
}
oracle.sort_by_key(|p| (p.z, p.y, p.x));
let islands = detect_islands(
&g,
IVec3::new(0, 0, 0),
IVec3::new(nx - 1, ny - 1, 255),
1_000_000,
);
let mut got: Vec<IVec3> = islands
.iter()
.flat_map(|i| i.voxels.iter().map(|&(p, _)| p))
.collect();
got.sort_by_key(|p| (p.z, p.y, p.x));
assert_eq!(got, oracle, "span-BFS must agree with the dense oracle");
}
}