//! Canonical spatial scale ladder (D-243) — **the single source of truth** for //! the metric containment hierarchy. Every cascade consumer references these //! constants and conversions; no module re-derives its own scale. //! //! This is the *structural* fix for the Q-110 failure mode — three files had //! three different "region" sizes (1 024 m, 624 km, "16 chunks") because the //! metres-per-scale lived as scattered magic numbers. With one canonical source //! a file physically cannot invent a scale that disagrees, and the `const` //! asserts below fail the build if the rungs ever stop nesting cleanly. //! //! ```text //! voxel 1 m → chunk 64 m → block 128 m → quarter 512 m → district 2 048 m //! → region ~205 km → (elastic seam) → planet //! ``` //! //! Below the region everything is fixed integer math; only [`regions_per_equator`] //! (the planetary count) varies per body. See [D-243] for the full model. //! //! [D-243]: ../../../governance/decisions/architecture.md // --------------------------------------------------------------------------- // Edge lengths in metres (absolute, fixed — D-243) // --------------------------------------------------------------------------- /// Voxel edge — the tile (D-228 / D-220). pub const VOXEL_M: i32 = 1; /// Chunk edge — the stream / derive unit (D-222, D-239). pub const CHUNK_M: i32 = 64; /// Block edge — generator planning unit (D-222). pub const BLOCK_M: i32 = 128; /// Quarter edge — settlement footprint cell (D-222). pub const QUARTER_M: i32 = 512; /// District edge — urban division **and** the local terrain/climate carrier cell /// (D-222, D-239 §2, D-243). The fine carrier `ChunkContext`/voxels consume. pub const DISTRICT_M: i32 = 2_048; /// Region edge — the top hard block: planetary grid + climate/weather/season /// lockdown (D-243). 100 districts on a side. pub const REGION_M: i32 = 204_800; // --------------------------------------------------------------------------- // Linear nesting ratios (children per parent edge) // --------------------------------------------------------------------------- /// Voxels per chunk edge (64 m / 1 m). A chunk is 64×64 voxels. pub const VOXELS_PER_CHUNK: i32 = CHUNK_M / VOXEL_M; /// Chunks per district edge (2 048 m / 64 m = 32 = `1 << 5`). pub const CHUNKS_PER_DISTRICT: i32 = DISTRICT_M / CHUNK_M; /// `log2(CHUNKS_PER_DISTRICT)` — chunk→district is an arithmetic shift (floors /// toward −∞ for negative coords, the correct tiling behaviour). pub const CHUNK_DISTRICT_SHIFT: u32 = 5; /// Districts per region edge (204 800 m / 2 048 m = 100). Not a power of two, /// so region mapping uses division, not a shift. pub const DISTRICTS_PER_REGION: i32 = REGION_M / DISTRICT_M; // Compile-time ladder invariants — the build fails if a rung stops nesting. const _: () = assert!(CHUNKS_PER_DISTRICT == 1 << CHUNK_DISTRICT_SHIFT); const _: () = assert!(CHUNKS_PER_DISTRICT * CHUNK_M == DISTRICT_M); const _: () = assert!(DISTRICTS_PER_REGION * DISTRICT_M == REGION_M); const _: () = assert!(DISTRICTS_PER_REGION == 100); // --------------------------------------------------------------------------- // Grid positions + fixed integer addressing // --------------------------------------------------------------------------- /// Position on the 64 m chunk grid (chunk-units, not metres). `BTreeMap` key (D-010). pub type ChunkPos = (i32, i32); /// Position on the 2 km district grid (district-units) — the terrain/climate carrier cell. pub type DistrictPos = (i32, i32); /// Position on the ~205 km region grid (region-units) — the top hard block. pub type RegionPos = (i32, i32); /// The district covering a chunk. Arithmetic shift floors toward −∞ so the tiling /// is consistent across the origin (a chunk at −1 belongs to district −1, not 0). #[inline] pub fn chunk_to_district(c: ChunkPos) -> DistrictPos { (c.0 >> CHUNK_DISTRICT_SHIFT, c.1 >> CHUNK_DISTRICT_SHIFT) } /// The region covering a district. Euclidean floor division matches the shift's /// flooring, so chunk→district→region tiling is seamless across the origin. #[inline] pub fn district_to_region(d: DistrictPos) -> RegionPos { ( d.0.div_euclid(DISTRICTS_PER_REGION), d.1.div_euclid(DISTRICTS_PER_REGION), ) } /// The region covering a chunk (chunk → district → region). #[inline] pub fn chunk_to_region(c: ChunkPos) -> RegionPos { district_to_region(chunk_to_district(c)) } // --------------------------------------------------------------------------- // Working-heightmap pixel ↔ district (cascade.rs working grid) // --------------------------------------------------------------------------- /// Number of working-heightmap-grid pixels per district side on the standard /// cascade working resolution (~128×64 working grid; D-203, D-239 §1, T-1023). /// /// This is NOT a metre-scale constant — it is the `grid_cells_per_district` /// parameter passed to [`crate::atlas::district_profile::derive_all_districts`]. /// Centralised here so plugin.rs and cascade.rs share one definition and neither /// hard-codes `8` independently. /// /// `DistrictPos = (col / HEIGHTMAP_CELLS_PER_DISTRICT, row / HEIGHTMAP_CELLS_PER_DISTRICT)` /// for a working-grid pixel `(row, col)` — see [`heightmap_pixel_to_district`]. pub const HEIGHTMAP_CELLS_PER_DISTRICT: usize = 8; /// Convert a working-heightmap-grid pixel coordinate `(row, col)` to the /// [`DistrictPos`] that covers it. /// /// `CityPlacement.position` and `RoadNode.position` are both stored in /// working-heightmap-grid coordinates (row-major, `(row, col)` order), and the /// district grid is built with the same pixel grid by /// [`crate::atlas::district_profile::derive_all_districts`]. Integer division /// floors toward zero, which matches the `BTreeMap` keys inserted by `derive_all_districts`. #[inline] pub fn heightmap_pixel_to_district(pixel: (u16, u16)) -> DistrictPos { let cpd = HEIGHTMAP_CELLS_PER_DISTRICT as i32; // pixel = (row, col); DistrictPos convention is (dx=col_district, dy=row_district). let dx = pixel.1 as i32 / cpd; let dy = pixel.0 as i32 / cpd; (dx, dy) } // --------------------------------------------------------------------------- // The elastic seam — region ↔ planet (the only per-body-floating quantity) // --------------------------------------------------------------------------- /// Number of ~205 km regions around the body's equator: `round(2πR / REGION_M)`, /// minimum 1 (D-243). `R` is `bodies.body_radius_km` (D-204) — the single /// body-specific input in the whole scale chain. Pole-to-pole is half this. pub fn regions_per_equator(body_radius_km: f64) -> u32 { let circumference_m = 2.0 * std::f64::consts::PI * body_radius_km * 1000.0; ((circumference_m / REGION_M as f64).round() as i64).max(1) as u32 } #[cfg(test)] mod tests { use super::*; #[test] fn ladder_nests_cleanly() { assert_eq!(VOXELS_PER_CHUNK, 64); assert_eq!(CHUNKS_PER_DISTRICT, 32); assert_eq!(DISTRICTS_PER_REGION, 100); assert_eq!(CHUNK_M * CHUNKS_PER_DISTRICT, DISTRICT_M); assert_eq!(DISTRICT_M * DISTRICTS_PER_REGION, REGION_M); } #[test] fn addressing_floors_across_origin() { // chunk → district (32 chunks/district) assert_eq!(chunk_to_district((0, 0)), (0, 0)); assert_eq!(chunk_to_district((31, 31)), (0, 0)); assert_eq!(chunk_to_district((32, 32)), (1, 1)); assert_eq!(chunk_to_district((-1, -1)), (-1, -1)); // floors, not truncates assert_eq!(chunk_to_district((-32, -32)), (-1, -1)); assert_eq!(chunk_to_district((-33, -33)), (-2, -2)); // district → region (100 districts/region) assert_eq!(district_to_region((99, 0)), (0, 0)); assert_eq!(district_to_region((100, 0)), (1, 0)); assert_eq!(district_to_region((-1, 0)), (-1, 0)); // chunk → region (3200 chunks/region) assert_eq!(chunk_to_region((3199, 0)), (0, 0)); assert_eq!(chunk_to_region((3200, 0)), (1, 0)); } #[test] fn regions_per_equator_earth_and_moon() { // Earth (R≈6371 km): 2πR ≈ 40 030 km / 204.8 km ≈ 195.46 → 195. assert_eq!(regions_per_equator(6371.0), 195); // A small moon (R≈200 km): ≈ 1257 km / 204.8 ≈ 6.1 → 6. assert_eq!(regions_per_equator(200.0), 6); // Degenerate radius still yields at least one region. assert_eq!(regions_per_equator(0.0), 1); } }