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