Every review finding fixed (no non-blocking parking lot): - shell.rs: debug_assert the D-110 floor-range i8 invariant (H1); clarify the ground_offset fallback comment (T1) and the roof_z None-case (T3); clarify the sub-chunk clip test comment (T4); add a test for the elevated/no-ground-floor fallback (H2). - scale.rs: add CHUNKS_PER_BLOCK (= BLOCK_M/CHUNK_M) + compile-time asserts; used by shell.rs fill_chunk's sub_chunk bounds assert (T5). - gen_queue.rs: document why FillChunk's Vec<BuildingPropertyTag> needs no Box (large_enum_variant non-issue) (T2). clippy --all-targets -D warnings clean; 1572 lib tests pass (+1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
458 lines
20 KiB
Rust
458 lines
20 KiB
Rust
//! Building-shell derivation — the D-230 on-demand `FillChunk` derive phase (T-987).
|
||
//!
|
||
//! This is the second half of the two-phase fill model (D-230). The **plan phase**
|
||
//! (`GenerateSkeleton`, [`crate::atlas::skeleton_gen`]) produces the per-footprint
|
||
//! [`BuildingPropertyTag`]s and caches them on the body's `QuarterWorldState`. This
|
||
//! module is the **derive phase**: given those frozen tags, it derives the actual
|
||
//! `{Void | Wall | FloorSlab | Roof}` shell voxels for a single 64 m chunk, purely
|
||
//! and on demand (no cache read, no side effects — the caller pre-resolves the tags
|
||
//! into the work item, mirroring the rest of the generation queue, [`crate::atlas::gen_queue`]).
|
||
//!
|
||
//! ## What this layer is (and is not)
|
||
//!
|
||
//! - **Is:** the structural *shell* — the four materials named in D-230. The geometry
|
||
//! is pure rectangle-containment (is the tile inside a footprint?) + z-range lookup
|
||
//! (which floor / roof does this voxel-z belong to?) over the cached tags. No RNG;
|
||
//! the shell is fully determined by `(footprint, extent)`, which are themselves a
|
||
//! deterministic function of the seed (D-010).
|
||
//! - **Is not:** the *surface* material vocabulary (`WallMaterial`/`RoofForm`/
|
||
//! `StreetSurface`, D-235) — that is the `BuildingExteriorTag` visual grammar (T-988),
|
||
//! layered on top of this shell. Interstitial street / open-space fill (D-215) and the
|
||
//! rolling condition overlay (D-198, T-999) are likewise out of scope here; this layer
|
||
//! emits only the four shell materials.
|
||
//!
|
||
//! ## Scale + coordinates (D-243)
|
||
//!
|
||
//! A quarter is 512 m = 4×4 **blocks** (128 m) = 8×8 **chunks** (64 m). A 64 m chunk is
|
||
//! exactly one quadrant of a 128 m block, so every chunk lies wholly inside a single
|
||
//! block — and a [`BuildingPropertyTag`]'s footprint is block-confined (`TileRect` is
|
||
//! block-local, 0..128). Therefore the *only* tags that can touch a chunk are the
|
||
//! covering block's tags. The fill works in block-local tile space and subtracts the
|
||
//! sub-chunk origin to land in chunk-local space (0..64).
|
||
//!
|
||
//! ## Vertical origin (D-110)
|
||
//!
|
||
//! [`FloorExtent`] addresses floors building-relative (base floor bottom = 0), but a
|
||
//! chunk mixes buildings with different basement depths, so the shell is emitted in a
|
||
//! single **quarter-ground** frame: the ground floor (index 0) bottom sits at `z = 0`,
|
||
//! basements are negative, upper floors positive. This is the D-110 convention
|
||
//! ("the quarter's ground level is always 0").
|
||
//!
|
||
//! ## D-010 compliance
|
||
//!
|
||
//! All arithmetic is integer. `FilledChunk` stores voxels in a `BTreeMap` so iteration
|
||
//! order is deterministic; only non-`Void` voxels are stored — the shell is **sparse**,
|
||
//! holding wall / per-floor-slab / roof *surfaces* but never the interior air between
|
||
//! floors. The derive is `O(built surface)` integer work (rectangle-containment +
|
||
//! z-range), which is what keeps it within the D-230 `<5 ms`/chunk budget. (If a future
|
||
//! profile shows the `BTreeMap` inserts hot for pathologically dense towers, a
|
||
//! pre-sized dense column buffer is the drop-in optimisation — matching D-230's
|
||
//! flat-array time estimate — without changing this layer's contract.)
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::atlas::scale::{CHUNKS_PER_BLOCK, CHUNK_M, VOXELS_PER_CHUNK};
|
||
use crate::simulation::generator::{BuildingPropertyTag, TileRect};
|
||
|
||
/// One structural shell voxel material (D-230).
|
||
///
|
||
/// `Void` (interior air / open space) is the implicit default and is **never stored**
|
||
/// in [`FilledChunk`]; it exists in the vocabulary so the type system can name the
|
||
/// full four-way classification and so callers can match exhaustively.
|
||
///
|
||
/// Integer-discriminant, append-only (D-010). Surface materials (D-235 `WallMaterial`
|
||
/// etc.) are a separate axis layered on top by T-988 — do not fold them in here.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||
#[repr(u8)]
|
||
pub enum ShellVoxel {
|
||
/// Interior air / open space — the implicit default, never stored.
|
||
#[default]
|
||
Void = 0,
|
||
/// Vertical structural wall — a footprint-perimeter column voxel.
|
||
Wall = 1,
|
||
/// Horizontal floor slab — the base voxel of a floor's interior.
|
||
FloorSlab = 2,
|
||
/// Roof cap — the voxel layer immediately above the topmost floor.
|
||
Roof = 3,
|
||
}
|
||
|
||
/// Chunk-local voxel coordinate: `(x, y)` in `0..64`, `z` quarter-ground-relative.
|
||
///
|
||
/// `x`/`y` are chunk-local tile indices (D-243: a chunk is 64×64 voxels). `z` is the
|
||
/// D-110 quarter-ground frame (ground floor bottom = 0, basements negative), so it is
|
||
/// signed.
|
||
pub type ShellVoxelPos = (u8, u8, i32);
|
||
|
||
/// The derived shell of a single 64 m chunk — the D-230 `FillChunk` output (T-987).
|
||
///
|
||
/// Sparse: only non-[`ShellVoxel::Void`] voxels are present. `BTreeMap` keeps iteration
|
||
/// deterministic (D-010). Carries its own quarter-relative address so a consumer
|
||
/// (Phase 5 rendering) can place it without re-deriving the mapping.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||
pub struct FilledChunk {
|
||
/// Stable id of the quarter this chunk belongs to (D-194/D-230).
|
||
pub quarter_id: u64,
|
||
/// Block grid position within the quarter's 4×4 block grid (0..4, 0..4).
|
||
pub block_pos: (u8, u8),
|
||
/// Sub-chunk quadrant within the block (0..2, 0..2) — a 128 m block is 2×2 chunks.
|
||
pub sub_chunk: (u8, u8),
|
||
/// Non-`Void` shell voxels, keyed by chunk-local position (D-010 ordered).
|
||
pub voxels: BTreeMap<ShellVoxelPos, ShellVoxel>,
|
||
}
|
||
|
||
impl FilledChunk {
|
||
/// Quarter-local chunk index `(0..8, 0..8)`: `block * 2 + sub_chunk`.
|
||
pub fn chunk_in_quarter(&self) -> (u8, u8) {
|
||
(
|
||
self.block_pos.0 * 2 + self.sub_chunk.0,
|
||
self.block_pos.1 * 2 + self.sub_chunk.1,
|
||
)
|
||
}
|
||
|
||
/// Number of non-`Void` voxels in this chunk.
|
||
pub fn voxel_count(&self) -> usize {
|
||
self.voxels.len()
|
||
}
|
||
|
||
/// Material at a chunk-local position; `Void` if nothing was emitted there.
|
||
pub fn get(&self, x: u8, y: u8, z: i32) -> ShellVoxel {
|
||
self.voxels.get(&(x, y, z)).copied().unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
/// Derive the shell [`FilledChunk`] for the sub-chunk `sub_chunk` of block `block_pos`,
|
||
/// given that block's pre-resolved building tags (D-230 derive phase, T-987).
|
||
///
|
||
/// Pure: the output is a total deterministic function of the inputs (D-010). `block_tags`
|
||
/// is the covering block's `Vec<BuildingPropertyTag>` from the cached `QuarterWorldState`
|
||
/// — pre-resolved by the caller because the work executor is cache-free
|
||
/// ([`crate::atlas::gen_queue`]).
|
||
///
|
||
/// `quarter_id` is threaded through for addressing only.
|
||
pub fn fill_chunk(
|
||
quarter_id: u64,
|
||
block_pos: (u8, u8),
|
||
sub_chunk: (u8, u8),
|
||
block_tags: &[BuildingPropertyTag],
|
||
) -> FilledChunk {
|
||
debug_assert!(
|
||
(sub_chunk.0 as i32) < CHUNKS_PER_BLOCK && (sub_chunk.1 as i32) < CHUNKS_PER_BLOCK,
|
||
"sub_chunk {sub_chunk:?} outside the block's {CHUNKS_PER_BLOCK}×{CHUNKS_PER_BLOCK} chunk grid"
|
||
);
|
||
let mut voxels: BTreeMap<ShellVoxelPos, ShellVoxel> = BTreeMap::new();
|
||
|
||
// Block-local tile range covered by this 64 m sub-chunk quadrant.
|
||
let chunk_lo_x = sub_chunk.0 as i32 * CHUNK_M;
|
||
let chunk_lo_y = sub_chunk.1 as i32 * CHUNK_M;
|
||
let chunk_hi_x = chunk_lo_x + CHUNK_M; // exclusive
|
||
let chunk_hi_y = chunk_lo_y + CHUNK_M; // exclusive
|
||
|
||
for tag in block_tags {
|
||
shell_derive_into(
|
||
&mut voxels,
|
||
tag,
|
||
(chunk_lo_x, chunk_lo_y, chunk_hi_x, chunk_hi_y),
|
||
);
|
||
}
|
||
|
||
FilledChunk {
|
||
quarter_id,
|
||
block_pos,
|
||
sub_chunk,
|
||
voxels,
|
||
}
|
||
}
|
||
|
||
/// Emit one building's shell voxels into `voxels`, clipped to the chunk's block-local
|
||
/// tile window `(lo_x, lo_y, hi_x, hi_y)` (hi exclusive).
|
||
///
|
||
/// Rectangle-containment (footprint ∩ chunk) × z-range (per-floor voxel bands from the
|
||
/// [`FloorExtent`]), per D-230. Walls on the footprint perimeter for the full height,
|
||
/// floor slabs on interior tiles at each floor base, a roof cap above the top floor.
|
||
fn shell_derive_into(
|
||
voxels: &mut BTreeMap<ShellVoxelPos, ShellVoxel>,
|
||
tag: &BuildingPropertyTag,
|
||
window: (i32, i32, i32, i32),
|
||
) {
|
||
let (win_lo_x, win_lo_y, win_hi_x, win_hi_y) = window;
|
||
let footprint = &tag.footprint;
|
||
let extent = &tag.extent;
|
||
|
||
// D-110 floor indices must fit i8 so the top-floor comparison and the roof
|
||
// derivation below cannot wrap. The generator caps floor counts well under this;
|
||
// the assert pins the invariant so a future change can't silently drop the roof.
|
||
debug_assert!(
|
||
extent.base_floor as i16 + extent.floor_count as i16 - 1 <= i8::MAX as i16,
|
||
"building floor range exceeds i8 — roof derivation would wrap"
|
||
);
|
||
|
||
// Footprint block-local tile span (inclusive lo, exclusive hi).
|
||
let fp_lo_x = footprint.origin.0 as i32;
|
||
let fp_lo_y = footprint.origin.1 as i32;
|
||
let fp_hi_x = fp_lo_x + footprint.size.0.max(1) as i32;
|
||
let fp_hi_y = fp_lo_y + footprint.size.1.max(1) as i32;
|
||
|
||
// Intersect footprint with the chunk window — nothing to do if disjoint.
|
||
let lo_x = fp_lo_x.max(win_lo_x);
|
||
let lo_y = fp_lo_y.max(win_lo_y);
|
||
let hi_x = fp_hi_x.min(win_hi_x);
|
||
let hi_y = fp_hi_y.min(win_hi_y);
|
||
if lo_x >= hi_x || lo_y >= hi_y {
|
||
return;
|
||
}
|
||
|
||
// Quarter-ground z origin (D-110): subtract the ground floor's building-relative
|
||
// base so floor 0 bottom lands at z = 0. `FloorExtent` addresses floors relative to
|
||
// `base_floor` (whose bottom is always its own 0), so when a building has no floor 0
|
||
// (all-basement / all-elevated — unreachable from the generator today) the fallback
|
||
// of 0 applies no shift: the building-relative z passes through unchanged.
|
||
let ground_offset = extent
|
||
.voxel_range_for_floor(0)
|
||
.map(|(lo, _)| lo)
|
||
.unwrap_or(0);
|
||
|
||
let top_floor = (extent.base_floor as i16 + extent.floor_count as i16 - 1) as i8;
|
||
let mut roof_z: Option<i32> = None;
|
||
|
||
for f_offset in 0..extent.floor_count {
|
||
let floor_index = (extent.base_floor as i16 + f_offset as i16) as i8;
|
||
let Some((rel_lo, rel_hi)) = extent.voxel_range_for_floor(floor_index) else {
|
||
continue;
|
||
};
|
||
let floor_base_z = rel_lo - ground_offset;
|
||
let floor_top_z = rel_hi - ground_offset;
|
||
|
||
for tx in lo_x..hi_x {
|
||
for ty in lo_y..hi_y {
|
||
let perimeter = is_perimeter(footprint, tx, ty);
|
||
// Chunk-local coordinate (0..64).
|
||
let cx = (tx - win_lo_x) as u8;
|
||
let cy = (ty - win_lo_y) as u8;
|
||
for z in floor_base_z..=floor_top_z {
|
||
let material = if perimeter {
|
||
ShellVoxel::Wall
|
||
} else if z == floor_base_z {
|
||
ShellVoxel::FloorSlab
|
||
} else {
|
||
continue; // interior air → Void, not stored
|
||
};
|
||
voxels.insert((cx, cy, z), material);
|
||
}
|
||
}
|
||
}
|
||
|
||
if floor_index == top_floor {
|
||
roof_z = Some(floor_top_z + 1);
|
||
}
|
||
}
|
||
|
||
// Roof cap: one voxel layer above the topmost floor, over the full footprint.
|
||
// `roof_z` is `None` only if the top floor's `voxel_range_for_floor` returned `None`
|
||
// (impossible for a well-formed `FloorExtent`) — in that case no roof is emitted.
|
||
if let Some(rz) = roof_z {
|
||
for tx in lo_x..hi_x {
|
||
for ty in lo_y..hi_y {
|
||
let cx = (tx - win_lo_x) as u8;
|
||
let cy = (ty - win_lo_y) as u8;
|
||
voxels.insert((cx, cy, rz), ShellVoxel::Roof);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Whether block-local tile `(tx, ty)` is on the outer ring of `footprint`.
|
||
///
|
||
/// A 1-wide footprint is all perimeter (no interior); callers rely on that so such
|
||
/// buildings become solid wall columns rather than empty shells.
|
||
fn is_perimeter(footprint: &TileRect, tx: i32, ty: i32) -> bool {
|
||
let lo_x = footprint.origin.0 as i32;
|
||
let lo_y = footprint.origin.1 as i32;
|
||
let hi_x = lo_x + footprint.size.0.max(1) as i32 - 1;
|
||
let hi_y = lo_y + footprint.size.1.max(1) as i32 - 1;
|
||
tx == lo_x || tx == hi_x || ty == lo_y || ty == hi_y
|
||
}
|
||
|
||
/// Compile-time sanity: a chunk is 64 voxels on a side, so chunk-local indices fit a u8.
|
||
const _: () = assert!(VOXELS_PER_CHUNK == CHUNK_M);
|
||
const _: () = assert!(CHUNK_M <= u8::MAX as i32 + 1);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::atlas::tile_condition::TileCondition;
|
||
use crate::simulation::generator::{
|
||
ArchitectureFlavorRef, BuildingEntryClass, ConstructionEra, EraCause, FloorExtent,
|
||
FloorHeightProfile, ZoneTypeId,
|
||
};
|
||
|
||
/// Build a `BuildingPropertyTag` with the given block-local footprint and a
|
||
/// uniform 3-voxel-per-floor extent (the D-229 default).
|
||
fn tag(
|
||
origin: (u8, u8),
|
||
size: (u8, u8),
|
||
base_floor: i8,
|
||
floor_count: u8,
|
||
) -> BuildingPropertyTag {
|
||
BuildingPropertyTag {
|
||
zone_type_id: ZoneTypeId::new("residential_low"),
|
||
footprint: TileRect::new(origin.0, origin.1, size.0, size.1),
|
||
extent: FloorExtent {
|
||
base_floor,
|
||
floor_count,
|
||
heights: FloorHeightProfile::Uniform(3),
|
||
},
|
||
entry_class: BuildingEntryClass::Public,
|
||
flavor_ref: ArchitectureFlavorRef { flavor_index: 0 },
|
||
era: ConstructionEra::Founding,
|
||
era_cause: EraCause::Original,
|
||
initial_condition: TileCondition::Intact,
|
||
doors: Vec::new(),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn empty_block_yields_empty_chunk() {
|
||
let fc = fill_chunk(7, (0, 0), (0, 0), &[]);
|
||
assert_eq!(fc.voxel_count(), 0);
|
||
assert_eq!(fc.quarter_id, 7);
|
||
assert_eq!(fc.chunk_in_quarter(), (0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn chunk_in_quarter_maps_block_and_sub_chunk() {
|
||
let fc = fill_chunk(0, (3, 2), (1, 0), &[]);
|
||
// block (3,2) sub-chunk (1,0) → quarter chunk (3*2+1, 2*2+0) = (7, 4).
|
||
assert_eq!(fc.chunk_in_quarter(), (7, 4));
|
||
}
|
||
|
||
#[test]
|
||
fn single_storey_box_has_walls_floor_and_roof() {
|
||
// 4×4 single-storey building at block-local origin (2,2), sub-chunk (0,0).
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &[tag((2, 2), (4, 4), 0, 1)]);
|
||
|
||
// Ground floor (3 voxels: z 0,1,2). Roof at z = 3.
|
||
// Corner (2,2) is perimeter → Wall through z 0..=2.
|
||
assert_eq!(fc.get(2, 2, 0), ShellVoxel::Wall);
|
||
assert_eq!(fc.get(2, 2, 2), ShellVoxel::Wall);
|
||
// Interior tile (3,3) → FloorSlab at the floor base (z 0), Void above.
|
||
assert_eq!(fc.get(3, 3, 0), ShellVoxel::FloorSlab);
|
||
assert_eq!(fc.get(3, 3, 1), ShellVoxel::Void);
|
||
// Roof caps the whole footprint at z = 3 (perimeter and interior alike).
|
||
assert_eq!(fc.get(2, 2, 3), ShellVoxel::Roof);
|
||
assert_eq!(fc.get(3, 3, 3), ShellVoxel::Roof);
|
||
// Outside the footprint → Void.
|
||
assert_eq!(fc.get(0, 0, 0), ShellVoxel::Void);
|
||
}
|
||
|
||
#[test]
|
||
fn multi_storey_stacks_floor_slabs() {
|
||
// 5×5, three storeys (z bands 0..2, 3..5, 6..8). Roof at z = 9.
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &[tag((0, 0), (5, 5), 0, 3)]);
|
||
// Interior tile gets a slab at each floor base: z 0, 3, 6.
|
||
assert_eq!(fc.get(2, 2, 0), ShellVoxel::FloorSlab);
|
||
assert_eq!(fc.get(2, 2, 3), ShellVoxel::FloorSlab);
|
||
assert_eq!(fc.get(2, 2, 6), ShellVoxel::FloorSlab);
|
||
// Between slabs is interior air.
|
||
assert_eq!(fc.get(2, 2, 1), ShellVoxel::Void);
|
||
// Perimeter wall runs the full height to the top floor's top voxel (z 8).
|
||
assert_eq!(fc.get(0, 0, 8), ShellVoxel::Wall);
|
||
// Roof one voxel above the top floor.
|
||
assert_eq!(fc.get(2, 2, 9), ShellVoxel::Roof);
|
||
}
|
||
|
||
#[test]
|
||
fn basement_floor_is_below_ground_zero() {
|
||
// base_floor = -1, 2 floors → basement (z -3..-1) + ground (z 0..2).
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &[tag((0, 0), (3, 3), -1, 2)]);
|
||
// Ground floor interior slab at z = 0 (D-110: ground bottom is the origin).
|
||
assert_eq!(fc.get(1, 1, 0), ShellVoxel::FloorSlab);
|
||
// Basement interior slab is below zero.
|
||
assert_eq!(fc.get(1, 1, -3), ShellVoxel::FloorSlab);
|
||
// Basement perimeter is wall.
|
||
assert_eq!(fc.get(0, 0, -1), ShellVoxel::Wall);
|
||
}
|
||
|
||
#[test]
|
||
fn elevated_building_with_no_ground_floor_anchors_at_its_own_bottom() {
|
||
// base_floor = 2, no floor 0 → ground_offset falls back to 0, so the building's
|
||
// own bottom maps to chunk-z 0 (no shift). 2 floors × 3 voxels, then a roof.
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &[tag((0, 0), (3, 3), 2, 2)]);
|
||
// Lowest present floor's interior slab sits at chunk-z 0.
|
||
assert_eq!(fc.get(1, 1, 0), ShellVoxel::FloorSlab);
|
||
// Second floor's slab one storey up (z 3).
|
||
assert_eq!(fc.get(1, 1, 3), ShellVoxel::FloorSlab);
|
||
// Perimeter wall from the bottom.
|
||
assert_eq!(fc.get(0, 0, 0), ShellVoxel::Wall);
|
||
// Roof one voxel above the two storeys (z 6).
|
||
assert_eq!(fc.get(1, 1, 6), ShellVoxel::Roof);
|
||
}
|
||
|
||
#[test]
|
||
fn one_wide_building_is_all_wall() {
|
||
// 1×4 footprint — every tile is perimeter, so all Wall (no interior slab).
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &[tag((0, 0), (1, 4), 0, 1)]);
|
||
for ty in 0..4u8 {
|
||
assert_eq!(fc.get(0, ty, 0), ShellVoxel::Wall);
|
||
}
|
||
// No FloorSlab anywhere (no interior tiles).
|
||
assert!(!fc.voxels.values().any(|v| *v == ShellVoxel::FloorSlab));
|
||
}
|
||
|
||
#[test]
|
||
fn footprint_clipped_to_sub_chunk() {
|
||
// A building spanning the block's left edge into the second sub-chunk.
|
||
// Footprint block-local x 60..68 straddles the x=64 sub-chunk seam.
|
||
let building = tag((60, 10), (8, 4), 0, 1);
|
||
let left = fill_chunk(1, (0, 0), (0, 0), std::slice::from_ref(&building));
|
||
let right = fill_chunk(1, (0, 0), (1, 0), std::slice::from_ref(&building));
|
||
|
||
// Left sub-chunk (0,0): the window origin is 0, so here chunk-local == block-local
|
||
// (x 60..64). The right sub-chunk below is the general case where they differ.
|
||
assert_ne!(left.voxel_count(), 0);
|
||
assert!(left.voxels.keys().all(|(x, _, _)| (60..64).contains(x)));
|
||
// Right sub-chunk holds block-local x 64..68 → chunk-local x 0..4.
|
||
assert_ne!(right.voxel_count(), 0);
|
||
assert!(right.voxels.keys().all(|(x, _, _)| (0..4).contains(x)));
|
||
}
|
||
|
||
#[test]
|
||
fn fill_is_deterministic() {
|
||
let tags = vec![tag((0, 0), (6, 6), -1, 4), tag((40, 40), (10, 8), 0, 2)];
|
||
let a = fill_chunk(99, (1, 1), (0, 1), &tags);
|
||
let b = fill_chunk(99, (1, 1), (0, 1), &tags);
|
||
assert_eq!(a, b);
|
||
}
|
||
|
||
#[test]
|
||
fn shell_is_sparse_versus_dense_volume() {
|
||
// A realistically dense chunk: a 4×4 grid of 14×14 buildings (2-tile gaps),
|
||
// each 5 storeys (15 voxels) + roof. The shell stores only surfaces — walls,
|
||
// per-floor slabs, roof — so it must hold strictly fewer voxels than the dense
|
||
// building volume (which would also store the interior air between floors).
|
||
// This sparsity is what keeps the derive within the D-230 <5 ms budget.
|
||
let mut tags = Vec::new();
|
||
for gx in 0..4u8 {
|
||
for gy in 0..4u8 {
|
||
tags.push(tag((gx * 16, gy * 16), (14, 14), 0, 5));
|
||
}
|
||
}
|
||
let fc = fill_chunk(1, (0, 0), (0, 0), &tags);
|
||
|
||
// Dense volume: 16 buildings × (14×14 footprint) × (5 floors × 3 + 1 roof).
|
||
let dense_volume = 16 * 14 * 14 * (5 * 3 + 1);
|
||
assert!(fc.voxel_count() > 0, "a built chunk must derive voxels");
|
||
assert!(
|
||
fc.voxel_count() < dense_volume,
|
||
"shell ({}) must be sparser than the dense volume ({dense_volume}) — \
|
||
interior air must not be stored",
|
||
fc.voxel_count()
|
||
);
|
||
}
|
||
}
|