feat(simulation): BuildingExteriorTag grammar + Layer-5 material fill + ops-surface (T-988/T-959/T-1097)

T-988 — BuildingExteriorTag {wall_material, roof_form, facade_rhythm,
setback_tier, color:HsvColor, street_surface}. New trait_exterior.rs:
3-step era-free derivation (visual_bundle filter -> zone-bias weighted
pick -> density->setback), color seed-sampled within the register band.
Resolved at GenerateSkeleton plan time (T-994 precedent), frozen on
BuildingPropertyTag; FillChunk only reads it. Four append-only
integer-discriminant enums (WallMaterial/RoofForm/FacadeRhythm/
StreetSurface), unknown token -> axis Generic + warn. trait_catalog_reader
now parses visual_bundle + the two new tables (OnceLock-cached).
New SeedDomain::TraitExterior (per-axis sub-chains — no cross-field
correlation).

T-959 — FillChunk reads the frozen exterior tag: wall_material/roof_form
onto Wall/Roof shell voxels (FilledChunk.surface_material). Additive,
ShellVoxel untouched, all shell tests pass. (Interstitial-fill-from-setback
split to T-1098 — needs a FillChunk block-metadata + geometry design pass;
nothing consumes FillChunk until Phase 5.)

T-1097 — BlockSkeleton.interstitial_character (OpenSpace|OperationsSurface),
D-233 bulk-driven: bulk-industry blocks' non-roofed remainder tags as built
economic infrastructure, not generic open space.

cargo check --all-targets clean; 1666 lib tests pass; no golden impact
(harnesses top out at CascadeLayer::RoadGraph, unreachable by Layer 4/5).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:42:20 +02:00
co-authored by Claude Fable 5
parent ba781f9324
commit 204359927a
11 changed files with 1777 additions and 29 deletions
+32 -3
View File
@@ -34,6 +34,7 @@ use crate::atlas::district_profile::BodyParams;
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::shell::{fill_chunk, FilledChunk};
use crate::atlas::skeleton_gen::{assign_all_block_tags, generate_quarter_skeleton};
use crate::atlas::trait_catalog_reader::ExteriorCatalog;
use crate::seed::SeedChain;
use crate::simulation::generator::{BuildingPropertyTag, CityGenerationContext, QuarterWorldState};
@@ -113,6 +114,15 @@ pub enum GenWorkItem {
economic_role: String,
population: i64,
founding_age_years: u32,
/// D-235 exterior-grammar content (T-988): the trait-template catalog's
/// `visual_bundle`s plus the two sibling content tables
/// (`architecture_zone_bias`, `color_register_bands`), pre-resolved at
/// dispatch time — mirrors `context`'s own pre-resolution rationale, so
/// `assign_block_tags` never touches `systems.db` (T-987/D-230 purity).
/// A `Vec`/`BTreeMap`-backed struct is a handful of words on the stack
/// regardless of its contents' size (same reasoning already documented
/// for `FillChunk.block_tags` below), so this needs no `Box`.
exterior_catalog: ExteriorCatalog,
},
/// Derive the building shell for one 64 m chunk of an existing quarter
/// (D-230 derive phase, T-987).
@@ -431,6 +441,7 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
economic_role,
population,
founding_age_years,
exterior_catalog,
} => {
// Build the Phase 1 skeleton from the pre-resolved D-199 context.
// `economic_role`, `population`, and `founding_age_years` are the
@@ -445,13 +456,16 @@ fn run_work_item(item: &GenWorkItem) -> GenCompletion {
*chain,
);
// Step-3 building-property tags per footprint (D-229, #957): subdivide
// each block into building plots and tag them.
// each block into building plots and tag them. `exterior_catalog`
// (D-235, T-988) resolves each tag's BuildingExteriorTag in the
// same pass.
let block_tags = assign_all_block_tags(
&skeleton,
context,
economic_role,
*founding_age_years,
*chain,
exterior_catalog,
);
GenCompletion::SkeletonGenerated {
city_id: *city_id,
@@ -591,6 +605,7 @@ mod tests {
economic_role: "service_mixed".to_string(),
population: 500_000,
founding_age_years: 200,
exterior_catalog: ExteriorCatalog::default(),
}
}
@@ -700,8 +715,10 @@ mod tests {
fn quarter_with_one_building() -> QuarterWorldState {
use crate::atlas::tile_condition::TileCondition;
use crate::simulation::generator::{
ArchitectureFlavorRef, BuildingEntryClass, BuildingPropertyTag, ConstructionEra,
EraCause, FloorExtent, FloorHeightProfile, QuarterSkeleton, TileRect, ZoneTypeId,
ArchitectureFlavorRef, BuildingEntryClass, BuildingExteriorTag, BuildingPropertyTag,
ConstructionEra, EraCause, FacadeRhythm, FloorExtent, FloorHeightProfile, HsvColor,
QuarterSkeleton, RoofForm, SetbackTier, StreetSurface, TileRect, WallMaterial,
ZoneTypeId,
};
use std::collections::BTreeMap;
@@ -721,6 +738,18 @@ mod tests {
era: ConstructionEra::Founding,
era_cause: EraCause::Original,
initial_condition: TileCondition::Intact,
exterior: BuildingExteriorTag {
wall_material: WallMaterial::Generic,
roof_form: RoofForm::Generic,
facade_rhythm: FacadeRhythm::Generic,
setback_tier: SetbackTier::Standard,
color: HsvColor {
hue: 0,
sat: 0,
val: 5_000,
},
street_surface: StreetSurface::Generic,
},
doors: Vec::new(),
}],
);