feat(simulation): add server/src/atlas/ — full Phase 1 generation pipeline

Ten-module atlas package implementing the D-194–D-218 district generation
stack: heightmap loader, BodyWorldState LRU cache, D8 drainage routing,
background generation queue, five-phase attractor-matching, three-component
district mix, block irregularity, tile condition thresholds, and the Phase 1
skeleton generator that wires them into DistrictSkeleton.

Closes #916 #917 #918 #919 #920 #922 #923 #924 #899.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-02 18:11:13 +02:00
co-authored by Claude Sonnet 4.6
parent f9fdfb7712
commit b9fd7a3fc8
10 changed files with 3326 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
//! Heightmap BLOB loader — reads float32 LE elevation grids from systems.db.
//!
//! Implements the Rust side of D-202. The Python pipeline stores each body's
//! elevation grid as a contiguous float32 little-endian BLOB in
//! `atlas_body_heightmaps.data`. This module loads that BLOB via `rusqlite`
//! and reinterprets the bytes into a `Vec<f32>` using `bytemuck`.
//!
//! Values are normalized elevation in [0.0, 1.0]. `sea_level` is the fraction
//! below which terrain is underwater (0.0 = no ocean).
//!
//! Canonical grid size: 512 × 256 (GRID_W × GRID_H), row-major.
use rusqlite::{params, Connection};
use thiserror::Error;
/// Canonical grid dimensions matching the Python pipeline (generate_atlas.py).
pub const GRID_W: u32 = 512;
pub const GRID_H: u32 = 256;
/// A loaded heightmap for one planetary body.
#[derive(Debug, Clone)]
pub struct BodyHeightmap {
pub body_id: String,
pub width: u32,
pub height: u32,
/// Row-major elevation values, normalized to [0.0, 1.0].
pub data: Vec<f32>,
/// Elevation fraction below which terrain is ocean/sea.
pub sea_level: f32,
}
impl BodyHeightmap {
/// Returns the elevation at (row, col), or `None` if out of bounds.
#[inline]
pub fn get(&self, row: u32, col: u32) -> Option<f32> {
if row < self.height && col < self.width {
Some(self.data[(row * self.width + col) as usize])
} else {
None
}
}
/// Returns `true` if the cell at (row, col) is land (above sea level).
#[inline]
pub fn is_land(&self, row: u32, col: u32) -> bool {
self.get(row, col).map_or(false, |e| e >= self.sea_level)
}
}
#[derive(Debug, Error)]
pub enum HeightmapLoadError {
#[error("no heightmap row for body '{0}'")]
NotFound(String),
#[error("BLOB size {actual} does not match declared grid {w}×{h}×4 = {expected}")]
BlobSizeMismatch {
actual: usize,
w: u32,
h: u32,
expected: usize,
},
#[error("SQLite error: {0}")]
Sql(#[from] rusqlite::Error),
}
/// Load the heightmap for `body_id` from the open `conn`.
///
/// The BLOB is reinterpreted in-place via `bytemuck::cast_slice` — no copy
/// beyond the initial `Vec<u8>` read from SQLite. On little-endian hosts
/// (all current targets) this is a zero-cost reinterpret. On big-endian hosts
/// the bytes are already stored LE, so each f32 would be byte-swapped; this
/// function does not perform that swap — big-endian support is deferred.
pub fn load_heightmap(
conn: &Connection,
body_id: &str,
) -> Result<BodyHeightmap, HeightmapLoadError> {
let result = conn.query_row(
"SELECT width, height, data, sea_level \
FROM atlas_body_heightmaps WHERE body_id = ?1",
params![body_id],
|row| {
let width: u32 = row.get(0)?;
let height: u32 = row.get(1)?;
let blob: Vec<u8> = row.get(2)?;
let sea_level: f64 = row.get(3)?;
Ok((width, height, blob, sea_level as f32))
},
);
match result {
Err(rusqlite::Error::QueryReturnedNoRows) => {
Err(HeightmapLoadError::NotFound(body_id.to_string()))
}
Err(e) => Err(HeightmapLoadError::Sql(e)),
Ok((width, height, blob, sea_level)) => {
let expected = (width * height * 4) as usize;
if blob.len() != expected {
return Err(HeightmapLoadError::BlobSizeMismatch {
actual: blob.len(),
w: width,
h: height,
expected,
});
}
// Reinterpret the LE bytes as f32 values. bytemuck::cast_slice
// is safe here: we verified the length is a multiple of 4, and
// f32 has no invalid bit patterns.
let floats: &[f32] = bytemuck::cast_slice(&blob);
let data = floats.to_vec();
Ok(BodyHeightmap {
body_id: body_id.to_string(),
width,
height,
data,
sea_level,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rusqlite::Connection;
fn make_test_db() -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE atlas_body_heightmaps (
body_id TEXT PRIMARY KEY,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
data BLOB NOT NULL,
sea_level REAL NOT NULL DEFAULT 0.0,
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
)
.unwrap();
conn
}
fn insert_heightmap(conn: &Connection, body_id: &str, w: u32, h: u32, sea_level: f32) {
let floats: Vec<f32> = (0..(w * h))
.map(|i| i as f32 / (w * h) as f32)
.collect();
let bytes: &[u8] = bytemuck::cast_slice(&floats);
conn.execute(
"INSERT INTO atlas_body_heightmaps (body_id, width, height, data, sea_level)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![body_id, w, h, bytes, sea_level],
)
.unwrap();
}
#[test]
fn round_trip_canonical_size() {
let conn = make_test_db();
insert_heightmap(&conn, "TestBody", GRID_W, GRID_H, 0.3);
let hm = load_heightmap(&conn, "TestBody").unwrap();
assert_eq!(hm.width, GRID_W);
assert_eq!(hm.height, GRID_H);
assert_eq!(hm.data.len(), (GRID_W * GRID_H) as usize);
assert!((hm.sea_level - 0.3).abs() < 1e-6);
// First cell is 0.0, last approaches 1.0
assert_eq!(hm.data[0], 0.0);
assert!(hm.data.last().copied().unwrap() < 1.0);
}
#[test]
fn get_and_is_land() {
let conn = make_test_db();
insert_heightmap(&conn, "LandBody", 4, 2, 0.5);
let hm = load_heightmap(&conn, "LandBody").unwrap();
// First cell (index 0) = 0.0 / 8 = 0.0 — below sea level
assert!(!hm.is_land(0, 0));
// Last cell (index 7) = 7.0 / 8 = 0.875 — above sea level
assert!(hm.is_land(1, 3));
// Out-of-bounds returns false
assert!(!hm.is_land(99, 99));
}
#[test]
fn not_found_error() {
let conn = make_test_db();
let err = load_heightmap(&conn, "Ghost").unwrap_err();
assert!(matches!(err, HeightmapLoadError::NotFound(_)));
}
#[test]
fn blob_size_mismatch_error() {
let conn = make_test_db();
// Insert a truncated BLOB
conn.execute(
"INSERT INTO atlas_body_heightmaps (body_id, width, height, data, sea_level)
VALUES ('BadBlob', 4, 4, X'DEADBEEF', 0.0)",
[],
)
.unwrap();
let err = load_heightmap(&conn, "BadBlob").unwrap_err();
assert!(matches!(err, HeightmapLoadError::BlobSizeMismatch { .. }));
}
}