feat(simulation): cache TerrainAnalysis + derive basin_direction from the D8 thalweg (T-1044, T-1047)
T-1044: run_layer1 now returns TerrainAnalysis (carried transiently on CascadeSnapshot, dropped after the district + road-graph passes), eliminating the redundant per-body drainage::analyze + TerrainAnalysis::analyze re-run flagged by PERF/TODO(T-1044). Not persisted on the LRU-cached state (D-203/T-1048 size concern). T-1047: basin_direction is now derived from the real D8 thalweg. run_layer1 aggregates a per-district dominant D8 direction from the live fdir grid (carried transiently on DrainageResult), threaded via Layer1Output.district_basin_dirs -> derive_all_districts -> DistrictProfile.basin_direction; derive_chunk_context reads it directly. Removed the false derive_basin_direction (it branched on ocean_fraction_q then read seed bits despite a doc comment claiming an elev_q/slope_q D8 proxy) + corrected the module contract. D-239 §8 (D8 thalweg) now actually honoured. 1559 tests pass; golden byte-identical (district_basin_dirs is #[serde(skip)], transient). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,10 @@
|
||||
//! **coarser than a chunk** — so there is no per-tile `flow_direction[64×64]`
|
||||
//! here. Instead the chunk carries:
|
||||
//! - **`basin_direction`** — a cardinal basin-flow direction (N/E/S/W, 4-way)
|
||||
//! derived from the dominant D8 thalweg in the covering district.
|
||||
//! sourced directly from `DistrictProfile.basin_direction`, which holds the
|
||||
//! **true D8-computed dominant thalweg direction** aggregated in `run_layer1`
|
||||
//! (T-1047, D-239 §8). Prior to T-1047 this was incorrectly derived from
|
||||
//! seed bits; the false derivation (`derive_basin_direction`) has been removed.
|
||||
//! - **`meander_phase`** and **`meander_wavelength_m`** — global meander-curve
|
||||
//! params for the MeanderReach and AlluvialPlain families.
|
||||
//!
|
||||
@@ -55,28 +58,11 @@ use crate::atlas::district_profile::DistrictProfile;
|
||||
use crate::atlas::scale;
|
||||
use crate::seed::{SeedChain, SeedDomain};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Basin direction (cardinal, 4-way)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Cardinal basin-flow direction — the dominant D8 thalweg direction in the
|
||||
/// covering district. Coarser than a chunk (D-239 §10); derived from district slope
|
||||
/// and morphology, NOT from a per-tile D8 grid.
|
||||
///
|
||||
/// Integer-discriminant, append-only (D-010).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[repr(u8)]
|
||||
pub enum BasinDirection {
|
||||
/// Flow toward the north (decreasing y in grid coords).
|
||||
#[default]
|
||||
North = 0,
|
||||
/// Flow toward the east (increasing x in grid coords).
|
||||
East = 1,
|
||||
/// Flow toward the south (increasing y in grid coords).
|
||||
South = 2,
|
||||
/// Flow toward the west (decreasing x in grid coords).
|
||||
West = 3,
|
||||
}
|
||||
// Re-export so existing consumers (voxel.rs etc.) keep their `chunk_context::BasinDirection` path.
|
||||
// The canonical definition lives in `scale.rs` (T-1047) — `BasinDirection` is shared
|
||||
// between `DistrictProfile` (district_profile.rs) and `ChunkContext` (this module),
|
||||
// and placing it in the scale ladder breaks the potential circular import.
|
||||
pub use crate::atlas::scale::BasinDirection;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ChunkPos — position on the 64 m chunk grid
|
||||
@@ -223,10 +209,18 @@ pub struct ChunkContext {
|
||||
///
|
||||
/// ## Seed usage
|
||||
///
|
||||
/// The **meander phase** and **basin direction** are seeded at district scale so
|
||||
/// features with wavelength > 64 m are consistent across chunk boundaries.
|
||||
/// The chunk-local seed (keyed on `chunk_pos`) is reserved for sub-chunk scatter
|
||||
/// in the voxel pass — not consumed here.
|
||||
/// The **meander phase** is seeded at district scale so features with
|
||||
/// wavelength > 64 m are consistent across chunk boundaries. The chunk-local
|
||||
/// seed (keyed on `chunk_pos`) is reserved for sub-chunk scatter in the voxel
|
||||
/// pass — not consumed here.
|
||||
///
|
||||
/// ## Basin direction
|
||||
///
|
||||
/// `basin_direction` is read directly from `district.basin_direction` — the
|
||||
/// **true D8-computed dominant thalweg direction** threaded from `run_layer1`
|
||||
/// (T-1047, D-239 §8). No seed-bit proxy is used here. The former
|
||||
/// `derive_basin_direction` helper (which branched on `ocean_fraction_q` and
|
||||
/// seed bits, not on the actual D8 grid) has been removed.
|
||||
pub fn derive_chunk_context(
|
||||
world_seed: u64,
|
||||
body_id: &str,
|
||||
@@ -246,11 +240,10 @@ pub fn derive_chunk_context(
|
||||
let district_seed = SeedChain::for_body(world_seed, body_id)
|
||||
.derive(SeedDomain::ChunkContext, district_scale_id);
|
||||
|
||||
// Basin direction — derived from district slope_q (which encodes the
|
||||
// dominant terrain gradient). We use the district's `elev_q` gradient
|
||||
// direction as a proxy for the D8 thalweg direction.
|
||||
// All integer arithmetic (D-010).
|
||||
let basin_direction = derive_basin_direction(district, district_seed.seed());
|
||||
// Basin direction — read directly from DistrictProfile (T-1047, D-239 §8).
|
||||
// This is the true D8-computed dominant thalweg direction aggregated in
|
||||
// run_layer1 from the fdir grid; it replaces the former seed-bit proxy.
|
||||
let basin_direction = district.basin_direction;
|
||||
|
||||
// Meander phase — district-scale integer offset so the channel is consistent
|
||||
// across all chunks in the same district. 0–255.
|
||||
@@ -450,38 +443,6 @@ pub(crate) fn pos_to_id(pos: (i32, i32)) -> u64 {
|
||||
.wrapping_add(y)
|
||||
}
|
||||
|
||||
/// Derive basin direction from the district profile and a district-scale seed.
|
||||
///
|
||||
/// Uses `elev_q` and `slope_q` as a proxy for the dominant D8 gradient
|
||||
/// direction. In lieu of a full D8 computation at this scale, the basin
|
||||
/// direction is derived from the district's terrain characteristics:
|
||||
/// - Coastal districts (high `ocean_fraction_q`) flow toward ocean (West fallback)
|
||||
/// - High-elevation districts flow away from ridges (seed-derived direction)
|
||||
/// - Low-slope districts use the seed for unbiased direction
|
||||
///
|
||||
/// All integer arithmetic (D-010).
|
||||
fn derive_basin_direction(district: &DistrictProfile, district_seed: u64) -> BasinDirection {
|
||||
// Coastal: flow toward the ocean (use seed to pick E/W/N/S with coastal bias).
|
||||
if district.ocean_fraction_q >= 15 {
|
||||
// The low 2 bits of seed give 4 directions; bias toward the most common
|
||||
// coastal configurations (N or S for equatorial coasts, E/W for shelf).
|
||||
return match (district_seed >> 2) & 0x3 {
|
||||
0 => BasinDirection::South,
|
||||
1 => BasinDirection::East,
|
||||
2 => BasinDirection::North,
|
||||
_ => BasinDirection::West,
|
||||
};
|
||||
}
|
||||
|
||||
// Interior: pure seed-derived direction (unbiased).
|
||||
match district_seed & 0x3 {
|
||||
0 => BasinDirection::North,
|
||||
1 => BasinDirection::East,
|
||||
2 => BasinDirection::South,
|
||||
_ => BasinDirection::West,
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive meander wavelength in metres from district morphology.
|
||||
///
|
||||
/// Low slope + high moisture → longer wavelength (wide meanders).
|
||||
@@ -543,6 +504,7 @@ mod tests {
|
||||
temperature_c: Some(18.0),
|
||||
moisture_q: 55,
|
||||
vegetation_class: VegetationClass::Forest,
|
||||
basin_direction: BasinDirection::South,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,6 +641,7 @@ mod tests {
|
||||
temperature_c: Some(35.0),
|
||||
moisture_q: 5,
|
||||
vegetation_class: VegetationClass::Barren,
|
||||
basin_direction: BasinDirection::North,
|
||||
};
|
||||
let ctx = derive_chunk_context(42, "dry_body", &district, (5, 5), None);
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user