Skip to main content

roxlap_formats/
edit.rs

1//! Voxel-edit primitives: column z-range buffer manipulation.
2//!
3//! Ported from voxlap5.c's column-edit helpers. Each column is
4//! represented during edit processing as a flat `[i32]` "z-range
5//! buffer" (`b2` in voxlap C):
6//!
7//! ```text
8//! [top0, bot0, top1, bot1, ..., top_sentinel, bot_sentinel]
9//! ```
10//!
11//! Each `(top_k, bot_k)` pair represents a contiguous SOLID region
12//! `[top_k, bot_k)`. Voxlap's z-axis grows downward (z=0 is sky), so
13//! `top_k < bot_k` and `bot_k` is exclusive. The list is terminated
14//! by a sentinel pair whose `bot` is `>= MAXZDIM`; air gaps live
15//! between adjacent slabs (`bot_k..top_{k+1}`).
16//!
17//! The buffer is owned by the caller; both helpers run in place and
18//! assume the caller sized `b2` with enough tail capacity to absorb
19//! the worst-case growth (one extra slab pair per `delslab` split,
20//! never more for `insslab` — it can only collapse). voxlap C sizes
21//! these via `SCPITCH * 3` slots; the Rust port inherits the
22//! contract.
23//!
24//! These helpers are CD.1 of the cave-demo plan. CD.2+ wraps them in
25//! `scum2_line` / `scum2_finish` and on-disk slab encode / decode.
26
27#![allow(dead_code)] // CD.1 lands the helpers; CD.2+ wires them up.
28
29/// Voxlap's `MAXZDIM` (voxlap5.h:10). World z is one byte → at most
30/// 256 voxels tall.
31pub const MAXZDIM: i32 = 256;
32
33/// Carve voxels in `[y0, y1)` to air on the column `b2`.
34///
35/// Port of `delslab` (voxlap5.c:4231). `b2` is mutated in place.
36///
37/// - `y0 >= y1` is a no-op.
38/// - `y1 >= MAXZDIM` is clamped to `MAXZDIM - 1` (matches C).
39/// - `b2.is_empty()` returns early (matches the C null-pointer
40///   guard).
41///
42/// In the worst case the carve splits a single solid slab in two,
43/// growing the list by one pair. The caller is responsible for
44/// sizing `b2` to absorb this. The helper does not allocate.
45pub fn delslab(b2: &mut [i32], y0: i32, mut y1: i32) {
46    if y1 >= MAXZDIM {
47        y1 = MAXZDIM - 1;
48    }
49    if y0 >= y1 || b2.is_empty() {
50        return;
51    }
52    let mut z = 0usize;
53    while y0 >= b2[z + 1] {
54        z += 2;
55    }
56    if y0 > b2[z] {
57        if y1 < b2[z + 1] {
58            // Carve sits strictly inside slab z: split it in two and
59            // shift the rest of the list right by one pair to make
60            // room.
61            let mut i = z;
62            while b2[i + 1] < MAXZDIM {
63                i += 2;
64            }
65            while i > z {
66                b2[i + 3] = b2[i + 1];
67                b2[i + 2] = b2[i];
68                i -= 2;
69            }
70            b2[z + 3] = b2[z + 1];
71            b2[z + 1] = y0;
72            b2[z + 2] = y1;
73            return;
74        }
75        // y1 reaches into (or past) the bottom of slab z: shrink slab
76        // z's bot to y0, then move on to handle slabs below.
77        b2[z + 1] = y0;
78        z += 2;
79    }
80    if y1 >= b2[z + 1] {
81        // y1 spans through slab z (and possibly further). Find the
82        // slab i that y1 lands in (above its bottom), adopt it as
83        // the new slab z, and shift the tail back to close the gap.
84        let mut i = z + 2;
85        while y1 >= b2[i + 1] {
86            i += 2;
87        }
88        let delta = i - z;
89        b2[z] = b2[i];
90        b2[z + 1] = b2[i + 1];
91        while b2[i + 1] < MAXZDIM {
92            i += 2;
93            b2[i - delta] = b2[i];
94            b2[i - delta + 1] = b2[i + 1];
95        }
96    }
97    if y1 > b2[z] {
98        // y1 falls inside slab z: clamp top.
99        b2[z] = y1;
100    }
101}
102
103/// Insert solid voxels in `[y0, y1)` on the column `b2`.
104///
105/// Port of `insslab` (voxlap5.c:4259). Mirrors the shape of
106/// [`delslab`]: walks `b2` to find where `[y0, y1)` lands and either
107/// inserts a fresh slab into an air gap or merges with adjacent
108/// slabs.
109///
110/// - `y0 >= y1` is a no-op.
111/// - `b2.is_empty()` returns early (matches the C null-pointer
112///   guard).
113/// - Unlike `delslab`, `insslab` does **not** clamp `y1` against
114///   `MAXZDIM`; voxlap relies on the caller for that. A `y1` value
115///   `>= MAXZDIM` collapses the column into a single solid slab
116///   that acts as the sentinel.
117pub fn insslab(b2: &mut [i32], y0: i32, y1: i32) {
118    if y0 >= y1 || b2.is_empty() {
119        return;
120    }
121    let mut z = 0usize;
122    while y0 > b2[z + 1] {
123        z += 2;
124    }
125    if y1 < b2[z] {
126        // [y0, y1) lives entirely in the air gap above slab z.
127        // Shift slabs [z..=last] right by one pair, then drop the
128        // new slab into slot z.
129        let mut i = z;
130        while b2[i + 1] < MAXZDIM {
131            i += 2;
132        }
133        loop {
134            b2[i + 3] = b2[i + 1];
135            b2[i + 2] = b2[i];
136            if i == z {
137                break;
138            }
139            i -= 2;
140        }
141        b2[z + 1] = y1;
142        b2[z] = y0;
143        return;
144    }
145    if y0 < b2[z] {
146        // [y0, y1) overlaps the top of slab z: extend the top up.
147        b2[z] = y0;
148    }
149    if y1 >= b2[z + 2] && b2[z + 1] < MAXZDIM {
150        // The insert reaches into slab z+2 (or further); merge slabs
151        // z..i into a single slab, where i is the last slab whose
152        // top is at or below y1.
153        let mut i = z + 2;
154        while y1 >= b2[i + 2] && b2[i + 1] < MAXZDIM {
155            i += 2;
156        }
157        let delta = i - z;
158        b2[z + 1] = b2[i + 1];
159        while b2[i + 1] < MAXZDIM {
160            i += 2;
161            b2[i - delta] = b2[i];
162            b2[i - delta + 1] = b2[i + 1];
163        }
164        // Stamp a sentinel at the now-vacated `i+2-delta` slot.
165        // The shift loop above exits with `b2[i+1] >= MAXZDIM`
166        // (slab `i` is the sentinel) WITHOUT copying it forward —
167        // so the merged slabs' old top/bot values are left in
168        // place between `b2[z+2]` and `b2[i+1]`. Walkers using
169        // `b2[i] < MAXZDIM` (top-check, e.g. `voxel_is_solid`)
170        // and the subsequent compilerle re-emit then see phantom
171        // overlapping runs that corrupt the column.
172        //
173        // Writing both top and bot ensures BOTH walker conventions
174        // (top-check and bot-check `< MAXZDIM`) terminate here.
175        b2[i + 2 - delta] = MAXZDIM;
176        b2[i + 3 - delta] = MAXZDIM;
177    }
178    if y1 > b2[z + 1] {
179        // y1 reaches past the bottom of slab z: extend the bot down.
180        b2[z + 1] = y1;
181    }
182}
183
184/// Decode a column's slab bytes into the `b2` z-range buffer.
185///
186/// Port of `expandrle` (voxlap5.c:4131). Walks the slab chain, writes
187/// `[top0, bot0, top1, bot1, ..., MAXZDIM_sentinel]` into `uind`.
188/// `uind` MUST be sized to hold every solid run plus the sentinel pair
189/// — voxlap allocates `MAXZDIM` slots, which is the worst-case bound
190/// (one slab per z value).
191///
192/// The `if (v[3] >= v[1]) continue` branch handles a degenerate slab
193/// where ceiling-z is at or below floor-z (no air gap above this
194/// slab); voxlap merges it implicitly into the previous solid run by
195/// skipping the slab in `uind`.
196pub fn expandrle(slab: &[u8], uind: &mut [i32]) {
197    uind[0] = i32::from(slab[1]);
198    let mut i = 2usize;
199    let mut v = 0usize;
200    while slab[v] != 0 {
201        v += usize::from(slab[v]) * 4;
202        if slab[v + 3] >= slab[v + 1] {
203            continue;
204        }
205        uind[i - 1] = i32::from(slab[v + 3]);
206        uind[i] = i32::from(slab[v + 1]);
207        i += 2;
208    }
209    uind[i - 1] = MAXZDIM;
210}
211
212/// One color-lookup record: original column's color for `z` in
213/// `[z_start, z_end)`. Built from a column's slab bytes by
214/// [`build_color_table`].
215#[derive(Debug)]
216struct ColorRange<'s> {
217    z_start: i32,
218    z_end: i32,
219    /// Colors for `[z_start, z_end)`, BGRA, 4 bytes per voxel,
220    /// ordered by `z`. Length `(z_end - z_start) * 4`.
221    colors: &'s [u8],
222}
223
224/// Build the `tbuf2` color-lookup table for a column. Voxlap's
225/// initial loop in `compilerle` (voxlap5.c:4163-4174). For each slab
226/// emits a floor-color range and (for non-first slabs) a ceiling-
227/// color range. Sentinel-terminated by a record at `z_start = MAXZDIM`.
228fn build_color_table(slab: &[u8]) -> Vec<ColorRange<'_>> {
229    let mut ranges = Vec::new();
230    let mut v = 0usize;
231    loop {
232        let z_start = i32::from(slab[v + 1]);
233        let z1c = i32::from(slab[v + 2]);
234        let z_end = z1c + 1;
235        let n_voxels = usize::try_from((z_end - z_start).max(0)).expect("voxel count >= 0");
236        let off = v + 4;
237        ranges.push(ColorRange {
238            z_start,
239            z_end,
240            colors: &slab[off..off + n_voxels * 4],
241        });
242
243        let nextptr = slab[v];
244        if nextptr == 0 {
245            break;
246        }
247        let prev_v = v;
248        v += usize::from(nextptr) * 4;
249        let ze = i32::from(slab[v + 3]);
250        // Ceiling color list of new slab. Voxlap stores these in the
251        // tail of the *previous* slab's bytes — between its floor
252        // colors and the new slab's header.
253        //
254        // C: ic[0] = ze + p.z - ia - i + 2, ic[1] = ze, ic[2] = v - ze*4
255        // where p.z = z1c_prev, ia = z1_prev, i = nextptr_prev.
256        let prev_z1 = i32::from(slab[prev_v + 1]);
257        let prev_z1c = i32::from(slab[prev_v + 2]);
258        let prev_nextptr = i32::from(slab[prev_v]);
259        let ceil_z_start = ze + prev_z1c - prev_z1 - prev_nextptr + 2;
260        let ceil_z_end = ze;
261        let ceil_n =
262            usize::try_from((ceil_z_end - ceil_z_start).max(0)).expect("ceiling voxel count >= 0");
263        // Colors live at slab[v - ceil_n*4 .. v].
264        let ceil_start = v - ceil_n * 4;
265        ranges.push(ColorRange {
266            z_start: ceil_z_start,
267            z_end: ceil_z_end,
268            colors: &slab[ceil_start..v],
269        });
270    }
271    ranges.push(ColorRange {
272        z_start: MAXZDIM,
273        z_end: MAXZDIM,
274        colors: &[],
275    });
276    ranges
277}
278
279/// Re-encode a column's `b2` z-range buffer to slab bytes.
280///
281/// Port of `compilerle` (voxlap5.c:4154). Walks `n0` (this column's
282/// b2) voxel-by-voxel, writing one BGRA color record per exposed
283/// solid voxel into `cbuf`. Color values are pulled from the
284/// `original_column`'s slab bytes (where present) or from
285/// `colfunc(px, py, z)` for newly-exposed voxels created by edits.
286///
287/// `n1..n4` are the four neighbor columns' b2 buffers (left, right,
288/// north, south, in voxlap's order); they drive the "exposed" flag
289/// `ia` that decides whether each voxel needs a color record.
290///
291/// Returns the number of bytes written to `cbuf`. Voxlap sizes
292/// `cbuf` to `MAXCSIZ = 1028` bytes — the caller must do the same.
293///
294/// # Panics
295///
296/// Panics if a `b2` z value (always in `0..=MAXZDIM`) doesn't fit in
297/// `u8` — would indicate a malformed b2 buffer.
298#[allow(
299    clippy::too_many_arguments,
300    clippy::too_many_lines,
301    clippy::missing_panics_doc
302)]
303pub(crate) fn compilerle(
304    n0: &[i32],
305    n1: &[i32],
306    n2: &[i32],
307    n3: &[i32],
308    n4: &[i32],
309    cbuf: &mut [u8],
310    original_column: &[u8],
311    px: i32,
312    py: i32,
313    colfunc: &mut dyn FnMut(i32, i32, i32) -> i32,
314) -> usize {
315    let tbuf2 = build_color_table(original_column);
316
317    let mut p_z: i32 = n0[0];
318    // Voxlap's char narrowing semantics: cbuf is byte-addressed, and
319    // `cbuf[n+1] = n0[i]` in C truncates to the low 8 bits. The
320    // sentinel run at the tail of `n0` carries MAXZDIM (=256) which
321    // wraps to 0 — voxlap uses that as part of the terminator slab.
322    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
323    let to_u8 = |v: i32| (v & 0xff) as u8;
324
325    cbuf[1] = to_u8(p_z);
326    let mut ze: i32 = n0[1];
327    cbuf[2] = to_u8(ze - 1);
328    cbuf[3] = 0;
329
330    let mut i = 0usize;
331    let mut onext = 0usize;
332    let mut ic = 0usize;
333    let mut ia: i32 = 15;
334    let mut n = 4usize;
335    let mut zend = if ze == MAXZDIM { -1 } else { ze - 1 };
336
337    let mut n1_idx = 0usize;
338    let mut n2_idx = 0usize;
339    let mut n3_idx = 0usize;
340    let mut n4_idx = 0usize;
341
342    'outer: loop {
343        let mut dacnt = 0;
344        'middle: loop {
345            // do { write voxel; ... } while (ia || p_z == zend)
346            let exit_to_rlendit2 = loop {
347                while p_z >= tbuf2[ic].z_end {
348                    ic += 1;
349                }
350                let color: i32 = if p_z >= tbuf2[ic].z_start {
351                    let off =
352                        usize::try_from((p_z - tbuf2[ic].z_start) * 4).expect("color offset >= 0");
353                    let bytes = &tbuf2[ic].colors[off..off + 4];
354                    i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
355                } else {
356                    colfunc(px, py, p_z)
357                };
358                cbuf[n..n + 4].copy_from_slice(&color.to_le_bytes());
359                n += 4;
360                p_z += 1;
361                if p_z >= ze {
362                    break true; // goto rlendit2
363                }
364                while p_z >= n1[n1_idx] {
365                    n1_idx += 1;
366                    ia ^= 1;
367                }
368                while p_z >= n2[n2_idx] {
369                    n2_idx += 1;
370                    ia ^= 2;
371                }
372                while p_z >= n3[n3_idx] {
373                    n3_idx += 1;
374                    ia ^= 4;
375                }
376                while p_z >= n4[n4_idx] {
377                    n4_idx += 1;
378                    ia ^= 8;
379                }
380                if !(ia != 0 || p_z == zend) {
381                    break false; // exit do-while: buried voxel
382                }
383            };
384
385            if exit_to_rlendit2 {
386                if ze >= MAXZDIM {
387                    break 'outer;
388                }
389                i += 2;
390                cbuf[onext] = u8::try_from((n - onext) >> 2).expect("slab dword count fits in u8");
391                onext = n;
392                p_z = n0[i];
393                cbuf[n + 1] = to_u8(p_z);
394                cbuf[n + 3] = to_u8(ze);
395                ze = n0[i + 1];
396                cbuf[n + 2] = to_u8(ze - 1);
397                n += 4;
398                zend = if ze == MAXZDIM { -1 } else { ze - 1 };
399                break 'middle; // restart 'outer with dacnt = 0
400            }
401
402            // Buried voxel: close the floor list (or open a sub-slab
403            // for the next exposed run).
404            if dacnt == 0 {
405                cbuf[onext + 2] = to_u8(p_z - 1);
406                dacnt = 1;
407            } else {
408                cbuf[onext] = u8::try_from((n - onext) >> 2).expect("slab dword count fits in u8");
409                onext = n;
410                cbuf[n + 1] = to_u8(p_z);
411                cbuf[n + 2] = to_u8(p_z - 1);
412                cbuf[n + 3] = to_u8(p_z);
413                n += 4;
414            }
415
416            // Skip forward to the smallest neighbor breakpoint.
417            let n1_v = n1[n1_idx];
418            let n2_v = n2[n2_idx];
419            let n3_v = n3[n3_idx];
420            let n4_v = n4[n4_idx];
421            if n1_v < n2_v && n1_v < n3_v && n1_v < n4_v {
422                if n1_v >= ze {
423                    p_z = ze - 1;
424                } else {
425                    p_z = n1_v;
426                    n1_idx += 1;
427                    ia ^= 1;
428                }
429            } else if n2_v < n3_v && n2_v < n4_v {
430                if n2_v >= ze {
431                    p_z = ze - 1;
432                } else {
433                    p_z = n2_v;
434                    n2_idx += 1;
435                    ia ^= 2;
436                }
437            } else if n3_v < n4_v {
438                if n3_v >= ze {
439                    p_z = ze - 1;
440                } else {
441                    p_z = n3_v;
442                    n3_idx += 1;
443                    ia ^= 4;
444                }
445            } else if n4_v >= ze {
446                p_z = ze - 1;
447            } else {
448                p_z = n4_v;
449                n4_idx += 1;
450                ia ^= 8;
451            }
452
453            if p_z == MAXZDIM - 1 {
454                break 'outer;
455            }
456            // continue 'middle: re-enter inner do-while with same dacnt
457        }
458    }
459
460    cbuf[onext] = 0;
461    n
462}
463
464// ====================================================================
465// ScumCtx: scum2_line + scum2_finish + scum2 dispatcher (CD.2.5)
466// ====================================================================
467//
468// Voxlap5.c:4431/4544/4507. The column-edit batch context.
469//
470// Acquires a `&mut Vxl` for the duration of an edit batch, manages
471// the rolling 3-row b2 buffer cache (`radar`), and re-encodes each
472// finished y-row through `compilerle` + `voxalloc`/`voxdealloc` /
473// `column_offset` updates.
474//
475// User flow (matching voxlap's setspans / setsphere / setcube):
476//
477// ```ignore
478// vxl.reserve_edit_capacity(headroom);
479// let mut ctx = ScumCtx::new(&mut vxl);
480// ctx.set_colfunc(|x, y, z| 0xff_8080_80);
481// for span in spans {
482//     let b2 = ctx.scum2(span.x, span.y).unwrap();
483//     insslab(b2, span.z0, span.z1);
484// }
485// ctx.finish();
486// ```
487//
488// Edits are accumulated per-column, then flushed when the y row
489// advances (so the 3-row neighborhood is stable). `finish` drains
490// the last 2 rows.
491
492use crate::vxl::Vxl;
493
494/// Voxlap's `SCPITCH` (`voxlap5.c:202`) — per-column-per-row stride
495/// (in i32 units) inside the radar buffer. b2 buffer for column X
496/// in a row whose base offset in radar is R lives at
497/// `radar[R + X * SCPITCH * 3 .. R + X * SCPITCH * 3 + SCPITCH]`.
498pub(crate) const SCPITCH: usize = 256;
499
500/// Voxlap's `MAXCSIZ` (`voxlap5.c:93`). Maximum bytes a single
501/// column can occupy after re-encoding through [`compilerle`].
502pub(crate) const MAXCSIZ: usize = 1028;
503
504/// Sentinel "no row started yet". Voxlap encodes this as
505/// `0x80000000` = `i32::MIN`.
506const SCOY_NONE: i32 = i32::MIN;
507
508/// Initial radar offset for `scoym3`. Voxlap's `&radar[SCPITCH*6]`.
509const SCOYM3_INITIAL: usize = SCPITCH * 6;
510
511/// When `scoym3` reaches this offset, wrap back to
512/// `SCOYM3_INITIAL`. Voxlap's `&radar[SCPITCH*9]` test.
513const SCOYM3_WRAP: usize = SCPITCH * 9;
514
515/// Column-edit batch context. Construct via [`ScumCtx::new`] after
516/// calling [`Vxl::reserve_edit_capacity`] on the world.
517///
518/// Holds a `&mut Vxl` borrow plus the rolling 3-row b2 buffer cache.
519/// Mutate columns via [`ScumCtx::scum2`] — it returns the b2 buffer
520/// for the requested column; mutate via [`delslab`] / [`insslab`].
521/// Edits are committed when the y row advances (or when
522/// [`ScumCtx::finish`] drains the last 2 rows).
523///
524/// Caller MUST invoke [`ScumCtx::finish`] explicitly. Drop without
525/// finish leaks the trailing 2 rows of edits — voxlap's contract.
526pub struct ScumCtx<'v> {
527    vxl: &'v mut Vxl,
528    /// Rolling b2 buffer cache. Sized `(vsid + 4) * 3 * SCPITCH` ints.
529    radar: Vec<i32>,
530    /// `compilerle` output scratch (= voxlap's `tbuf`).
531    cbuf: Vec<u8>,
532    /// Color callback (= voxlap's `vx5.colfunc`). Called by
533    /// `compilerle` for each newly-exposed voxel.
534    colfunc: Box<dyn FnMut(i32, i32, i32) -> i32 + 'v>,
535
536    // ---- rolling state (matches voxlap5.c globals) ------------------
537    scoy: i32,
538    scoym3: usize,
539    scx0: i32,
540    scx1: i32,
541    scox0: i32,
542    scox1: i32,
543    scoox0: i32,
544    scoox1: i32,
545    scex0: i32,
546    scex1: i32,
547    sceox0: i32,
548    sceox1: i32,
549
550    /// `(x, y)` of the most-recent successful [`ScumCtx::scum2`] call,
551    /// or `None` if no column has been loaded since the last row
552    /// advance / context creation. Used by [`ScumCtx::with_column`]
553    /// to skip the redundant `expandrle` when successive edits hit
554    /// the same column — voxlap's `setspans` correctness contract:
555    /// re-loading from `sptr` would wipe pending in-radar edits.
556    last_scum2: Option<(i32, i32)>,
557}
558
559#[allow(
560    clippy::cast_possible_truncation,
561    clippy::cast_possible_wrap,
562    clippy::cast_sign_loss,
563    clippy::if_not_else,
564    clippy::similar_names
565)]
566impl<'v> ScumCtx<'v> {
567    /// Open a new column-edit batch on a Vxl. The Vxl MUST have been
568    /// upgraded with [`Vxl::reserve_edit_capacity`] beforehand (the
569    /// slab allocator must be initialised).
570    ///
571    /// # Panics
572    ///
573    /// Panics if `vxl.vbit` is empty (no edit capacity reserved).
574    pub fn new(vxl: &'v mut Vxl) -> Self {
575        assert!(
576            !vxl.vbit.is_empty(),
577            "ScumCtx::new requires Vxl::reserve_edit_capacity to be called first"
578        );
579        let radar_size = (vxl.vsid as usize + 4) * 3 * SCPITCH;
580        Self {
581            vxl,
582            radar: vec![0i32; radar_size],
583            cbuf: vec![0u8; MAXCSIZ],
584            colfunc: Box::new(|_, _, _| 0),
585            scoy: SCOY_NONE,
586            scoym3: SCOYM3_INITIAL,
587            scx0: 0,
588            scx1: 0,
589            scox0: 0,
590            scox1: 0,
591            scoox0: 0,
592            scoox1: 0,
593            scex0: 0,
594            scex1: 0,
595            sceox0: 0,
596            sceox1: 0,
597            last_scum2: None,
598        }
599    }
600
601    /// Install the color callback. Voxlap's `vx5.colfunc`. Called
602    /// for each newly-exposed voxel produced by edits.
603    pub fn set_colfunc<F>(&mut self, f: F)
604    where
605        F: FnMut(i32, i32, i32) -> i32 + 'v,
606    {
607        self.colfunc = Box::new(f);
608    }
609
610    /// Open column `(x, y)` for editing; returns its b2 buffer.
611    /// Caller mutates via [`delslab`] / [`insslab`].
612    ///
613    /// Auto-flushes any prior y row that's no longer in the rolling
614    /// window. Returns `None` if `(x, y)` is out of world bounds.
615    pub fn scum2(&mut self, x: i32, y: i32) -> Option<&mut [i32]> {
616        let vsid = self.vxl.vsid as i32;
617        if x < 0 || x >= vsid || y < 0 || y >= vsid {
618            return None;
619        }
620
621        if y != self.scoy {
622            if self.scoy != SCOY_NONE {
623                self.scum2_line();
624                while self.scoy < y - 1 {
625                    self.scx0 = i32::MAX;
626                    self.scx1 = i32::MIN;
627                    self.advance_row();
628                    self.scum2_line();
629                }
630                self.advance_row();
631            } else {
632                self.scoox0 = i32::MAX;
633                self.scox0 = i32::MAX;
634                self.sceox0 = x + 1;
635                self.scex0 = x + 1;
636                self.sceox1 = x;
637                self.scex1 = x;
638                self.scoy = y;
639                self.scoym3 = SCOYM3_INITIAL;
640            }
641            self.scx0 = x;
642        } else {
643            // Same y row as previous call: fill any skipped columns
644            // between scx1 and x so the 3-row window stays continuous.
645            while self.scx1 < x - 1 {
646                self.scx1 += 1;
647                let scx1 = self.scx1;
648                self.expand_column_into_row(scx1, y, self.scoym3);
649            }
650        }
651
652        let radar_idx = self.scoym3 + (x as usize) * SCPITCH * 3;
653        self.scx1 = x;
654        self.expand_column_into_row(x, y, self.scoym3);
655        self.last_scum2 = Some((x, y));
656        Some(&mut self.radar[radar_idx..radar_idx + SCPITCH])
657    }
658
659    /// Edit one column with closure-based access. If `(x, y)` matches
660    /// the immediately-previous successful [`ScumCtx::scum2`] /
661    /// `with_column` call, reuses the cached b2 buffer in radar
662    /// (skipping the redundant `expandrle` that would wipe pending
663    /// edits). Otherwise calls `scum2` to load the column.
664    ///
665    /// This is the primary edit API for span-style batch operations
666    /// where multiple z ranges land on the same column —
667    /// [`set_spans`] is a thin wrapper. Returns `false` and skips
668    /// the closure if `(x, y)` is out of world bounds.
669    pub fn with_column<F>(&mut self, x: i32, y: i32, f: F) -> bool
670    where
671        F: FnOnce(&mut [i32]),
672    {
673        if self.last_scum2 != Some((x, y)) && self.scum2(x, y).is_none() {
674            return false;
675        }
676        // At this point either the cache hit or scum2 succeeded —
677        // both leave the column's b2 at scoym3 + x * SCPITCH * 3.
678        let radar_idx = self.scoym3 + (x as usize) * SCPITCH * 3;
679        let b2 = &mut self.radar[radar_idx..radar_idx + SCPITCH];
680        f(b2);
681        true
682    }
683
684    /// Drain the last 2 rows and consume the context. MUST be called
685    /// — Drop does not auto-finish (voxlap contract).
686    pub fn finish(mut self) {
687        if self.scoy == SCOY_NONE {
688            return;
689        }
690        for _ in 0..2 {
691            self.scum2_line();
692            self.scx0 = i32::MAX;
693            self.scx1 = i32::MIN;
694            self.advance_row();
695        }
696        self.scum2_line();
697        self.scoy = SCOY_NONE;
698    }
699
700    /// Bump scoy by 1 and advance scoym3 in the radar ring.
701    /// Invalidates the [`ScumCtx::with_column`] cache: the prior
702    /// row's column slots are still in the radar but their relative
703    /// offset to the new `scoym3` has shifted, so re-using the
704    /// cached `(x, y)` would index the wrong slot.
705    fn advance_row(&mut self) {
706        self.scoy += 1;
707        self.scoym3 += SCPITCH;
708        if self.scoym3 == SCOYM3_WRAP {
709            self.scoym3 = SCOYM3_INITIAL;
710        }
711        self.last_scum2 = None;
712    }
713
714    /// Load column `(x, y)` from the slab pool into the radar slot
715    /// at `row_base + x * SCPITCH * 3`. Out-of-world columns get the
716    /// all-solid sentinel `[0, MAXZDIM]` (matches voxlap's expandrle
717    /// out-of-bounds behaviour).
718    fn expand_column_into_row(&mut self, x: i32, y: i32, row_base: usize) {
719        let vsid = self.vxl.vsid as i32;
720        // Radar offset; voxlap relies on the prefix slack for x = -1.
721        let radar_idx_signed = (row_base as isize) + (x as isize) * (SCPITCH as isize) * 3;
722        if radar_idx_signed < 0 {
723            return;
724        }
725        #[allow(clippy::cast_sign_loss)]
726        let radar_idx = radar_idx_signed as usize;
727        if radar_idx + SCPITCH > self.radar.len() {
728            return;
729        }
730        if x < 0 || x >= vsid || y < 0 || y >= vsid {
731            self.radar[radar_idx] = 0;
732            self.radar[radar_idx + 1] = MAXZDIM;
733            return;
734        }
735        let idx = (y as usize) * (vsid as usize) + (x as usize);
736        let slab = self.vxl.column_data(idx);
737        expandrle(slab, &mut self.radar[radar_idx..radar_idx + SCPITCH]);
738    }
739
740    /// Flush row `scoy - 1` (the middle of the rolling 3-row window).
741    /// Voxlap5.c:4431.
742    #[allow(clippy::too_many_lines)]
743    fn scum2_line(&mut self) {
744        let vsid = self.vxl.vsid as i32;
745
746        // x0 = min(scox0-1, min(scx0, scoox0)); x1 = max(scox1+1, max(scx1, scoox1))
747        let x0 = (self.scox0 - 1).min(self.scx0).min(self.scoox0);
748        self.scoox0 = self.scox0;
749        self.scox0 = self.scx0;
750        let x1 = (self.scox1 + 1).max(self.scx1).max(self.scoox1);
751        self.scoox1 = self.scox1;
752        self.scox1 = self.scx1;
753
754        let uptr = wrap_radar(self.scoym3 + SCPITCH);
755        let mptr = wrap_radar(uptr + SCPITCH);
756
757        // Load row scoy-2 (uptr) for [x0, x1] minus [sceox0, sceox1].
758        let scoy_2 = self.scoy - 2;
759        if x1 < self.sceox0 || x0 > self.sceox1 {
760            for x in x0..=x1 {
761                self.expand_column_into_row(x, scoy_2, uptr);
762            }
763        } else {
764            for x in x0..self.sceox0 {
765                self.expand_column_into_row(x, scoy_2, uptr);
766            }
767            let mut x = x1;
768            while x > self.sceox1 {
769                self.expand_column_into_row(x, scoy_2, uptr);
770                x -= 1;
771            }
772        }
773
774        // Load row scoy-1 (mptr) for [x0-1, x1+1].
775        let scoy_1 = self.scoy - 1;
776        if (self.scex1 | x1) >= 0 {
777            for x in (x1 + 2)..self.scex0 {
778                self.expand_column_into_row(x, scoy_1, mptr);
779            }
780            let mut x = x0 - 2;
781            while x > self.scex1 {
782                self.expand_column_into_row(x, scoy_1, mptr);
783                x -= 1;
784            }
785        }
786        if x1 + 1 < self.scex0 || x0 - 1 > self.scex1 {
787            for x in (x0 - 1)..=(x1 + 1) {
788                self.expand_column_into_row(x, scoy_1, mptr);
789            }
790        } else {
791            for x in (x0 - 1)..self.scex0 {
792                self.expand_column_into_row(x, scoy_1, mptr);
793            }
794            let mut x = x1 + 1;
795            while x > self.scex1 {
796                self.expand_column_into_row(x, scoy_1, mptr);
797                x -= 1;
798            }
799        }
800        self.sceox0 = (x0 - 1).min(self.scex0);
801        self.sceox1 = (x1 + 1).max(self.scex1);
802
803        // Load row scoy (scoym3) for [x0, x1] minus [scx0, scx1].
804        let scoy_0 = self.scoy;
805        let scoym3 = self.scoym3;
806        if x1 < self.scx0 || x0 > self.scx1 {
807            for x in x0..=x1 {
808                self.expand_column_into_row(x, scoy_0, scoym3);
809            }
810        } else {
811            for x in x0..self.scx0 {
812                self.expand_column_into_row(x, scoy_0, scoym3);
813            }
814            let mut x = x1;
815            while x > self.scx1 {
816                self.expand_column_into_row(x, scoy_0, scoym3);
817                x -= 1;
818            }
819        }
820        self.scex0 = x0;
821        self.scex1 = x1;
822
823        // Flush row scoy-1: re-encode each column in [x0, x1] within
824        // [0, vsid).
825        let y = self.scoy - 1;
826        if !(0..vsid).contains(&y) {
827            return;
828        }
829        let x0_clamped = x0.max(0);
830        let x1_clamped = x1.min(vsid - 1);
831
832        for x in x0_clamped..=x1_clamped {
833            self.flush_column(x, y, mptr, uptr, scoym3);
834        }
835    }
836
837    /// Re-encode column (x, y) using its b2 buffer in `mptr` and
838    /// neighbor b2s in mptr (left/right) + uptr (above) + scoym3
839    /// (below). Commits the new bytes to the slab pool.
840    #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
841    fn flush_column(&mut self, x: i32, y: i32, mptr: usize, uptr: usize, scoym3: usize) {
842        let vsid = self.vxl.vsid as usize;
843        let k = (x as usize) * SCPITCH * 3;
844        let n0_pos = mptr + k;
845        let n1_pos_signed = (mptr as isize) + (k as isize) - (SCPITCH as isize) * 3;
846        let n2_pos = mptr + k + SCPITCH * 3;
847        let n3_pos = uptr + k;
848        let n4_pos = scoym3 + k;
849
850        // n1_pos may be at a negative offset for x = 0; voxlap relies
851        // on radar prefix slack. Skip if the slot is outside our radar.
852        if n1_pos_signed < 0 {
853            return;
854        }
855        let n1_pos = n1_pos_signed as usize;
856
857        let idx = (y as usize) * vsid + (x as usize);
858
859        // Snapshot original column bytes — compilerle reads colors
860        // from them, and we overwrite them later via voxalloc + copy.
861        let original_bytes: Vec<u8> = self.vxl.column_data(idx).to_vec();
862
863        let written = {
864            let radar = &self.radar;
865            let n0 = &radar[n0_pos..n0_pos + SCPITCH];
866            let n1 = &radar[n1_pos..n1_pos + SCPITCH];
867            let n2 = &radar[n2_pos..n2_pos + SCPITCH];
868            let n3 = &radar[n3_pos..n3_pos + SCPITCH];
869            let n4 = &radar[n4_pos..n4_pos + SCPITCH];
870            compilerle(
871                n0,
872                n1,
873                n2,
874                n3,
875                n4,
876                &mut self.cbuf,
877                &original_bytes,
878                x,
879                y,
880                &mut *self.colfunc,
881            )
882        };
883
884        let old_offset = self.vxl.column_offset[idx];
885        self.vxl.voxdealloc(old_offset);
886        let new_offset = self.vxl.voxalloc(written as u32);
887        self.vxl.data[new_offset as usize..new_offset as usize + written]
888            .copy_from_slice(&self.cbuf[..written]);
889        self.vxl.column_offset[idx] = new_offset;
890    }
891}
892
893/// Wrap a radar offset back into the rolling-window range
894/// `[SCOYM3_INITIAL, SCOYM3_WRAP)`.
895fn wrap_radar(off: usize) -> usize {
896    if off == SCOYM3_WRAP {
897        SCOYM3_INITIAL
898    } else {
899        off
900    }
901}
902
903// ====================================================================
904// set_spans (CD.3) — thin wrapper over ScumCtx + delslab/insslab.
905// ====================================================================
906
907/// One vertical span on a column: `(x, y, z0..=z1)` solid voxels.
908///
909/// `z1` is INCLUSIVE per voxlap's `vspans` convention — the actual
910/// edited range is the half-open `[z0, z1 + 1)`. Same convention
911/// makes a `Vspan { z0: 100, z1: 100 }` carve / fill exactly one
912/// voxel at z=100.
913///
914/// `x` / `y` are full-world `u32` coordinates (cleaner than voxlap's
915/// 8-bit `vspans.x` + `setspans` `offs` trick — for our cave demo
916/// the patch-relative + offset machinery isn't needed).
917#[derive(Debug, Clone, Copy, PartialEq, Eq)]
918pub struct Vspan {
919    pub x: u32,
920    pub y: u32,
921    pub z0: u8,
922    pub z1: u8,
923}
924
925/// Operation for span-style edits.
926///
927/// `Carve` flips the listed voxels to air (per-span [`delslab`]) —
928/// the colfunc is consulted by the internal RLE re-compile step for
929/// newly-exposed voxels just outside the carved range (above and
930/// below) which weren't previously in the column's color list.
931///
932/// `Insert` flips the listed voxels to solid (per-span [`insslab`])
933/// — the colfunc is consulted for the inserted voxels themselves.
934#[derive(Debug, Clone, Copy, PartialEq, Eq)]
935pub enum SpanOp {
936    Carve,
937    Insert,
938}
939
940/// Apply a list of column-aligned vertical spans with a custom color
941/// callback. Voxlap5.c:5247 `setspans` ported with full `vx5.colfunc`
942/// flexibility — the closure can capture arbitrary state to implement
943/// voxlap's `curcolfunc` / `jitcolfunc` / `pngcolfunc` / `dust` /
944/// procedural colour patterns.
945///
946/// `colfunc(x, y, z) -> i32` returns the BGRA colour for any voxel
947/// that needs one: the inserted voxels (Insert op) or the newly-
948/// exposed voxels just outside the carved range (Carve op).
949///
950/// **Contract**: `spans` MUST be sorted ascending by `(y, x)` and,
951/// within each `(x, y)` group, ascending by `z0`. Voxlap relies on
952/// this for correct row-flush ordering and for `with_column`'s
953/// caching invariant. Out-of-bounds spans (x or y >= vsid) are
954/// silently skipped.
955///
956/// Empty input is a no-op (no `ScumCtx` is created).
957///
958/// # Panics
959///
960/// Panics if `world.vbit` is empty — call
961/// [`Vxl::reserve_edit_capacity`] first.
962#[allow(clippy::cast_possible_wrap)]
963pub fn set_spans_with_colfunc<F>(world: &mut Vxl, spans: &[Vspan], op: SpanOp, colfunc: F)
964where
965    F: FnMut(i32, i32, i32) -> i32,
966{
967    if spans.is_empty() {
968        return;
969    }
970    let inserting = op == SpanOp::Insert;
971    let mut ctx = ScumCtx::new(world);
972    ctx.set_colfunc(colfunc);
973    for span in spans {
974        let x = span.x as i32;
975        let y = span.y as i32;
976        let z0 = i32::from(span.z0);
977        let z1 = i32::from(span.z1) + 1; // inclusive → half-open exclusive
978        ctx.with_column(x, y, |b2| {
979            if inserting {
980                insslab(b2, z0, z1);
981            } else {
982                delslab(b2, z0, z1);
983            }
984        });
985    }
986    ctx.finish();
987}
988
989/// Apply a list of column-aligned vertical spans with a constant
990/// colour. Convenience wrapper over [`set_spans_with_colfunc`] for
991/// the common cases:
992///
993/// - `color = None` → carve. Newly-exposed voxels get colour 0.
994/// - `color = Some(c)` → insert. Inserted voxels get colour `c`.
995///
996/// For non-constant colours (jitter, texture-mapped, position-
997/// dependent, gradient by depth), use [`set_spans_with_colfunc`]
998/// directly with a closure capturing the relevant state.
999///
1000/// See [`set_spans_with_colfunc`] for the sort-order contract and
1001/// panic semantics.
1002pub fn set_spans(world: &mut Vxl, spans: &[Vspan], color: Option<u32>) {
1003    let op = if color.is_some() {
1004        SpanOp::Insert
1005    } else {
1006        SpanOp::Carve
1007    };
1008    #[allow(clippy::cast_possible_wrap)]
1009    let c_i32 = color.unwrap_or(0) as i32;
1010    set_spans_with_colfunc(world, spans, op, move |_, _, _| c_i32);
1011}
1012
1013// ====================================================================
1014// set_cube / set_rect / set_sphere (CD.4) — region wrappers.
1015// ====================================================================
1016
1017/// Edit a single voxel at `(x, y, z)`. Voxlap5.c:4669 `setcube`.
1018///
1019/// `color = None` carves to air; `Some(c)` inserts solid coloured
1020/// `c`. Out-of-bounds coordinates are silently skipped.
1021///
1022/// **Note**: This port skips voxlap C's "exposed-solid in-place
1023/// colour overwrite" optimization — every call goes through the
1024/// scum2 + delslab/insslab + compilerle pipeline. Per-voxel edits
1025/// are rare in the cave-demo workload; the optimization can land
1026/// later if needed.
1027///
1028/// # Panics
1029///
1030/// Panics if `world.vbit` is empty — call
1031/// [`Vxl::reserve_edit_capacity`] first.
1032pub fn set_cube(world: &mut Vxl, x: i32, y: i32, z: i32, color: Option<u32>) {
1033    let op = if color.is_some() {
1034        SpanOp::Insert
1035    } else {
1036        SpanOp::Carve
1037    };
1038    #[allow(clippy::cast_possible_wrap)]
1039    let c_i32 = color.unwrap_or(0) as i32;
1040    set_cube_with_colfunc(world, x, y, z, op, move |_, _, _| c_i32);
1041}
1042
1043/// [`set_cube`] with a custom colour callback.
1044#[allow(
1045    clippy::cast_possible_truncation,
1046    clippy::cast_possible_wrap,
1047    clippy::cast_sign_loss
1048)]
1049pub fn set_cube_with_colfunc<F>(world: &mut Vxl, x: i32, y: i32, z: i32, op: SpanOp, colfunc: F)
1050where
1051    F: FnMut(i32, i32, i32) -> i32,
1052{
1053    let vsid = world.vsid as i32;
1054    if x < 0 || x >= vsid || y < 0 || y >= vsid || !(0..MAXZDIM).contains(&z) {
1055        return;
1056    }
1057    let span = Vspan {
1058        x: x as u32,
1059        y: y as u32,
1060        z0: z as u8,
1061        z1: z as u8,
1062    };
1063    set_spans_with_colfunc(world, &[span], op, colfunc);
1064}
1065
1066/// Edit an axis-aligned box `[lo, hi]` (inclusive on both ends in
1067/// every axis). Voxlap5.c:5214 `setrect`.
1068///
1069/// `color = None` carves to air; `Some(c)` inserts solid coloured
1070/// `c`. The box is sorted and clamped to world bounds before
1071/// iteration; an empty box (any axis where lo > hi after clamp) is
1072/// a no-op.
1073///
1074/// # Panics
1075///
1076/// Panics if `world.vbit` is empty — call
1077/// [`Vxl::reserve_edit_capacity`] first.
1078pub fn set_rect(world: &mut Vxl, lo: [i32; 3], hi: [i32; 3], color: Option<u32>) {
1079    let op = if color.is_some() {
1080        SpanOp::Insert
1081    } else {
1082        SpanOp::Carve
1083    };
1084    #[allow(clippy::cast_possible_wrap)]
1085    let c_i32 = color.unwrap_or(0) as i32;
1086    set_rect_with_colfunc(world, lo, hi, op, move |_, _, _| c_i32);
1087}
1088
1089/// [`set_rect`] with a custom colour callback.
1090#[allow(
1091    clippy::cast_possible_truncation,
1092    clippy::cast_possible_wrap,
1093    clippy::cast_sign_loss
1094)]
1095pub fn set_rect_with_colfunc<F>(world: &mut Vxl, lo: [i32; 3], hi: [i32; 3], op: SpanOp, colfunc: F)
1096where
1097    F: FnMut(i32, i32, i32) -> i32,
1098{
1099    let vsid = world.vsid as i32;
1100    let xs = lo[0].min(hi[0]).max(0);
1101    let xe = lo[0].max(hi[0]).min(vsid - 1);
1102    let ys = lo[1].min(hi[1]).max(0);
1103    let ye = lo[1].max(hi[1]).min(vsid - 1);
1104    let zs = lo[2].min(hi[2]).max(0);
1105    let ze = lo[2].max(hi[2]).min(MAXZDIM - 1);
1106    if xs > xe || ys > ye || zs > ze {
1107        return;
1108    }
1109    let inserting = op == SpanOp::Insert;
1110    let mut ctx = ScumCtx::new(world);
1111    ctx.set_colfunc(colfunc);
1112    for y in ys..=ye {
1113        for x in xs..=xe {
1114            ctx.with_column(x, y, |b2| {
1115                if inserting {
1116                    insslab(b2, zs, ze + 1);
1117                } else {
1118                    delslab(b2, zs, ze + 1);
1119                }
1120            });
1121        }
1122    }
1123    ctx.finish();
1124}
1125
1126/// Edit a sphere of voxels centred at `center` with the given
1127/// `radius`. Voxlap5.c:4970 `setsphere`.
1128///
1129/// Uses Euclidean distance (voxlap's default `vx5.curpow = 2.0`
1130/// case). For voxlap-style non-Euclidean shapes (octahedron at
1131/// `curpow = 1.0`, etc.) the user can drop down to [`ScumCtx`] and
1132/// roll their own iteration; the cave-demo's spherical bullet
1133/// impacts only need the Euclidean case.
1134///
1135/// `color = None` carves to air; `Some(c)` inserts solid coloured
1136/// `c`. The bounding box is clamped to world bounds; a sphere fully
1137/// outside the world is a no-op.
1138///
1139/// # Panics
1140///
1141/// Panics if `world.vbit` is empty — call
1142/// [`Vxl::reserve_edit_capacity`] first.
1143pub fn set_sphere(world: &mut Vxl, center: [i32; 3], radius: u32, color: Option<u32>) {
1144    let op = if color.is_some() {
1145        SpanOp::Insert
1146    } else {
1147        SpanOp::Carve
1148    };
1149    #[allow(clippy::cast_possible_wrap)]
1150    let c_i32 = color.unwrap_or(0) as i32;
1151    set_sphere_with_colfunc(world, center, radius, op, move |_, _, _| c_i32);
1152}
1153
1154/// [`set_sphere`] with a custom colour callback.
1155#[allow(
1156    clippy::cast_possible_truncation,
1157    clippy::cast_possible_wrap,
1158    clippy::cast_sign_loss,
1159    clippy::cast_precision_loss,
1160    clippy::similar_names
1161)]
1162pub fn set_sphere_with_colfunc<F>(
1163    world: &mut Vxl,
1164    center: [i32; 3],
1165    radius: u32,
1166    op: SpanOp,
1167    colfunc: F,
1168) where
1169    F: FnMut(i32, i32, i32) -> i32,
1170{
1171    let vsid = world.vsid as i32;
1172    let cx = center[0];
1173    let cy = center[1];
1174    let cz = center[2];
1175    let r = radius as i32;
1176    let xs = (cx - r).max(0);
1177    let xe = (cx + r).min(vsid - 1);
1178    let ys = (cy - r).max(0);
1179    let ye = (cy + r).min(vsid - 1);
1180    let zs = (cz - r).max(0);
1181    let ze = (cz + r).min(MAXZDIM - 1);
1182    if xs > xe || ys > ye || zs > ze {
1183        return;
1184    }
1185    let r_sq = r * r;
1186    let inserting = op == SpanOp::Insert;
1187    let mut ctx = ScumCtx::new(world);
1188    ctx.set_colfunc(colfunc);
1189    for y in ys..=ye {
1190        let dy = y - cy;
1191        let dy_sq = dy * dy;
1192        if dy_sq > r_sq {
1193            continue;
1194        }
1195        for x in xs..=xe {
1196            let dx = x - cx;
1197            let dx_sq = dx * dx;
1198            let xy_sq = dx_sq + dy_sq;
1199            if xy_sq > r_sq {
1200                continue;
1201            }
1202            // dz_max satisfies dx² + dy² + dz² <= r²; voxel range is
1203            // z = cz - dz_max ..= cz + dz_max.
1204            let dz_max_sq = r_sq - xy_sq;
1205            let dz_max = (dz_max_sq as f32).sqrt() as i32;
1206            let z_lo = (cz - dz_max).max(zs);
1207            let z_hi = (cz + dz_max).min(ze);
1208            if z_lo > z_hi {
1209                continue;
1210            }
1211            ctx.with_column(x, y, |b2| {
1212                if inserting {
1213                    insslab(b2, z_lo, z_hi + 1);
1214                } else {
1215                    delslab(b2, z_lo, z_hi + 1);
1216                }
1217            });
1218        }
1219    }
1220    ctx.finish();
1221}
1222
1223#[cfg(test)]
1224#[allow(
1225    clippy::cast_possible_truncation,
1226    clippy::cast_possible_wrap,
1227    clippy::cast_sign_loss,
1228    clippy::items_after_statements
1229)]
1230mod tests {
1231    use super::*;
1232
1233    /// Build a sentinel-terminated `b2` from a list of solid slabs.
1234    /// The buffer has slack at the tail so split-style ops have room
1235    /// to shift.
1236    fn build_b2(slabs: &[(i32, i32)]) -> Vec<i32> {
1237        let mut buf: Vec<i32> = Vec::new();
1238        for &(top, bot) in slabs {
1239            assert!(top < bot, "slab top must be < bot");
1240            assert!(bot < MAXZDIM, "slab bot must fit below MAXZDIM");
1241            buf.push(top);
1242            buf.push(bot);
1243        }
1244        // Sentinel pair. voxlap's expandrle terminates with
1245        // bot = MAXZDIM; top is unread (writes only).
1246        buf.push(MAXZDIM);
1247        buf.push(MAXZDIM);
1248        // Slack — accommodates worst-case growth for any test.
1249        buf.resize(buf.len() + 32, 0);
1250        buf
1251    }
1252
1253    /// Read back the slab list before the sentinel.
1254    fn read_slabs(b2: &[i32]) -> Vec<(i32, i32)> {
1255        let mut out = Vec::new();
1256        let mut i = 0;
1257        while b2[i + 1] < MAXZDIM {
1258            out.push((b2[i], b2[i + 1]));
1259            i += 2;
1260        }
1261        out
1262    }
1263
1264    // ---- delslab ----------------------------------------------------
1265
1266    #[test]
1267    fn delslab_noop_y0_ge_y1() {
1268        let mut b2 = build_b2(&[(10, 20)]);
1269        delslab(&mut b2, 15, 15);
1270        assert_eq!(read_slabs(&b2), [(10, 20)]);
1271        delslab(&mut b2, 20, 10);
1272        assert_eq!(read_slabs(&b2), [(10, 20)]);
1273    }
1274
1275    #[test]
1276    fn delslab_split_inside_one_slab() {
1277        let mut b2 = build_b2(&[(10, 30)]);
1278        delslab(&mut b2, 15, 20);
1279        assert_eq!(read_slabs(&b2), [(10, 15), (20, 30)]);
1280    }
1281
1282    #[test]
1283    fn delslab_shrink_bot_of_slab() {
1284        let mut b2 = build_b2(&[(10, 30)]);
1285        delslab(&mut b2, 20, 30);
1286        assert_eq!(read_slabs(&b2), [(10, 20)]);
1287    }
1288
1289    #[test]
1290    fn delslab_shrink_top_of_slab() {
1291        let mut b2 = build_b2(&[(10, 30)]);
1292        delslab(&mut b2, 5, 15);
1293        assert_eq!(read_slabs(&b2), [(15, 30)]);
1294    }
1295
1296    #[test]
1297    fn delslab_carve_full_slab() {
1298        let mut b2 = build_b2(&[(10, 30)]);
1299        delslab(&mut b2, 5, 35);
1300        assert_eq!(read_slabs(&b2), Vec::<(i32, i32)>::new());
1301    }
1302
1303    #[test]
1304    fn delslab_in_air_noop() {
1305        let mut b2 = build_b2(&[(10, 30)]);
1306        delslab(&mut b2, 0, 8);
1307        assert_eq!(read_slabs(&b2), [(10, 30)]);
1308        delslab(&mut b2, 35, 50);
1309        assert_eq!(read_slabs(&b2), [(10, 30)]);
1310    }
1311
1312    #[test]
1313    fn delslab_span_two_slabs_carve_middle() {
1314        let mut b2 = build_b2(&[(10, 30), (50, 70)]);
1315        delslab(&mut b2, 20, 60);
1316        assert_eq!(read_slabs(&b2), [(10, 20), (60, 70)]);
1317    }
1318
1319    #[test]
1320    fn delslab_carve_two_full_slabs_keep_third() {
1321        let mut b2 = build_b2(&[(10, 20), (30, 40), (50, 60)]);
1322        delslab(&mut b2, 5, 45);
1323        assert_eq!(read_slabs(&b2), [(50, 60)]);
1324    }
1325
1326    #[test]
1327    fn delslab_y1_clamped_to_maxzdim_minus_1() {
1328        let mut b2 = build_b2(&[(10, 200)]);
1329        delslab(&mut b2, 100, MAXZDIM);
1330        assert_eq!(read_slabs(&b2), [(10, 100)]);
1331    }
1332
1333    #[test]
1334    fn delslab_carve_top_edge_of_slab() {
1335        // y1 == top of slab → should leave the slab untouched (the
1336        // carve range ends right at the surface).
1337        let mut b2 = build_b2(&[(10, 30)]);
1338        delslab(&mut b2, 5, 10);
1339        assert_eq!(read_slabs(&b2), [(10, 30)]);
1340    }
1341
1342    #[test]
1343    fn delslab_carve_bot_edge_of_slab() {
1344        // y0 == bot of slab → no overlap.
1345        let mut b2 = build_b2(&[(10, 30)]);
1346        delslab(&mut b2, 30, 35);
1347        assert_eq!(read_slabs(&b2), [(10, 30)]);
1348    }
1349
1350    #[test]
1351    fn delslab_carve_exact_full_slab_keeps_neighbors() {
1352        let mut b2 = build_b2(&[(10, 20), (30, 40), (50, 60)]);
1353        delslab(&mut b2, 30, 40);
1354        assert_eq!(read_slabs(&b2), [(10, 20), (50, 60)]);
1355    }
1356
1357    // ---- insslab ----------------------------------------------------
1358
1359    #[test]
1360    fn insslab_noop_y0_ge_y1() {
1361        let mut b2 = build_b2(&[(10, 20)]);
1362        insslab(&mut b2, 15, 15);
1363        assert_eq!(read_slabs(&b2), [(10, 20)]);
1364        insslab(&mut b2, 20, 10);
1365        assert_eq!(read_slabs(&b2), [(10, 20)]);
1366    }
1367
1368    #[test]
1369    fn insslab_into_pure_air() {
1370        let mut b2 = build_b2(&[]);
1371        insslab(&mut b2, 10, 30);
1372        assert_eq!(read_slabs(&b2), [(10, 30)]);
1373    }
1374
1375    #[test]
1376    fn insslab_into_air_gap_above_slab() {
1377        let mut b2 = build_b2(&[(50, 70)]);
1378        insslab(&mut b2, 10, 30);
1379        assert_eq!(read_slabs(&b2), [(10, 30), (50, 70)]);
1380    }
1381
1382    #[test]
1383    fn insslab_into_air_gap_between_slabs() {
1384        let mut b2 = build_b2(&[(10, 20), (60, 70)]);
1385        insslab(&mut b2, 30, 50);
1386        assert_eq!(read_slabs(&b2), [(10, 20), (30, 50), (60, 70)]);
1387    }
1388
1389    #[test]
1390    fn insslab_into_air_gap_below_all_slabs() {
1391        let mut b2 = build_b2(&[(10, 20)]);
1392        insslab(&mut b2, 30, 50);
1393        assert_eq!(read_slabs(&b2), [(10, 20), (30, 50)]);
1394    }
1395
1396    #[test]
1397    fn insslab_extend_top_of_slab() {
1398        let mut b2 = build_b2(&[(50, 70)]);
1399        insslab(&mut b2, 30, 60);
1400        assert_eq!(read_slabs(&b2), [(30, 70)]);
1401    }
1402
1403    #[test]
1404    fn insslab_extend_bot_of_slab() {
1405        let mut b2 = build_b2(&[(50, 70)]);
1406        insslab(&mut b2, 60, 80);
1407        assert_eq!(read_slabs(&b2), [(50, 80)]);
1408    }
1409
1410    #[test]
1411    fn insslab_merge_into_last_slab_writes_sentinel() {
1412        // Repro for the multi-call set_spans / terrain bug: inserting
1413        // [105, 255) into a column that already has runs (100, 105)
1414        // and (255, MAXZDIM) should produce a single run (100,
1415        // MAXZDIM), with a clean sentinel at b2[2..]. Pre-fix this
1416        // left phantom values at b2[2..4] = (255, MAXZDIM) that
1417        // compilerle then re-emitted as overlapping garbage (column
1418        // collapsed to just the first voxel at z=100).
1419        // Built manually because `build_b2` rejects bot == MAXZDIM —
1420        // expandrle does produce that pattern (the bedrock slab at
1421        // z=255 lands as the last run with bot=MAXZDIM).
1422        let mut b2: Vec<i32> = vec![100, 105, 255, MAXZDIM, MAXZDIM, MAXZDIM];
1423        b2.resize(b2.len() + 32, 0); // slack
1424        insslab(&mut b2, 105, 255);
1425        // After: single run from z=100 to the bedrock-equivalent
1426        // sentinel. read_slabs returns until b2[i+1] < MAXZDIM.
1427        assert_eq!(b2[0], 100);
1428        assert!(
1429            b2[1] >= MAXZDIM,
1430            "expected merged run to extend to MAXZDIM, got b2[1] = {}",
1431            b2[1]
1432        );
1433        // The phantom (255, MAXZDIM) slot must NOT still be there —
1434        // sentinel-stamping in the merge branch fix.
1435        assert!(
1436            b2[2] >= MAXZDIM,
1437            "b2[2] should be sentinel, got {} (pre-fix this was 255 from the un-shifted phantom slab)",
1438            b2[2]
1439        );
1440    }
1441
1442    #[test]
1443    fn insslab_touch_top_merges() {
1444        // y1 == top of slab → adjacent insert merges (extends top).
1445        let mut b2 = build_b2(&[(50, 70)]);
1446        insslab(&mut b2, 30, 50);
1447        assert_eq!(read_slabs(&b2), [(30, 70)]);
1448    }
1449
1450    #[test]
1451    fn insslab_touch_bot_merges() {
1452        // y0 == bot of slab → adjacent insert merges (extends bot).
1453        let mut b2 = build_b2(&[(50, 70)]);
1454        insslab(&mut b2, 70, 80);
1455        assert_eq!(read_slabs(&b2), [(50, 80)]);
1456    }
1457
1458    #[test]
1459    fn insslab_merge_two_slabs() {
1460        let mut b2 = build_b2(&[(10, 30), (50, 70)]);
1461        insslab(&mut b2, 20, 60);
1462        assert_eq!(read_slabs(&b2), [(10, 70)]);
1463    }
1464
1465    #[test]
1466    fn insslab_engulf_inner_slabs() {
1467        let mut b2 = build_b2(&[(10, 20), (30, 40), (50, 60)]);
1468        insslab(&mut b2, 5, 70);
1469        assert_eq!(read_slabs(&b2), [(5, 70)]);
1470    }
1471
1472    #[test]
1473    fn insslab_engulf_then_keep_lower() {
1474        let mut b2 = build_b2(&[(10, 20), (30, 40), (60, 80)]);
1475        insslab(&mut b2, 5, 50);
1476        assert_eq!(read_slabs(&b2), [(5, 50), (60, 80)]);
1477    }
1478
1479    #[test]
1480    fn insslab_engulf_then_merge_lower() {
1481        let mut b2 = build_b2(&[(10, 20), (30, 40), (60, 80)]);
1482        insslab(&mut b2, 5, 60);
1483        assert_eq!(read_slabs(&b2), [(5, 80)]);
1484    }
1485
1486    #[test]
1487    fn insslab_chain_of_touching_inserts() {
1488        let mut b2 = build_b2(&[]);
1489        insslab(&mut b2, 10, 20);
1490        insslab(&mut b2, 20, 30);
1491        insslab(&mut b2, 30, 40);
1492        assert_eq!(read_slabs(&b2), [(10, 40)]);
1493    }
1494
1495    #[test]
1496    fn insslab_carve_then_insert_round_trip() {
1497        // Land on slab, carve the middle, fill it back: end result
1498        // is identical to the original.
1499        let original = [(10, 50)];
1500        let mut b2 = build_b2(&original);
1501        delslab(&mut b2, 20, 30);
1502        assert_eq!(read_slabs(&b2), [(10, 20), (30, 50)]);
1503        insslab(&mut b2, 20, 30);
1504        assert_eq!(read_slabs(&b2), original);
1505    }
1506
1507    #[test]
1508    fn insslab_into_sentinel_only_buffer_with_z_advance() {
1509        // Insert below an existing slab — z advances past slab[0].
1510        let mut b2 = build_b2(&[(10, 20)]);
1511        insslab(&mut b2, 100, 150);
1512        assert_eq!(read_slabs(&b2), [(10, 20), (100, 150)]);
1513    }
1514
1515    // ---- expandrle (CD.2.3) ------------------------------------------
1516
1517    /// Strip the `[top, bot, top, bot, ..., MAXZDIM]` decoded shape
1518    /// off a `b2` produced by [`expandrle`]. Last bot is the sentinel
1519    /// (== MAXZDIM); preceding pairs are the solid runs.
1520    fn read_uind(uind: &[i32]) -> Vec<(i32, i32)> {
1521        let mut out = Vec::new();
1522        let mut i = 0;
1523        while uind[i + 1] < MAXZDIM {
1524            out.push((uind[i], uind[i + 1]));
1525            i += 2;
1526        }
1527        // Last solid run terminated by sentinel.
1528        out.push((uind[i], uind[i + 1]));
1529        out
1530    }
1531
1532    #[test]
1533    fn expandrle_single_slab_fully_solid_column() {
1534        // Voxlap encoding for solid [0, MAXZDIM) — the on-disk fixture
1535        // we see for "ground all the way down" columns.
1536        // [nextptr=0, z1=0, z1c=MAXZDIM-1, z0=0] + MAXZDIM × 4 colours.
1537        let z1c = u8::try_from(MAXZDIM - 1).expect("MAXZDIM-1 fits in u8");
1538        let mut slab = vec![0u8, 0, z1c, 0];
1539        slab.extend(std::iter::repeat_n(0u8, (MAXZDIM as usize) * 4));
1540        let mut uind = vec![0i32; 16];
1541        expandrle(&slab, &mut uind);
1542        // One solid run from z=0 to MAXZDIM, followed by sentinel.
1543        assert_eq!(uind[0], 0);
1544        assert_eq!(uind[1], MAXZDIM);
1545    }
1546
1547    #[test]
1548    fn expandrle_single_slab_partial_floor() {
1549        // [nextptr=0, z1=64, z1c=66, z0=0] + 3 colours (don't matter).
1550        // Solid run = [64, MAXZDIM) — voxlap treats below-floor as
1551        // implicit solid.
1552        let slab = [0u8, 64, 66, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0];
1553        let mut uind = vec![0i32; 16];
1554        expandrle(&slab, &mut uind);
1555        assert_eq!(uind[0], 64);
1556        assert_eq!(uind[1], MAXZDIM);
1557    }
1558
1559    #[test]
1560    fn expandrle_two_slabs_with_cave() {
1561        // Slab 0: nextptr=2 (advance 8 bytes), z1=10, z1c=12, z0=0.
1562        // Floor list: 3 colours = 12 bytes; total slab 0 = 4 + 12 - 4 = 12?
1563        // Actually slab 0 size = nextptr * 4 = 8 bytes. So with floor
1564        // list shorter than (z1c - z1 + 1) = 3 colours = 12 bytes, size
1565        // would be 16. To fit nextptr=2 (= 8 bytes), z1c-z1+1 must be 1
1566        // colour = 4 bytes (8 = 4 header + 4 colour).
1567        //
1568        // Layout: [2, 10, 10, 0, c0, c0, c0, c0, 0, 50, 52, 30, ...].
1569        // Slab 0: 1-voxel floor, then implicit solid [11, 30) (between
1570        // slab 0 floor end and slab 1 ceiling top).
1571        // Slab 1: ceiling [30, 50), floor [50, 53), implicit below.
1572        //
1573        // expandrle output:
1574        //   uind[0] = v[1]_s0 = 10
1575        //   advance to slab 1; v[3]=30 < v[1]=50 → write
1576        //   uind[1] = 30, uind[2] = 50, i = 4
1577        //   slab 1 nextptr=0 → exit loop
1578        //   uind[3] = MAXZDIM
1579        let slab = [
1580            2u8, 10, 10, 0, // slab 0 header
1581            0xaa, 0, 0, 0, // slab 0 1-voxel floor colour
1582            0, 50, 52, 30, // slab 1 (last) header
1583            0xbb, 0, 0, 0, // slab 1 floor 0
1584            0xcc, 0, 0, 0, // slab 1 floor 1
1585            0xdd, 0, 0, 0, // slab 1 floor 2
1586        ];
1587        let mut uind = vec![0i32; 16];
1588        expandrle(&slab, &mut uind);
1589        assert_eq!(uind[0], 10);
1590        assert_eq!(uind[1], 30);
1591        assert_eq!(uind[2], 50);
1592        assert_eq!(uind[3], MAXZDIM);
1593    }
1594
1595    #[test]
1596    fn expandrle_skips_degenerate_slab_with_no_ceiling_gap() {
1597        // Slab 1 has v[3] >= v[1]: ceiling collapses with floor → no
1598        // air gap above. Voxlap's `continue` skips emitting a new
1599        // solid run for this slab, merging it with the previous run.
1600        //
1601        // Layout:
1602        //   slab 0: nextptr=2, z1=10, z1c=10, z0=0, 1 floor colour
1603        //   slab 1: nextptr=0, z1=20, z1c=22, z0=20 (z0 == z1 → skip)
1604        let slab = [
1605            2u8, 10, 10, 0, // slab 0 header
1606            0xaa, 0, 0, 0, // 1 floor colour
1607            0, 20, 22, 20, // slab 1 (degenerate: z0=z1=20)
1608            0xbb, 0, 0, 0, 0xcc, 0, 0, 0, 0xdd, 0, 0, 0, // floor colours
1609        ];
1610        let mut uind = vec![0i32; 16];
1611        expandrle(&slab, &mut uind);
1612        // Only one solid run (slab 0's), since slab 1 was skipped.
1613        assert_eq!(uind[0], 10);
1614        assert_eq!(uind[1], MAXZDIM);
1615    }
1616
1617    #[test]
1618    fn expandrle_round_trips_through_b2_helpers() {
1619        // Decode a 2-slab column, then verify delslab can carve a
1620        // hole into the air gap (the `read_uind` shape matches the b2
1621        // shape that delslab/insslab consume).
1622        let slab = [
1623            2u8, 10, 10, 0, 0xaa, 0, 0, 0, 0, 50, 52, 30, 0xbb, 0, 0, 0, 0xcc, 0, 0, 0, 0xdd, 0, 0,
1624            0,
1625        ];
1626        let mut uind = vec![0i32; 16];
1627        expandrle(&slab, &mut uind);
1628        let runs = read_uind(&uind[..4]);
1629        assert_eq!(runs, [(10, 30), (50, MAXZDIM)]);
1630    }
1631
1632    // ---- build_color_table + compilerle (CD.2.4) ----------------------
1633
1634    /// Build a sentinel-terminated all-air b2 buffer for a neighbor.
1635    /// Sized with slack so compilerle's index walks don't run off
1636    /// the end (worst case = n0's voxel count + 1).
1637    fn all_air_neighbor() -> Vec<i32> {
1638        // Just the sentinel pair + slack.
1639        let mut buf = vec![MAXZDIM, MAXZDIM];
1640        buf.resize(buf.len() + MAXZDIM as usize, MAXZDIM);
1641        buf
1642    }
1643
1644    /// Build a sentinel-terminated b2 from a list of solid runs.
1645    /// Compatible with [`compilerle`]'s input shape — the trailing
1646    /// sentinel must come last with bot == MAXZDIM.
1647    fn b2_from_runs(runs: &[(i32, i32)]) -> Vec<i32> {
1648        let mut buf = Vec::new();
1649        for &(top, bot) in runs {
1650            buf.push(top);
1651            buf.push(bot);
1652        }
1653        buf.push(MAXZDIM);
1654        buf.push(MAXZDIM);
1655        buf.resize(buf.len() + MAXZDIM as usize, MAXZDIM);
1656        buf
1657    }
1658
1659    #[test]
1660    fn build_color_table_single_slab_one_floor_voxel() {
1661        // [nextptr=0, z1=10, z1c=10, z0=0] + 1 colour.
1662        let slab = [0u8, 10, 10, 0, 0xa1, 0xa2, 0xa3, 0xa4];
1663        let table = build_color_table(&slab);
1664        assert_eq!(table.len(), 2);
1665        assert_eq!(table[0].z_start, 10);
1666        assert_eq!(table[0].z_end, 11);
1667        assert_eq!(table[0].colors, &[0xa1, 0xa2, 0xa3, 0xa4]);
1668        // Sentinel.
1669        assert_eq!(table[1].z_start, MAXZDIM);
1670        assert_eq!(table[1].z_end, MAXZDIM);
1671    }
1672
1673    #[test]
1674    fn build_color_table_two_slabs_with_ceiling() {
1675        // Slab 0: nextptr=4 (16 bytes total) — 4 hdr + 1 floor + 8 ceiling-of-slab1
1676        //   Layout: [4, 10, 10, 0, F0,F0,F0,F0, C0,C0,C0,C0, C1,C1,C1,C1]
1677        // Slab 1: [0, 50, 52, 30, ...]
1678        //   Slab 1's ceiling list = 2 voxels at z=28..30 (stored in slab 0's tail).
1679        let slab = [
1680            4u8, 10, 10, 0, // slab 0 header
1681            0xf0, 0xf0, 0xf0, 0xf0, // 1 floor color
1682            0xc0, 0xc0, 0xc0, 0xc0, // ceiling color for z=28
1683            0xc1, 0xc1, 0xc1, 0xc1, // ceiling color for z=29
1684            0u8, 50, 52, 30, // slab 1 header
1685            0xfa, 0xfa, 0xfa, 0xfa, // floor z=50
1686            0xfb, 0xfb, 0xfb, 0xfb, // floor z=51
1687            0xfc, 0xfc, 0xfc, 0xfc, // floor z=52
1688        ];
1689        let table = build_color_table(&slab);
1690        assert_eq!(table.len(), 4);
1691        // Slab 0 floor.
1692        assert_eq!(table[0].z_start, 10);
1693        assert_eq!(table[0].z_end, 11);
1694        assert_eq!(table[0].colors.len(), 4);
1695        // Slab 1 ceiling — 2 voxels at z=28..30.
1696        assert_eq!(table[1].z_start, 28);
1697        assert_eq!(table[1].z_end, 30);
1698        assert_eq!(
1699            table[1].colors,
1700            &[0xc0, 0xc0, 0xc0, 0xc0, 0xc1, 0xc1, 0xc1, 0xc1]
1701        );
1702        // Slab 1 floor.
1703        assert_eq!(table[2].z_start, 50);
1704        assert_eq!(table[2].z_end, 53);
1705        assert_eq!(table[2].colors.len(), 12);
1706        // Sentinel.
1707        assert_eq!(table[3].z_start, MAXZDIM);
1708    }
1709
1710    #[test]
1711    fn compilerle_round_trip_single_slab_solid_to_maxzdim() {
1712        // Original column: solid from z=10 to MAXZDIM, full floor
1713        // color list. compilerle with all-air neighbors should
1714        // re-encode bit-equivalently in b2 shape.
1715        let mut slab = vec![0u8, 10, (MAXZDIM - 1) as u8, 0];
1716        for z in 10..MAXZDIM {
1717            // Distinct color per z so we can verify exact output bytes.
1718            slab.extend_from_slice(&[z as u8, (z + 1) as u8, (z + 2) as u8, 0]);
1719        }
1720
1721        // Decode to b2.
1722        let mut b2 = vec![0i32; (MAXZDIM as usize) + 4];
1723        expandrle(&slab, &mut b2);
1724        assert_eq!(b2[0], 10);
1725        assert_eq!(b2[1], MAXZDIM);
1726
1727        // Re-encode with all-air neighbors → no buried-voxel skip.
1728        let n_air = all_air_neighbor();
1729        let mut cbuf = vec![0u8; 1028];
1730        let mut colfunc_called = 0;
1731        let mut colfunc = |_x: i32, _y: i32, _z: i32| -> i32 {
1732            colfunc_called += 1;
1733            0
1734        };
1735        let written = compilerle(
1736            &b2,
1737            &n_air,
1738            &n_air,
1739            &n_air,
1740            &n_air,
1741            &mut cbuf,
1742            &slab,
1743            0,
1744            0,
1745            &mut colfunc,
1746        );
1747        assert_eq!(colfunc_called, 0, "all colors should come from tbuf2");
1748
1749        // Output should match the input slab byte-for-byte (it's
1750        // already the minimal full-floor encoding).
1751        assert_eq!(written, slab.len());
1752        assert_eq!(&cbuf[..written], &slab[..]);
1753
1754        // And expandrle on the output reproduces the same b2.
1755        let mut b2_round = vec![0i32; (MAXZDIM as usize) + 4];
1756        expandrle(&cbuf[..written], &mut b2_round);
1757        assert_eq!(b2_round[0], 10);
1758        assert_eq!(b2_round[1], MAXZDIM);
1759    }
1760
1761    #[test]
1762    fn compilerle_round_trip_two_solid_runs_with_cave() {
1763        // b2 = [10, 30, 50, MAXZDIM] — one cave between two solid runs.
1764        // Build a synthetic original column that has full floor color
1765        // lists for both slabs; ceiling list for slab 1 is non-empty.
1766        // For simplicity construct via compilerle from an all-air-
1767        // neighbor first encode of the desired b2, then round-trip.
1768
1769        // Step 1: build a SEED column. We'll compilerle from a
1770        // hand-rolled "all is colfunc" variant — colfunc returns z as
1771        // its color, deterministic.
1772        let dummy = vec![0u8, 0, (MAXZDIM - 1) as u8, 0];
1773        let mut dummy_full = dummy;
1774        dummy_full.extend(std::iter::repeat_n(0u8, (MAXZDIM as usize) * 4));
1775
1776        let n_air = all_air_neighbor();
1777        let b2 = b2_from_runs(&[(10, 30), (50, MAXZDIM)]);
1778        let mut seed = vec![0u8; 1028];
1779        let mut colfunc = |_x: i32, _y: i32, z: i32| -> i32 { z };
1780        let seed_len = compilerle(
1781            &b2,
1782            &n_air,
1783            &n_air,
1784            &n_air,
1785            &n_air,
1786            &mut seed,
1787            &dummy_full,
1788            0,
1789            0,
1790            &mut colfunc,
1791        );
1792        seed.truncate(seed_len);
1793
1794        // Step 2: decode the seed back to b2.
1795        let mut b2_round = vec![0i32; (MAXZDIM as usize) + 4];
1796        expandrle(&seed, &mut b2_round);
1797        // Two solid runs followed by sentinel.
1798        assert_eq!(b2_round[0], 10);
1799        assert_eq!(b2_round[1], 30);
1800        assert_eq!(b2_round[2], 50);
1801        assert_eq!(b2_round[3], MAXZDIM);
1802
1803        // Step 3: compilerle again using the seed as the original
1804        // column — should produce byte-identical output (idempotent
1805        // round trip).
1806        let mut cbuf = vec![0u8; 1028];
1807        let mut never_called = 0;
1808        let mut colfunc2 = |_x: i32, _y: i32, _z: i32| -> i32 {
1809            never_called += 1;
1810            0
1811        };
1812        let written = compilerle(
1813            &b2,
1814            &n_air,
1815            &n_air,
1816            &n_air,
1817            &n_air,
1818            &mut cbuf,
1819            &seed,
1820            0,
1821            0,
1822            &mut colfunc2,
1823        );
1824        assert_eq!(never_called, 0, "second pass needs no colfunc");
1825        assert_eq!(written, seed_len);
1826        assert_eq!(&cbuf[..written], &seed[..]);
1827    }
1828
1829    #[test]
1830    fn compilerle_buried_voxel_optimization_with_all_solid_neighbors() {
1831        // All 4 neighbors solid means every voxel below the top one is
1832        // buried — compilerle's dacnt path closes the floor list right
1833        // after writing the first exposed voxel.
1834
1835        // Self column: solid [10, MAXZDIM). Voxlap's b2 convention has
1836        // the last real solid run extending to MAXZDIM (solid below is
1837        // implicit), so we never transition into the sentinel slab.
1838        let b2 = b2_from_runs(&[(10, MAXZDIM)]);
1839        let n_solid = b2_from_runs(&[(0, MAXZDIM)]);
1840        // Original column over-encoded with a full floor color list so
1841        // colfunc is never needed (verifies tbuf2 lookup for buried
1842        // voxels we'd otherwise skip).
1843        let mut slab = vec![0u8, 10, (MAXZDIM - 1) as u8, 0];
1844        for z in 10..MAXZDIM {
1845            slab.extend_from_slice(&[z as u8, 0, 0, 0]);
1846        }
1847        let mut cbuf = vec![0u8; 1028];
1848        let mut colfunc_called = 0;
1849        let mut colfunc = |_x: i32, _y: i32, _z: i32| -> i32 {
1850            colfunc_called += 1;
1851            0
1852        };
1853        let written = compilerle(
1854            &b2,
1855            &n_solid,
1856            &n_solid,
1857            &n_solid,
1858            &n_solid,
1859            &mut cbuf,
1860            &slab,
1861            0,
1862            0,
1863            &mut colfunc,
1864        );
1865        assert_eq!(colfunc_called, 0, "tbuf2 should cover every voxel");
1866        // Compressed output: only the top voxel exposed (z=10) →
1867        // 4-byte header + 1 color = 8 bytes.
1868        assert_eq!(written, 8);
1869        assert_eq!(cbuf[0], 0); // terminator nextptr
1870        assert_eq!(cbuf[1], 10); // z1
1871        assert_eq!(cbuf[2], 10); // z1c (only one exposed voxel)
1872        assert_eq!(cbuf[3], 0); // z0 dummy
1873                                // expandrle on output reproduces the b2 shape (still solid
1874                                // from z=10 onward, despite the compressed encoding).
1875        let mut b2_round = vec![0i32; (MAXZDIM as usize) + 4];
1876        expandrle(&cbuf[..written], &mut b2_round);
1877        assert_eq!(b2_round[0], 10);
1878        assert_eq!(b2_round[1], MAXZDIM);
1879    }
1880
1881    // ---- ScumCtx (CD.2.5) ---------------------------------------------
1882
1883    /// Build a 1×1 Vxl with a single fully-solid column. Minimal slab
1884    /// encoding: 1 floor color = 8 bytes total.
1885    fn build_1x1_min_solid_vxl() -> Vxl {
1886        let column = vec![0u8, 0, 0, 0, 0xff, 0x80, 0x40, 0x20];
1887        let column_offset = vec![0u32, column.len() as u32].into_boxed_slice();
1888        Vxl {
1889            vsid: 1,
1890            ipo: [0.0; 3],
1891            ist: [1.0, 0.0, 0.0],
1892            ihe: [0.0, 0.0, 1.0],
1893            ifo: [0.0, 1.0, 0.0],
1894            data: column.into_boxed_slice(),
1895            column_offset,
1896            mip_base_offsets: Box::new([0, 2]),
1897            vbit: Box::new([]),
1898            vbiti: 0,
1899        }
1900    }
1901
1902    #[test]
1903    fn scum2_no_edit_round_trip_1x1_minimal_column() {
1904        // Open a batch on a minimal-encoded 1×1 column, run scum2 +
1905        // finish without mutating, verify the column's b2 shape is
1906        // preserved.
1907        let mut vxl = build_1x1_min_solid_vxl();
1908        vxl.reserve_edit_capacity(4096);
1909
1910        let mut ctx = ScumCtx::new(&mut vxl);
1911        let _b2 = ctx.scum2(0, 0).expect("column 0,0 in bounds");
1912        ctx.finish();
1913
1914        let column = vxl.column_data(0);
1915        let mut b2_after = vec![0i32; SCPITCH];
1916        expandrle(column, &mut b2_after);
1917        assert_eq!(b2_after[0], 0);
1918        assert_eq!(b2_after[1], MAXZDIM);
1919    }
1920
1921    #[test]
1922    fn scum2_carve_edit_1x1_creates_air_gap() {
1923        // Carve a hole in a fully-solid column; verify the post-edit
1924        // b2 reflects the carve.
1925        let mut vxl = build_1x1_min_solid_vxl();
1926        vxl.reserve_edit_capacity(4096);
1927
1928        let mut ctx = ScumCtx::new(&mut vxl);
1929        ctx.set_colfunc(|_x, _y, _z| 0x80_60_40_20u32 as i32);
1930        {
1931            let b2 = ctx.scum2(0, 0).expect("column 0,0 in bounds");
1932            // Carve [50, 100) to air.
1933            delslab(b2, 50, 100);
1934        }
1935        ctx.finish();
1936
1937        let column = vxl.column_data(0);
1938        let mut b2_after = vec![0i32; SCPITCH];
1939        expandrle(column, &mut b2_after);
1940        // Two solid runs now: [0, 50) and [100, MAXZDIM).
1941        assert_eq!(b2_after[0], 0);
1942        assert_eq!(b2_after[1], 50);
1943        assert_eq!(b2_after[2], 100);
1944        assert_eq!(b2_after[3], MAXZDIM);
1945    }
1946
1947    /// Build a 4×4 Vxl with all 16 columns sharing the same minimal
1948    /// fully-solid encoding. Useful for testing batch edits.
1949    fn build_4x4_min_solid_vxl() -> Vxl {
1950        const COL: [u8; 8] = [0, 0, 0, 0, 0xff, 0x80, 0x40, 0x20];
1951        let mut data = Vec::with_capacity(16 * 8);
1952        let mut offsets = Vec::with_capacity(17);
1953        for i in 0..16 {
1954            offsets.push((i * 8) as u32);
1955            data.extend_from_slice(&COL);
1956        }
1957        offsets.push((16 * 8) as u32);
1958        Vxl {
1959            vsid: 4,
1960            ipo: [0.0; 3],
1961            ist: [1.0, 0.0, 0.0],
1962            ihe: [0.0, 0.0, 1.0],
1963            ifo: [0.0, 1.0, 0.0],
1964            data: data.into_boxed_slice(),
1965            column_offset: offsets.into_boxed_slice(),
1966            mip_base_offsets: Box::new([0, 17]),
1967            vbit: Box::new([]),
1968            vbiti: 0,
1969        }
1970    }
1971
1972    #[test]
1973    fn scum2_batch_edits_multiple_columns_same_row() {
1974        // Edit columns (1, 2) and (2, 2) — same y row. Both should
1975        // get the same carve.
1976        let mut vxl = build_4x4_min_solid_vxl();
1977        vxl.reserve_edit_capacity(8192);
1978
1979        let mut ctx = ScumCtx::new(&mut vxl);
1980        ctx.set_colfunc(|_x, _y, _z| 0);
1981        {
1982            let b2 = ctx.scum2(1, 2).unwrap();
1983            delslab(b2, 50, 100);
1984        }
1985        {
1986            let b2 = ctx.scum2(2, 2).unwrap();
1987            delslab(b2, 50, 100);
1988        }
1989        ctx.finish();
1990
1991        for x in [1, 2] {
1992            let idx = 2 * 4 + x;
1993            let mut b2_after = vec![0i32; SCPITCH];
1994            expandrle(vxl.column_data(idx), &mut b2_after);
1995            assert_eq!(b2_after[0], 0);
1996            assert_eq!(b2_after[1], 50);
1997            assert_eq!(b2_after[2], 100);
1998            assert_eq!(b2_after[3], MAXZDIM);
1999        }
2000        // Untouched columns retain their original b2.
2001        for x in [0, 3] {
2002            let idx = 2 * 4 + x;
2003            let mut b2_after = vec![0i32; SCPITCH];
2004            expandrle(vxl.column_data(idx), &mut b2_after);
2005            assert_eq!(b2_after[0], 0);
2006            assert_eq!(b2_after[1], MAXZDIM);
2007        }
2008    }
2009
2010    #[test]
2011    fn scum2_batch_edits_across_rows() {
2012        // Edit column (1, 1) then column (1, 2) — y advances, prior
2013        // row gets flushed automatically.
2014        let mut vxl = build_4x4_min_solid_vxl();
2015        vxl.reserve_edit_capacity(8192);
2016
2017        let mut ctx = ScumCtx::new(&mut vxl);
2018        ctx.set_colfunc(|_x, _y, _z| 0);
2019        {
2020            let b2 = ctx.scum2(1, 1).unwrap();
2021            delslab(b2, 60, 80);
2022        }
2023        {
2024            let b2 = ctx.scum2(1, 2).unwrap();
2025            delslab(b2, 60, 80);
2026        }
2027        ctx.finish();
2028
2029        for y in [1, 2] {
2030            let idx = y * 4 + 1;
2031            let mut b2_after = vec![0i32; SCPITCH];
2032            expandrle(vxl.column_data(idx), &mut b2_after);
2033            assert_eq!(b2_after[0], 0);
2034            assert_eq!(b2_after[1], 60);
2035            assert_eq!(b2_after[2], 80);
2036            assert_eq!(b2_after[3], MAXZDIM);
2037        }
2038    }
2039
2040    #[test]
2041    fn scum2_finish_without_any_edit_is_noop() {
2042        // Begin then immediately finish without any scum2 call.
2043        let mut vxl = build_1x1_min_solid_vxl();
2044        vxl.reserve_edit_capacity(4096);
2045        let original = vxl.column_data(0).to_vec();
2046        let ctx = ScumCtx::new(&mut vxl);
2047        ctx.finish();
2048        assert_eq!(vxl.column_data(0), &original[..]);
2049    }
2050
2051    #[test]
2052    fn scum2_returns_none_for_out_of_bounds() {
2053        let mut vxl = build_1x1_min_solid_vxl();
2054        vxl.reserve_edit_capacity(4096);
2055        let mut ctx = ScumCtx::new(&mut vxl);
2056        assert!(ctx.scum2(-1, 0).is_none());
2057        assert!(ctx.scum2(0, -1).is_none());
2058        assert!(ctx.scum2(1, 0).is_none());
2059        assert!(ctx.scum2(0, 1).is_none());
2060        ctx.finish();
2061    }
2062
2063    // ---- set_spans (CD.3) --------------------------------------------
2064
2065    #[test]
2066    fn set_spans_empty_is_noop() {
2067        let mut vxl = build_1x1_min_solid_vxl();
2068        let original = vxl.column_data(0).to_vec();
2069        set_spans(&mut vxl, &[], None);
2070        // Nothing should have changed — and we never reserved edit
2071        // capacity, so this also tests that empty input is fully
2072        // short-circuited (no ScumCtx::new + assert).
2073        assert_eq!(vxl.column_data(0), &original[..]);
2074    }
2075
2076    #[test]
2077    fn set_spans_single_carve_creates_air_gap() {
2078        let mut vxl = build_1x1_min_solid_vxl();
2079        vxl.reserve_edit_capacity(4096);
2080        set_spans(
2081            &mut vxl,
2082            &[Vspan {
2083                x: 0,
2084                y: 0,
2085                z0: 50,
2086                z1: 99,
2087            }],
2088            None,
2089        );
2090        let mut b2 = vec![0i32; SCPITCH];
2091        expandrle(vxl.column_data(0), &mut b2);
2092        // Half-open exclusive end is z1+1 = 100.
2093        assert_eq!(b2[0], 0);
2094        assert_eq!(b2[1], 50);
2095        assert_eq!(b2[2], 100);
2096        assert_eq!(b2[3], MAXZDIM);
2097    }
2098
2099    #[test]
2100    fn set_spans_multi_span_same_column_accumulates() {
2101        // Two non-overlapping carves on the same column. Voxlap's
2102        // setspans correctness relies on the with_column dedup —
2103        // re-calling scum2 between spans would wipe the first carve.
2104        let mut vxl = build_1x1_min_solid_vxl();
2105        vxl.reserve_edit_capacity(4096);
2106        set_spans(
2107            &mut vxl,
2108            &[
2109                Vspan {
2110                    x: 0,
2111                    y: 0,
2112                    z0: 30,
2113                    z1: 49,
2114                },
2115                Vspan {
2116                    x: 0,
2117                    y: 0,
2118                    z0: 100,
2119                    z1: 119,
2120                },
2121            ],
2122            None,
2123        );
2124        let mut b2 = vec![0i32; SCPITCH];
2125        expandrle(vxl.column_data(0), &mut b2);
2126        // Three solid runs: [0, 30), [50, 100), [120, MAXZDIM).
2127        assert_eq!(b2[0], 0);
2128        assert_eq!(b2[1], 30);
2129        assert_eq!(b2[2], 50);
2130        assert_eq!(b2[3], 100);
2131        assert_eq!(b2[4], 120);
2132        assert_eq!(b2[5], MAXZDIM);
2133    }
2134
2135    #[test]
2136    fn set_spans_insert_color_fills_air() {
2137        // Start with a column that's already partly carved, fill the
2138        // air gap back in with a known color.
2139        let mut vxl = build_1x1_min_solid_vxl();
2140        vxl.reserve_edit_capacity(4096);
2141        // First carve [50, 100) to create air.
2142        set_spans(
2143            &mut vxl,
2144            &[Vspan {
2145                x: 0,
2146                y: 0,
2147                z0: 50,
2148                z1: 99,
2149            }],
2150            None,
2151        );
2152        // Now fill [60, 80) back to solid with a known color.
2153        const FILL: u32 = 0x80_aa_bb_cc;
2154        set_spans(
2155            &mut vxl,
2156            &[Vspan {
2157                x: 0,
2158                y: 0,
2159                z0: 60,
2160                z1: 79,
2161            }],
2162            Some(FILL),
2163        );
2164        let mut b2 = vec![0i32; SCPITCH];
2165        expandrle(vxl.column_data(0), &mut b2);
2166        // Three solid runs: [0, 50), [60, 80), [100, MAXZDIM).
2167        assert_eq!(b2[0], 0);
2168        assert_eq!(b2[1], 50);
2169        assert_eq!(b2[2], 60);
2170        assert_eq!(b2[3], 80);
2171        assert_eq!(b2[4], 100);
2172        assert_eq!(b2[5], MAXZDIM);
2173    }
2174
2175    #[test]
2176    fn set_spans_skips_out_of_bounds_silently() {
2177        let mut vxl = build_1x1_min_solid_vxl();
2178        vxl.reserve_edit_capacity(4096);
2179        set_spans(
2180            &mut vxl,
2181            &[Vspan {
2182                x: 7,
2183                y: 9,
2184                z0: 50,
2185                z1: 99,
2186            }],
2187            None,
2188        );
2189        // Column (0,0) untouched — out-of-bounds span had no effect.
2190        let mut b2 = vec![0i32; SCPITCH];
2191        expandrle(vxl.column_data(0), &mut b2);
2192        assert_eq!(b2[0], 0);
2193        assert_eq!(b2[1], MAXZDIM);
2194    }
2195
2196    #[test]
2197    fn set_spans_with_colfunc_z_dependent_colour() {
2198        // Insert solid with a colour that depends on z. To make every
2199        // voxel in [60, 80) exposed (and thus needing a colfunc call
2200        // each), use a 4x4 world with neighbors carved to air at the
2201        // same z range. Center column (1, 1) is then surrounded by
2202        // air on all 4 sides at [60, 80), giving compilerle a full
2203        // floor list to fill via the closure.
2204        let mut vxl = build_4x4_min_solid_vxl();
2205        vxl.reserve_edit_capacity(8192);
2206        // Step 1: carve [50, 100) on every column so the insert sits
2207        // in air on every side.
2208        let carve_spans: Vec<Vspan> = (0..4)
2209            .flat_map(|y| {
2210                (0..4).map(move |x| Vspan {
2211                    x,
2212                    y,
2213                    z0: 50,
2214                    z1: 99,
2215                })
2216            })
2217            .collect();
2218        set_spans(&mut vxl, &carve_spans, None);
2219        // Step 2: insert [60, 80) on the center column with a
2220        // z-dependent colour.
2221        set_spans_with_colfunc(
2222            &mut vxl,
2223            &[Vspan {
2224                x: 1,
2225                y: 1,
2226                z0: 60,
2227                z1: 79,
2228            }],
2229            SpanOp::Insert,
2230            |_x, _y, z| (0x80ff_ff00u32 as i32) | z,
2231        );
2232        // Walk column (1,1) and find the slab with z1=60; verify each
2233        // floor colour matches the closure's output.
2234        let idx = 4 + 1; // y=1, x=1 in a 4-wide world
2235        let column = vxl.column_data(idx);
2236        let mut v = 0usize;
2237        let mut found = false;
2238        loop {
2239            let nextptr = column[v];
2240            let z1 = column[v + 1];
2241            if z1 == 60 {
2242                let z1c = column[v + 2];
2243                assert_eq!(z1c, 79, "z1c");
2244                let n_voxels = usize::from(z1c) - usize::from(z1) + 1;
2245                for i in 0..n_voxels {
2246                    let off = v + 4 + i * 4;
2247                    let c = u32::from_le_bytes([
2248                        column[off],
2249                        column[off + 1],
2250                        column[off + 2],
2251                        column[off + 3],
2252                    ]);
2253                    let z = u32::from(z1) + (i as u32);
2254                    assert_eq!(
2255                        c,
2256                        0x80ff_ff00 | z,
2257                        "z={z}: expected colour {:#010x}, got {:#010x}",
2258                        0x80ff_ff00 | z,
2259                        c
2260                    );
2261                }
2262                found = true;
2263                break;
2264            }
2265            if nextptr == 0 {
2266                break;
2267            }
2268            v += usize::from(nextptr) * 4;
2269        }
2270        assert!(found, "did not find a slab with z1=60");
2271    }
2272
2273    // ---- set_cube / set_rect / set_sphere (CD.4) ---------------------
2274
2275    #[test]
2276    fn set_cube_carves_single_voxel() {
2277        let mut vxl = build_4x4_min_solid_vxl();
2278        vxl.reserve_edit_capacity(4096);
2279        set_cube(&mut vxl, 1, 1, 100, None);
2280        let mut b2 = vec![0i32; SCPITCH];
2281        expandrle(vxl.column_data(4 + 1), &mut b2);
2282        // Solid runs: [0, 100), [101, MAXZDIM).
2283        assert_eq!(b2[0], 0);
2284        assert_eq!(b2[1], 100);
2285        assert_eq!(b2[2], 101);
2286        assert_eq!(b2[3], MAXZDIM);
2287    }
2288
2289    #[test]
2290    fn set_cube_skips_oob() {
2291        let mut vxl = build_4x4_min_solid_vxl();
2292        vxl.reserve_edit_capacity(4096);
2293        // Negative x, oversized y, z >= MAXZDIM, all silently no-op.
2294        set_cube(&mut vxl, -1, 1, 100, None);
2295        set_cube(&mut vxl, 5, 1, 100, None);
2296        set_cube(&mut vxl, 1, 1, 256, None);
2297        // Column (1, 1) untouched.
2298        let mut b2 = vec![0i32; SCPITCH];
2299        expandrle(vxl.column_data(4 + 1), &mut b2);
2300        assert_eq!(b2[0], 0);
2301        assert_eq!(b2[1], MAXZDIM);
2302    }
2303
2304    #[test]
2305    fn set_rect_carves_aabb() {
2306        let mut vxl = build_4x4_min_solid_vxl();
2307        vxl.reserve_edit_capacity(8192);
2308        // Carve a 2x2x50 box at (1..=2, 1..=2, 50..=99).
2309        set_rect(&mut vxl, [1, 1, 50], [2, 2, 99], None);
2310        for y in 1..=2 {
2311            for x in 1..=2 {
2312                let idx = (y * 4 + x) as usize;
2313                let mut b2 = vec![0i32; SCPITCH];
2314                expandrle(vxl.column_data(idx), &mut b2);
2315                assert_eq!(b2[0], 0, "col ({x},{y})");
2316                assert_eq!(b2[1], 50, "col ({x},{y})");
2317                assert_eq!(b2[2], 100, "col ({x},{y})");
2318                assert_eq!(b2[3], MAXZDIM, "col ({x},{y})");
2319            }
2320        }
2321        // Untouched corners still solid through the full column.
2322        for &(x, y) in &[(0, 0), (3, 3)] {
2323            let idx = (y * 4 + x) as usize;
2324            let mut b2 = vec![0i32; SCPITCH];
2325            expandrle(vxl.column_data(idx), &mut b2);
2326            assert_eq!(b2[0], 0);
2327            assert_eq!(b2[1], MAXZDIM);
2328        }
2329    }
2330
2331    #[test]
2332    fn set_rect_clamps_to_world() {
2333        let mut vxl = build_4x4_min_solid_vxl();
2334        vxl.reserve_edit_capacity(8192);
2335        // Box extends well past world bounds — clamps to [0, 3] in
2336        // each axis.
2337        set_rect(&mut vxl, [-10, -10, -10], [100, 100, 1000], None);
2338        // Every column carved over [0, MAXZDIM) → all-air.
2339        for idx in 0..16 {
2340            let mut b2 = vec![0i32; SCPITCH];
2341            expandrle(vxl.column_data(idx), &mut b2);
2342            // delslab clamps z1 to MAXZDIM-1, leaving voxel at
2343            // z=MAXZDIM-1 solid. The b2 reflects this: solid run
2344            // [255, MAXZDIM) only.
2345            assert_eq!(b2[0], 255, "col {idx}");
2346            assert_eq!(b2[1], MAXZDIM, "col {idx}");
2347        }
2348    }
2349
2350    #[test]
2351    fn set_sphere_carves_centred_sphere() {
2352        // 4x4 world; sphere radius 1 carves a "+" pattern at z=128
2353        // (center voxel + 4 axis-adjacent + 2 z-axis voxels).
2354        let mut vxl = build_4x4_min_solid_vxl();
2355        vxl.reserve_edit_capacity(8192);
2356        set_sphere(&mut vxl, [1, 1, 128], 1, None);
2357        // Voxels carved at: (1,1,127), (1,1,128), (1,1,129) [z axis],
2358        // (0,1,128), (2,1,128) [x axis], (1,0,128), (1,2,128) [y axis].
2359        // Center column (1,1) has z range [127, 130) carved.
2360        let mut b2 = vec![0i32; SCPITCH];
2361        expandrle(vxl.column_data(4 + 1), &mut b2);
2362        assert_eq!(b2[0], 0);
2363        assert_eq!(b2[1], 127);
2364        assert_eq!(b2[2], 130);
2365        assert_eq!(b2[3], MAXZDIM);
2366        // Adjacent column (0, 1) has only z=128 carved.
2367        let mut b2 = vec![0i32; SCPITCH];
2368        expandrle(vxl.column_data(4), &mut b2);
2369        assert_eq!(b2[0], 0);
2370        assert_eq!(b2[1], 128);
2371        assert_eq!(b2[2], 129);
2372        assert_eq!(b2[3], MAXZDIM);
2373    }
2374
2375    #[test]
2376    fn set_sphere_radius_zero_is_single_voxel() {
2377        let mut vxl = build_4x4_min_solid_vxl();
2378        vxl.reserve_edit_capacity(4096);
2379        set_sphere(&mut vxl, [1, 1, 100], 0, None);
2380        // Same as set_cube — only (1, 1, 100) carved.
2381        let mut b2 = vec![0i32; SCPITCH];
2382        expandrle(vxl.column_data(4 + 1), &mut b2);
2383        assert_eq!(b2[0], 0);
2384        assert_eq!(b2[1], 100);
2385        assert_eq!(b2[2], 101);
2386        assert_eq!(b2[3], MAXZDIM);
2387    }
2388
2389    #[test]
2390    fn set_sphere_with_colfunc_position_dependent_color() {
2391        // Carve a cave first (so the inserted sphere has air on every
2392        // side, exposing its surface voxels), then insert a sphere
2393        // with a colfunc that returns z in the low byte. Verify
2394        // (a) b2 reflects the sphere shape and (b) the top exposed
2395        // voxel's colour is the colfunc output.
2396        //
2397        // Note: voxlap's compilerle stores colours only for EXPOSED
2398        // voxels (top of run + skip-forward landings); buried voxels
2399        // in the middle of a slab don't get colfunc-derived colours
2400        // recorded. So we can only verify colours for voxels at the
2401        // run boundary or near them.
2402        let mut vxl = build_4x4_min_solid_vxl();
2403        vxl.reserve_edit_capacity(8192);
2404        // Carve [50, 200) on every column.
2405        set_rect(&mut vxl, [0, 0, 50], [3, 3, 199], None);
2406        // Insert a sphere of radius 2 at (1, 1, 128). Center column
2407        // gets z=126..130 inclusive (5 voxels) inserted.
2408        set_sphere_with_colfunc(&mut vxl, [1, 1, 128], 2, SpanOp::Insert, |_, _, z| {
2409            (0x80ff_ff00u32 as i32) | z
2410        });
2411        // (a) b2 has the expected three solid runs.
2412        let mut b2 = vec![0i32; SCPITCH];
2413        expandrle(vxl.column_data(4 + 1), &mut b2);
2414        assert_eq!(b2[0], 0, "b2 first run top");
2415        assert_eq!(b2[1], 50, "b2 first run bot");
2416        assert_eq!(b2[2], 126, "b2 sphere run top");
2417        assert_eq!(b2[3], 131, "b2 sphere run bot");
2418        assert_eq!(b2[4], 200, "b2 third run top");
2419        assert_eq!(b2[5], MAXZDIM, "b2 third run bot");
2420
2421        // (b) top of the sphere (z=126) is exposed (air above from
2422        // the carve). Its colour is the FIRST byte of the slab's
2423        // floor list.
2424        let column = vxl.column_data(4 + 1).to_vec();
2425        let mut v = 0usize;
2426        let mut top_color = None;
2427        loop {
2428            let nextptr = column[v];
2429            let z1 = column[v + 1];
2430            if z1 == 126 {
2431                let off = v + 4;
2432                top_color = Some(u32::from_le_bytes([
2433                    column[off],
2434                    column[off + 1],
2435                    column[off + 2],
2436                    column[off + 3],
2437                ]));
2438                break;
2439            }
2440            if nextptr == 0 {
2441                break;
2442            }
2443            v += usize::from(nextptr) * 4;
2444        }
2445        assert_eq!(
2446            top_color,
2447            Some(0x80ff_ff7e),
2448            "exposed voxel at z=126 should have colfunc-derived colour"
2449        );
2450    }
2451
2452    #[test]
2453    fn set_spans_4x4_batch_carves_each_listed_column() {
2454        // Sorted (y, x) ascending; each column gets the same carve.
2455        let mut vxl = build_4x4_min_solid_vxl();
2456        vxl.reserve_edit_capacity(8192);
2457        let spans: Vec<Vspan> = (0..4)
2458            .flat_map(|y| {
2459                (0..4).map(move |x| Vspan {
2460                    x,
2461                    y,
2462                    z0: 50,
2463                    z1: 99,
2464                })
2465            })
2466            .collect();
2467        set_spans(&mut vxl, &spans, None);
2468        // Every column should have the [50, 100) carve.
2469        for idx in 0..16 {
2470            let mut b2 = vec![0i32; SCPITCH];
2471            expandrle(vxl.column_data(idx), &mut b2);
2472            assert_eq!(b2[0], 0, "col {idx}");
2473            assert_eq!(b2[1], 50, "col {idx}");
2474            assert_eq!(b2[2], 100, "col {idx}");
2475            assert_eq!(b2[3], MAXZDIM, "col {idx}");
2476        }
2477    }
2478}