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
+174
View File
@@ -850,6 +850,172 @@ pub enum ArchitectureFlavorRef {
Swerve(String),
}
// ---------------------------------------------------------------------------
// D-235 building exterior visual grammar (T-988)
// ---------------------------------------------------------------------------
//
// The vocabulary below is the RATIFIED ObjectTag palette (T-995, resolving
// Q-049) — `wiki/economics/object_tag_vocabulary.toml` — not the illustrative
// example lists in D-235's original body text (superseded by the 2026-07-07
// amendment). Each axis carries its specific tokens plus one `Generic`
// fallback-terminal placeholder (the D-235 asset-resolution degrade target).
//
// Integer-discriminant, append-only (D-010) — these values round-trip through
// `systems.db`-backed derivation today and are candidates for savegame
// persistence in Phase 5+ (D-227), so historical discriminants must never be
// renumbered, only appended to. Unknown registry strings parse to the axis's
// `Generic` variant with a `tracing::warn!` (patch tolerance) — see
// `atlas::trait_catalog_reader`.
/// Wall material token (D-235 `wall` axis, 10 specific + `Generic`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum WallMaterial {
ConcreteWall = 0,
SteelFrame = 1,
BrickWall = 2,
RenderedWall = 3,
StoneWall = 4,
TimberWall = 5,
StuccoWall = 6,
GlassCurtainWall = 7,
CompositePanel = 8,
RammedEarthWall = 9,
/// Fallback-terminal placeholder (`generic_wall`) — renders until the
/// specific asset ships (D-235), and the "unrecognized token" parse target.
#[default]
Generic = 10,
}
/// Roof form token (D-235 `roof` axis, 7 specific + `Generic`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum RoofForm {
FlatRoof = 0,
PitchedRoof = 1,
CorrugatedRoof = 2,
ClayTileRoof = 3,
TerracedRoof = 4,
VaultedRoof = 5,
GreenRoof = 6,
/// Fallback-terminal placeholder (`generic_roof`) — see [`WallMaterial::Generic`].
#[default]
Generic = 7,
}
/// Facade rhythm token (D-235 `facade` axis, 8 specific + `Generic`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum FacadeRhythm {
RegularFacade = 0,
OrnamentalFacade = 1,
IndustrialGlazing = 2,
ArcadeFacade = 3,
ShutteredFacade = 4,
ScreenFacade = 5,
Colonnade = 6,
LatticeScreen = 7,
/// Fallback-terminal placeholder (`generic_facade`) — see [`WallMaterial::Generic`].
#[default]
Generic = 8,
}
/// Street surface token (D-235 `street` axis, 7 specific + `Generic`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum StreetSurface {
Paved = 0,
Cobble = 1,
PackedEarth = 2,
CanalWay = 3,
ElevatedWalkway = 4,
HeavyHaul = 5,
Boardwalk = 6,
/// Fallback-terminal placeholder (`generic_street`) — see [`WallMaterial::Generic`].
#[default]
Generic = 7,
}
/// Building setback tier (D-235 step 3): derived from `BlockSkeleton.density_pct`
/// bands — Dense blocks read `ZeroLot`, Frontier blocks read `Campus`. Drives
/// the interstitial-space character (`void`/`court`/`garden`/`plaza`/
/// `dock_slip`/`market_pad`/`open_lawn`, D-235) a future fill pass reads.
///
/// The five-tier vocabulary is fixed by D-235 (`zero_lot | tight | standard |
/// generous | campus`); the density-band **thresholds** that map onto it are
/// **calibration placeholders** (T-988) pending Nigel/Araminta tuning, same
/// status as the swerve-rate constants (`trait_swerve`). Declaration order is
/// the tier's natural density ordering (tightest → loosest), so `Ord`
/// comparisons read correctly; not currently used as a save-critical
/// discriminant, so it is not `#[repr(u8)]`-pinned like the axis tokens above.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SetbackTier {
/// Buildings meet the lot line — no yard, shared walls common.
ZeroLot,
Tight,
Standard,
Generous,
/// Buildings sit well back within generous open grounds.
Campus,
}
/// A single sampled point within a D-235 `color_register` HSV band (T-988).
///
/// Integer-only (D-010, save-critical under D-227): `hue` is centidegrees
/// (degrees × 100, 0..36000); `sat`/`val` are basis points (0..10000). Sampled
/// once per building at plan time and frozen — never re-derived, no drift.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HsvColor {
pub hue: u32,
pub sat: u32,
pub val: u32,
}
/// The D-235 visible-form layer over a building's tags (T-988) — the frozen
/// output of the 3-step derivation: **(1)** the resolved trait template's
/// `visual_bundle` filters each axis's available token set; **(2)** the
/// building's `zone_type_id` biases the pick within that filtered set
/// (`architecture_zone_bias.toml`, uniform where unauthored); **(3)** the
/// block's `density_pct` sets `setback_tier`. `color` is seed-picked uniformly
/// within the template's `color_register` band.
///
/// **Era is deliberately absent** (D-232 reframe, D-235 amendment): the Reach
/// has no material-technology ladder, so era is never a material gate — it
/// reads as maintenance/wear via the D-217 condition layer instead.
///
/// Resolved once inside `GenerateSkeleton` (the same plan-time phase as
/// `flavor_ref`/`zone_type_id`) and stored frozen on `BuildingPropertyTag`;
/// `FillChunk` only reads it (T-987 purity) — see `atlas::trait_exterior`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildingExteriorTag {
pub wall_material: WallMaterial,
pub roof_form: RoofForm,
pub facade_rhythm: FacadeRhythm,
pub setback_tier: SetbackTier,
pub color: HsvColor,
pub street_surface: StreetSurface,
}
/// Character of a block's non-roofed remainder — the space between building
/// footprints (D-233, T-1097).
///
/// Bulk-industry blocks (`BulkClass::BulkSolid`/`BulkLiquid`) read this space
/// as **built economic infrastructure** (haul roads, ore pads, conveyor runs,
/// tank berms) — functional ground the settlement's economy depends on, never
/// generic open space. Every other `BulkClass` keeps the pre-T-1097 default:
/// informal interstitial space (yards, parks, lots) whose specific character
/// (`void`/`court`/`garden`/`plaza`/…) is the separate `SetbackTier`-driven
/// axis (D-235, T-988) — the two systems are distinct derivation axes sharing
/// the same physical space, not alternatives.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum InterstitialCharacter {
/// Generic open space — yards, parks, informal lots.
#[default]
OpenSpace,
/// Built economic infrastructure (D-233) — never open space.
OperationsSurface,
}
/// A single building footprint tag — the frozen step-3 output placed on every
/// building footprint at plan time (D-229).
///
@@ -874,6 +1040,9 @@ pub struct BuildingPropertyTag {
/// Frozen-amber condition snapshot from `prosperity_baseline_bps` (D-197/D-217).
/// The rolling condition overlay (D-198) paints over this; never mutates the tag.
pub initial_condition: crate::atlas::tile_condition::TileCondition,
/// D-235 visible-form layer (T-988): wall/roof/facade/street materials,
/// setback tier, and color — resolved once here, frozen thereafter.
pub exterior: BuildingExteriorTag,
/// Doors into / out of this building (D-231). At least one `Main` door.
///
/// Using `Vec<DoorSpec>` rather than `SmallVec<[DoorSpec; 4]>` for now;
@@ -1232,6 +1401,11 @@ pub struct BlockSkeleton {
/// Integer to avoid f32 non-determinism (D-010).
pub density_pct: u8,
pub landmark: Option<LandmarkSlot>,
/// Character of this block's non-roofed remainder (D-233, T-1097):
/// `OperationsSurface` for bulk-industry blocks, `OpenSpace` otherwise.
/// Derived once from `CityGenerationContext.dominant_bulk_class` at the
/// same plan-time pass as the rest of this skeleton.
pub interstitial_character: InterstitialCharacter,
}
/// Floor zone within a multi-level reservation.