Skip to main content

roxlap_scene/
islands.rs

1//! DT.0 — floating-island detection, the destruction stage's core
2//! (see `docs/porting/PORTING-DESTRUCTION.md`).
3//!
4//! After a carve, voxel regions that no longer connect to support must
5//! crumble. [`detect_islands`] finds them with a budgeted breadth-first
6//! flood over the RLE **runs** of the affected columns — a run is one
7//! BFS node (chunk-format-native, no dense decode), 6-connected,
8//! seeded from every run touching the carved bbox (±1 pad). A
9//! component that reaches a **bedrock-anchored** run (the final run of
10//! any materialised chunk's column: the `.vxl` format extends it to
11//! the column bottom, and its bottom voxel at local z=255 is
12//! uncarvable — `delslab` clamps below `MAXZDIM` — so a component
13//! containing it can never physically fall, chz stacks included) or
14//! grows past `budget` voxels is supported and abandoned early; only
15//! components that exhaust under budget without finding support come
16//! back as [`Island`]s.
17//!
18//! Cost per call ≈ O(Σ min(component size, budget)) over the distinct
19//! components touching the bbox. False negatives are by design: a
20//! detached region larger than `budget` stays floating (voxlap behaved
21//! the same way) — raise the budget when a host wants bigger crumbles.
22
23use 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
32/// Default flood budget in voxels: a component that grows past this is
33/// declared supported (early exit). Tuned for carve-sized overhangs;
34/// hosts wanting building-sized crumbles pass a bigger budget.
35pub const DEFAULT_ISLAND_BUDGET: usize = 4096;
36
37/// One detached voxel region: everything a caller needs to extract it
38/// from the grid and hand it to a debris system.
39#[derive(Debug, Clone)]
40pub struct Island {
41    /// Every solid voxel of the component, grid-local coordinates plus
42    /// the colour it had at detection time (untextured interior cells
43    /// inherit the nearest colour above them in the column).
44    pub voxels: Vec<(IVec3, VoxColor)>,
45    /// Inclusive grid-local bounds of `voxels`.
46    pub bbox: (IVec3, IVec3),
47}
48
49impl Island {
50    /// DT.1 — carve this island's voxels out of `grid`, then re-bake
51    /// the touched region so lighting on the newly exposed faces is
52    /// correct: one [`Grid::set_rect`] carve per contiguous column
53    /// segment (the voxel list is stored run-major, so coalescing is a
54    /// single pass) and one [`Grid::bake_bbox`] over the island bbox
55    /// (its internal padding relights the surroundings; the edits and
56    /// the bake bump the touched chunk versions). After extraction, a
57    /// re-run of [`detect_islands`] over the same region finds
58    /// nothing.
59    ///
60    /// **Mip ladders are NOT regenerated** — the same contract as
61    /// every edit primitive ([`Grid::bake_bbox`] documents it too): a
62    /// caller rendering distant chunks must remip the touched columns
63    /// (`Vxl::remip_bbox`) exactly as it already does for its carves,
64    /// or mip-N keeps drawing the extracted island beyond
65    /// `mip_scan_dist` while its sprite twin falls next to it.
66    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    /// DT.1 — build the island's KV6 sprite model: dims are the
89    /// inclusive bbox extent, voxels sit at bbox-relative positions,
90    /// and colours are the detected ones with the high byte normalised
91    /// to `0x80` (full-bright — the sprite paths do their own
92    /// lighting, and a baked brightness byte would double-darken).
93    /// Surface-only ([`Kv6::from_fn`]): the interior is invisible in
94    /// flight, and a shatter samples [`Self::voxels`], not the model.
95    /// The model pivot is the bbox centre (`from_fn`'s convention) —
96    /// pose it with [`Self::world_pivot`].
97    #[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    /// DT.1 — the world-space position of the island's **minimum
114    /// corner** (`bbox.0`, voxel corner), through the grid transform
115    /// with `voxel_world_size` honoured (SC).
116    #[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    /// DT.1 — the world-space position of the island's **bbox centre**
123    /// — where [`Self::to_kv6`]'s pivot sits, i.e. the position a
124    /// debris system spawns the sprite instance at so the model lands
125    /// exactly over the voxels it replaced.
126    #[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/// DT.5 — how a detached region breaks apart before it falls, keyed
135/// by material in a debris system's side table (deliberately NOT a
136/// `Material` field — the render palette stays render-only).
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
138pub enum FracturePattern {
139    /// One piece — the default for unmapped materials.
140    Whole,
141    /// Rounded rubble (stone): jittered-Voronoi cells on a grid of
142    /// roughly `cell`-voxel spacing. `cell` is clamped to ≥ 2.
143    Chunks {
144        /// Approximate fragment edge length in voxels.
145        cell: u32,
146    },
147    /// Sharp plates (glass/crystal): the region sliced by 1–7
148    /// near-parallel planes of one random orientation into `plates`
149    /// slabs (clamped to `2..=8`), boundaries jittered.
150    Shards {
151        /// Number of plates to slice into.
152        plates: u32,
153    },
154}
155
156/// Tiny deterministic generator for the partitioners (SplitMix64) —
157/// keeps `split` reproducible from its explicit seed with no RNG
158/// state anywhere else.
159struct 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    /// Uniform in `[0, 1)`.
170    #[allow(clippy::cast_precision_loss)]
171    fn unit(&mut self) -> f64 {
172        (self.next() >> 11) as f64 / (1u64 << 53) as f64
173    }
174    /// Uniform integer in `[0, n)` (`n ≥ 1`).
175    #[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    /// DT.5 — partition this island into fragments per `pattern`: a
183    /// **disjoint cover** of the original voxels (colours ride along),
184    /// each fragment with a recomputed bbox, deterministic in
185    /// `(island, pattern, seed)`. [`FracturePattern::Whole`] returns
186    /// the island unsplit; degenerate cases (an island smaller than a
187    /// cell, all sites in one plate) collapse gracefully to fewer
188    /// fragments. A fragment may be internally disconnected (a
189    /// Voronoi cell straddling a concave gap) — it falls as one piece
190    /// (accepted v1 simplification).
191    #[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                // One jittered Voronoi site per **occupied** cell
200                // (keyed by the voxel's cell coordinate, first-seen
201                // order — deterministic): cost scales with the island,
202                // O(voxels × occupied cells), never with its bbox
203                // volume — a long sparse island (a shot-off diagonal
204                // vein) has a huge bbox but few occupied cells, and a
205                // bbox-grid of sites would OOM on it (maintainer
206                // review). Every voxel joins its nearest site (ties →
207                // the lower site index).
208                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                // One random unit normal (uniform on the sphere), then
246                // k slabs over the projection range with jittered
247                // boundaries — near-parallel plates.
248                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        // Group by fragment id, preserving first-seen order (stable,
274        // deterministic), dropping empty cells/plates.
275        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/// One solid z-run of a column in **grid-local** z (chz stacking
305/// already applied), `[top, bot)` half-open, z-down. `anchored` marks
306/// the bedrock run that counts as support.
307#[derive(Clone, Copy)]
308struct Run {
309    top: i32,
310    bot: i32,
311    anchored: bool,
312}
313
314/// Decode one chunk-local column's solid runs, mirroring the
315/// `vxl_voxel_solid` slab walk (`chunks.rs`): run k spans
316/// `[top_k, bot_k)`; the final run extends to the column bottom and is
317/// flagged `is_final` (the format's implicit bedrock).
318fn 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            // Degenerate slab (no air gap above): merges into the
327            // current run — same skip `expandrle` takes.
328            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/// Tiny multiply-mix hasher for the `(x, y)` column keys: the flood's
339/// hot loop is one map probe per neighbour column per node, and the
340/// default SipHash dominated it (interior coordinates are not
341/// DoS-facing, so the strong hash buys nothing here).
342#[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        // Rotate between the tuple's two lanes so (x, y) ≠ (y, x).
357        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
364/// Per-call flood state. Columns decode lazily into one shared
365/// **arena** (`runs` + the parallel `visited` marks) — a per-column
366/// `(start, len)` range in the map instead of per-column heap
367/// allocations, because the budget-exit worst case (a thin plate:
368/// thousands of one-voxel runs) is dominated by per-column setup, not
369/// by the BFS itself. A one-entry chunk cache covers the same reason:
370/// consecutive columns almost always live in the same chunk.
371struct Flood<'a> {
372    grid: &'a Grid,
373    /// Sorted chz list per (chx, chy) — which chunks stack under an XY
374    /// footprint (missing entries are implicit air).
375    stacks: ColMap<Vec<i32>>,
376    /// Column key → `(start, len)` range into `runs`/`visited`.
377    columns: ColMap<(u32, u32)>,
378    /// Run arena, grid-local z, in column-decode order.
379    runs: Vec<Run>,
380    /// Parallel to `runs`: id of the traversal that first reached the
381    /// run, `0` = unvisited.
382    visited: Vec<u32>,
383    /// One-entry `Grid::chunk` cache (the `chunks` HashMap probe is
384    /// SipHash — measurable at thousands of columns per call).
385    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    /// The arena range of grid-local column `(x, y)`, decoding it on
408    /// first touch: each present chunk in the chz stack contributes
409    /// its runs offset by `chz * CHUNK_SIZE_Z`; runs meeting exactly
410    /// at a chunk border merge; **every** chunk's final run is a
411    /// bedrock anchor — its local z=255 voxel is format-pinned
412    /// (uncarvable), so even on chz stacks a component containing it
413    /// cannot fall. (Gating the anchor on "no chunk below" was wrong:
414    /// it let the placeholder-bedrock sheet of an upper chunk flood
415    /// into components as 128×128 phantom voxels.) Split field
416    /// borrows keep the whole decode allocation-free (arena append).
417    #[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/// Detect voxel regions detached from support after an edit inside the
475/// grid-local bbox `[carve_lo, carve_hi]` (inclusive, any corner
476/// order). Seeds every solid run within the bbox padded by ±1 voxel —
477/// anything the carve could have disconnected touches that pad — and
478/// floods each unvisited component with 6-connectivity. Components
479/// that reach a bedrock-anchored run, exceed `budget` voxels, or merge
480/// into a previously-flooded (supported) region are dropped; the rest
481/// are returned as [`Island`]s, deterministic in both order and
482/// content for a given grid state.
483///
484/// Runs where the carve itself ran — call it **after** the edit.
485#[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                // Seed only runs intersecting the padded z window.
500                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
517/// Flood one component from `seed`; `Some(runs)` iff it is a genuine
518/// island (exhausted under budget, no bedrock anchor, no merge into an
519/// earlier — necessarily supported — traversal's territory).
520fn 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; // bedrock support
536        }
537        count += usize::try_from(r.bot - r.top).unwrap_or(usize::MAX);
538        if count > budget {
539            return None; // too big to crumble — assume supported
540        }
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; // no z overlap ⇒ not 6-adjacent
548                }
549                let v = fl.visited[i];
550                if v == traversal {
551                    continue;
552                }
553                if v != 0 {
554                    // A different traversal reached this run first.
555                    // Completed islands are closed components (nothing
556                    // outside them touches them), so this can only be
557                    // the territory of a supported-aborted flood ⇒ we
558                    // are the same component ⇒ supported.
559                    return None;
560                }
561                fl.visited[i] = traversal;
562                queue.push_back((nx, ny, nr));
563            }
564        }
565    }
566    Some(comp)
567}
568
569/// Materialise a detected component into an [`Island`]: enumerate its
570/// voxels with colours (untextured interior cells inherit the nearest
571/// colour above in the run — the `.vxl` format only stores surface
572/// colours) and fold the inclusive bbox.
573fn 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    /// Vertical pillar `[z0, 255]` at `(x, y)` — merged with the
607    /// bedrock run, i.e. genuinely supported.
608    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    /// Cutting a horizontal beam one voxel from its pillar detaches
619    /// exactly the tip — voxels, colours and bbox all exact.
620    #[test]
621    fn beam_cut_detaches_tip() {
622        let mut g = grid();
623        pillar(&mut g, 2, 2, 100);
624        // Beam sticking out along +x at z=100: x ∈ [3, 6].
625        g.set_rect(IVec3::new(3, 2, 100), IVec3::new(6, 2, 100), Some(STONE));
626
627        // No cut yet: everything hangs off the pillar.
628        assert!(detect_islands(&g, IVec3::new(3, 2, 100), IVec3::new(3, 2, 100), 4096).is_empty());
629
630        // Cut at x=3: the tip x ∈ [4, 6] is an island.
631        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    /// An arch: cutting one leg leaves the beam supported by the
648    /// other; cutting both detaches it.
649    #[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        // Sever the left leg below the beam.
657        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        // Sever the right leg too: the whole beam comes down.
664        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    /// An island whose body crosses the x = 127/128 chunk border is
671    /// found whole — the flood walks the chunk HashMap, not one Vxl.
672    #[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    /// A detached plate bigger than the budget is treated as supported
691    /// (stays floating, by design); a raised budget finds it.
692    #[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    /// Carving where nothing was solid detects nothing.
710    #[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    /// Two separate islands from one carve come back as two islands:
720    /// both beams hang on a single keystone voxel atop the pillar;
721    /// carving it detaches each side independently.
722    #[test]
723    fn two_islands_from_one_carve() {
724        let mut g = grid();
725        pillar(&mut g, 5, 5, 101);
726        // The keystone both beams connect through, sitting on the pillar.
727        g.set_voxel(IVec3::new(5, 5, 100), Some(STONE));
728        // Two beams off the keystone, along +x and −x.
729        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        // With the keystone in place everything is supported.
733        assert!(detect_islands(&g, IVec3::new(5, 5, 100), IVec3::new(5, 5, 100), 4096).is_empty());
734
735        // Carve the keystone: each beam floats on its own.
736        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    /// A pillar through the chz = 0/1 chunk border: every chunk's
745    /// local z=255 is format-pinned placeholder bedrock (`delslab`
746    /// clamps below `MAXZDIM`, so it is uncarvable — a component
747    /// containing it can never fall). A cut below the border must
748    /// detach exactly the segment under the cut — not report the
749    /// pinned upper segment, and not flood the upper chunk's 128×128
750    /// bedrock sheet into a phantom mega-island.
751    #[test]
752    fn stacked_chz_bedrock_is_anchored() {
753        let mut g = grid();
754        // Column (5,5), z ∈ [200, 400] — crosses the border at z=256
755        // and merges with chunk 0's pinned bedrock at z=255 on the way.
756        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    /// Manual perf probe (house-style, `--ignored --nocapture`): the
769    /// entry-doc gates are supported-exit (budget path) < 0.5 ms and
770    /// detach cost ∝ island size.
771    #[test]
772    #[ignore = "manual perf probe — cargo test -p roxlap-scene --lib islands -- --ignored --nocapture"]
773    fn perf_probe() {
774        // Worst case: a carve into a huge anchored plate — the flood
775        // must burn through DEFAULT_ISLAND_BUDGET and exit.
776        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        // Typical detach: a 64-voxel beam cut at its root.
794        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    // ---------- DT.1: extraction → sprite ----------
813
814    /// Extraction carves exactly the island's voxels (support stays),
815    /// bumps the chunk version, and a re-run of detection over the
816    /// same carve region finds nothing.
817    #[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    /// Extraction of a vertical run (one column, many voxels)
848    /// coalesces into span carves and clears the whole segment —
849    /// the stacked-chz island from `stacked_chz_bedrock_is_anchored`.
850    #[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    /// The KV6 model matches the island: dims = bbox extent, one
867    /// (surface) voxel per island voxel for a thin beam, colours
868    /// normalised to full-bright `0x80RRGGBB` — including a voxel
869    /// whose grid colour carries a dim baked brightness byte.
870    #[test]
871    fn to_kv6_matches_bbox_and_colours() {
872        const DIM_STONE: VoxColor = VoxColor(0x40B0_8040); // baked-dark byte
873        let mut g = grid();
874        pillar(&mut g, 2, 2, 100);
875        // Per-voxel inserts into air (an insert over an existing solid
876        // voxel does not repaint): x=5 carries the dim brightness byte.
877        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    /// A hand-constructed empty island (detection never yields one)
900    /// is a no-op: no edits, no bake over the degenerate default bbox.
901    #[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    /// World anchors honour the grid transform including
919    /// `voxel_world_size` (SC): the origin is the bbox-min corner, the
920    /// pivot the bbox centre (where `to_kv6`'s pivot sits).
921    #[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    // ---------- DT.5: fracture partitioners ----------
933
934    /// A hand-built solid box island for the partition tests.
935    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    /// The union of the fragments is exactly the original voxel set,
951    /// pairwise disjoint, with correct per-fragment bboxes.
952    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    /// `Chunks` yields several rounded cells forming a disjoint cover,
974    /// bit-identically for the same seed and differently-grouped for
975    /// another.
976    #[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    /// `Shards` slices into the requested number of roughly-equal
991    /// plates; each plate is thin along some direction (the slicing
992    /// normal — found here by sampling), unlike the island itself.
993    #[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            // Planarity metric: over sampled directions, the plate's
1007            // thinnest extent is well under the cube's 12-voxel edge —
1008            // a compact (non-planar) third of the cube could not be.
1009            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    /// A long sparse diagonal island (huge bbox, few voxels) splits
1032    /// in time and memory proportional to the ISLAND, not its bbox —
1033    /// the bbox-grid site placement this replaces would have built
1034    /// millions of sites here (maintainer review, DT.5).
1035    #[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    /// `Whole` and degenerate inputs pass through unchanged.
1051    #[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        // A cell far larger than the island ⇒ one fragment.
1058        let coarse = isl.split(FracturePattern::Chunks { cell: 64 }, 1);
1059        assert_eq!(coarse.len(), 1);
1060        assert_disjoint_cover(&isl, &coarse);
1061    }
1062
1063    /// Property test: on a randomised scene, `detect_islands` over the
1064    /// whole region agrees exactly with a naive dense oracle (flood
1065    /// from the bedrock plane; solid voxels not reached = islands).
1066    #[test]
1067    fn matches_dense_oracle() {
1068        // Deterministic xorshift.
1069        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        // A few anchored pillars…
1081        for _ in 0..4 {
1082            pillar(&mut g, rnd(nx), rnd(ny), 200);
1083        }
1084        // …and a scatter of small blobs, some touching pillars, some
1085        // not — clamped inside the region so the dense oracle sees the
1086        // whole world it reasons about.
1087        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        // One carve through the middle.
1093        g.set_sphere(IVec3::new(nx / 2, ny / 2, 205), 6, None);
1094
1095        // Oracle: 6-conn flood from every solid voxel at z=255 (the
1096        // bedrock plane all final runs share), dense over the region.
1097        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        // detect_islands seeded over the whole region.
1137        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}