Files
settled-reach/server/src/atlas/heightmap.rs
T
jpmschweitzerandClaude Sonnet 4.6 b9fd7a3fc8 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>
2026-05-02 18:11:13 +02:00

202 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 { .. }));
}
}