//! 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, } 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` 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 = 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, 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 = 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() ); } }