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:
@@ -27,7 +27,9 @@ use crate::atlas::road_graph::{RoadGraph, RoadNode};
|
||||
use crate::atlas::scale;
|
||||
use crate::atlas::skeleton_gen::derive_complexity;
|
||||
use crate::atlas::source_resolver::BodySourceResolverResource;
|
||||
use crate::atlas::trait_catalog_reader::{TraitBias, TraitCatalogReaderResource, TraitTemplate};
|
||||
use crate::atlas::trait_catalog_reader::{
|
||||
ExteriorCatalog, TraitBias, TraitCatalogReaderResource, TraitTemplate,
|
||||
};
|
||||
use crate::atlas::trait_draw::{
|
||||
complexity_k, draw_body_vocabulary, hard_gate_eligible, pick_district_dominant_by_type,
|
||||
VocabularyDrawInputs,
|
||||
@@ -177,6 +179,25 @@ fn drain_generation_completions(
|
||||
),
|
||||
None => (Vec::new(), Vec::new()),
|
||||
};
|
||||
// D-235 exterior-grammar content (T-988), read alongside the
|
||||
// D-232 catalog above — same L3→L4 dispatch-time rationale
|
||||
// (`assign_block_tags` stays DB-free downstream, T-987/D-230).
|
||||
// `templates` reuses the already-fetched `catalog` (itself
|
||||
// OnceLock-cached inside the reader) rather than re-querying.
|
||||
let exterior_catalog: ExteriorCatalog = match trait_catalog.as_ref() {
|
||||
Some(tc) => ExteriorCatalog {
|
||||
templates: catalog.clone(),
|
||||
zone_bias: tc.0.read_zone_bias().unwrap_or_else(|e| {
|
||||
tracing::warn!(body_id = %body_id, error = %e, "zone bias read failed — uniform draw everywhere");
|
||||
BTreeMap::new()
|
||||
}),
|
||||
color_bands: tc.0.read_color_register_bands().unwrap_or_else(|e| {
|
||||
tracing::warn!(body_id = %body_id, error = %e, "color register bands read failed — neutral color everywhere");
|
||||
BTreeMap::new()
|
||||
}),
|
||||
},
|
||||
None => ExteriorCatalog::default(),
|
||||
};
|
||||
let body_sector: Option<&str> = resolved
|
||||
.first()
|
||||
.and_then(|(_, rs)| rs.geographic_sector.as_deref());
|
||||
@@ -208,6 +229,7 @@ fn drain_generation_completions(
|
||||
catalog: &catalog,
|
||||
eligible: &eligible,
|
||||
swerve_pools: &swerve_pools,
|
||||
exterior_catalog: &exterior_catalog,
|
||||
};
|
||||
|
||||
for (placement, read_set) in resolved {
|
||||
@@ -364,6 +386,12 @@ struct BodyVocabularyContext<'a> {
|
||||
/// heritage-callback), cloned onto each settlement's context — the
|
||||
/// per-building wildcard draws from these at `assign_block_tags` time.
|
||||
swerve_pools: &'a SwervePools,
|
||||
/// D-235 exterior-grammar content (T-988): the catalog's `visual_bundle`s
|
||||
/// plus the two sibling content tables, read once per body and cloned
|
||||
/// verbatim onto every settlement's `GenerateSkeleton` work item — the
|
||||
/// per-building `BuildingExteriorTag` draw happens at `assign_block_tags`
|
||||
/// time (`atlas::trait_exterior`), never inside `FillChunk`.
|
||||
exterior_catalog: &'a ExteriorCatalog,
|
||||
}
|
||||
|
||||
/// A city's node degree in the T-1038 road/rail graph — the T-1003 swerve's
|
||||
@@ -534,6 +562,7 @@ fn build_skeleton_work_item(
|
||||
economic_role,
|
||||
population,
|
||||
founding_age_years,
|
||||
exterior_catalog: vocab.exterior_catalog.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,12 +819,19 @@ mod tests {
|
||||
foreign: Vec::new(),
|
||||
heritage: Vec::new(),
|
||||
};
|
||||
// `ExteriorCatalog::default()` isn't a const fn (derived `Default`),
|
||||
// so a `static` binding isn't available the way it is for
|
||||
// `EMPTY_POOLS` above — leak a tiny one-off value instead (test-only,
|
||||
// matches this function's existing 'static-returning contract).
|
||||
let exterior_catalog: &'static ExteriorCatalog =
|
||||
Box::leak(Box::new(ExteriorCatalog::default()));
|
||||
BodyVocabularyContext {
|
||||
trait_selection: &[],
|
||||
body_district_type_mix: &[],
|
||||
catalog: &[],
|
||||
eligible: &[],
|
||||
swerve_pools: &EMPTY_POOLS,
|
||||
exterior_catalog,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +938,7 @@ mod tests {
|
||||
base_weight: 10_000,
|
||||
weight_mods: BTreeMap::new(),
|
||||
zone_affinity: [(DistrictType::MixedUse, 10_000)].into_iter().collect(),
|
||||
visual_bundle: Default::default(),
|
||||
}
|
||||
}
|
||||
let catalog = vec![tmpl("temp_a"), tmpl("temp_b")];
|
||||
@@ -912,12 +949,14 @@ mod tests {
|
||||
foreign: vec![("foreign_x".to_string(), 10_000)],
|
||||
heritage: vec![("herit_y".to_string(), 10_000)],
|
||||
};
|
||||
let exterior_catalog = ExteriorCatalog::default();
|
||||
let vocab = BodyVocabularyContext {
|
||||
trait_selection: &trait_selection,
|
||||
body_district_type_mix: &mix,
|
||||
catalog: &catalog,
|
||||
eligible: &eligible,
|
||||
swerve_pools: &pools,
|
||||
exterior_catalog: &exterior_catalog,
|
||||
};
|
||||
|
||||
// City 1 sits in the road graph with degree 2; city 2 has no node (degree 0).
|
||||
|
||||
Reference in New Issue
Block a user