feat: wire shell placeholders to backend actions #12

Merged
moldybits merged 11 commits from feat/terrain-gen-abstraction into main 2026-05-17 19:30:57 -04:00
3 changed files with 174 additions and 0 deletions
Showing only changes of commit 11e5ceaf0a - Show all commits
+1
View File
@@ -9,3 +9,4 @@ pub mod scene;
pub mod scene_file;
pub mod script;
pub mod terrain;
pub mod terrain_gen;
+124
View File
@@ -0,0 +1,124 @@
use crate::terrain::{HeightGrid, TerrainError};
/// Describes a terrain generation request.
///
/// Specs are immutable once constructed; identical specs always yield
/// identical [`HeightGrid`]s from a given [`TerrainGenerator`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerrainGenerationSpec {
seed: u64,
width: u32,
height: u32,
}
impl TerrainGenerationSpec {
/// Creates a spec, rejecting zero-sized grids with [`TerrainError::ZeroDimension`].
pub fn new(seed: u64, width: u32, height: u32) -> Result<Self, TerrainError> {
if width == 0 || height == 0 {
return Err(TerrainError::ZeroDimension);
}
Ok(Self {
seed,
width,
height,
})
}
pub fn seed(&self) -> u64 {
self.seed
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
}
/// Produces a [`HeightGrid`] from a [`TerrainGenerationSpec`].
pub trait TerrainGenerator {
fn generate(&self, spec: &TerrainGenerationSpec) -> Result<HeightGrid, TerrainError>;
}
/// A deterministic, in-memory reference generator.
///
/// Heights are derived purely from the spec's seed and each sample's
/// coordinates, so identical specs always produce identical grids. No noise
/// algorithms are used yet; this is a stable baseline for CLI and rendering.
#[derive(Debug, Clone, Copy, Default)]
pub struct DeterministicTerrainGenerator;
impl DeterministicTerrainGenerator {
pub fn new() -> Self {
Self
}
}
/// Hashes seed and coordinates into a height in `[0.0, 1.0)`.
fn sample_height(seed: u64, x: u32, y: u32) -> f32 {
// SplitMix64-style finalizer over a coordinate-mixed seed.
let mut h = seed
^ (u64::from(x).wrapping_mul(0x9E37_79B9_7F4A_7C15))
^ (u64::from(y).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
h ^= h >> 30;
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
h ^= h >> 27;
h = h.wrapping_mul(0x94D0_49BB_1331_11EB);
h ^= h >> 31;
// Take the top 24 bits for an exact f32 fraction in [0, 1).
(h >> 40) as f32 / (1u64 << 24) as f32
}
impl TerrainGenerator for DeterministicTerrainGenerator {
fn generate(&self, spec: &TerrainGenerationSpec) -> Result<HeightGrid, TerrainError> {
let width = spec.width();
let height = spec.height();
let mut samples = Vec::with_capacity((width as usize) * (height as usize));
for y in 0..height {
for x in 0..width {
samples.push(sample_height(spec.seed(), x, y));
}
}
HeightGrid::new(width, height, samples)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_zero_dimensions() {
assert_eq!(
TerrainGenerationSpec::new(1, 0, 4).unwrap_err(),
TerrainError::ZeroDimension
);
assert_eq!(
TerrainGenerationSpec::new(1, 4, 0).unwrap_err(),
TerrainError::ZeroDimension
);
}
#[test]
fn generate_matches_spec_dimensions() {
let spec = TerrainGenerationSpec::new(99, 8, 5).unwrap();
let grid = DeterministicTerrainGenerator::new()
.generate(&spec)
.unwrap();
assert_eq!(grid.width(), 8);
assert_eq!(grid.height(), 5);
}
#[test]
fn different_seeds_diverge() {
let a = DeterministicTerrainGenerator::new()
.generate(&TerrainGenerationSpec::new(1, 4, 4).unwrap())
.unwrap();
let b = DeterministicTerrainGenerator::new()
.generate(&TerrainGenerationSpec::new(2, 4, 4).unwrap())
.unwrap();
assert_ne!(a.sample(0, 0), b.sample(0, 0));
}
}
+49
View File
@@ -0,0 +1,49 @@
use openvistapro::terrain::{HeightGrid, TerrainError};
use openvistapro::terrain_gen::{
DeterministicTerrainGenerator, TerrainGenerationSpec, TerrainGenerator,
};
fn assert_same_grid(a: &HeightGrid, b: &HeightGrid) {
assert_eq!(a.width(), b.width());
assert_eq!(a.height(), b.height());
for y in 0..a.height() {
for x in 0..a.width() {
assert_eq!(a.sample(x, y), b.sample(x, y), "mismatch at ({x}, {y})");
}
}
}
#[test]
fn terrain_gen_deterministic_generator_returns_requested_dimensions() {
let spec = TerrainGenerationSpec::new(0xfeed_beef, 4, 3).expect("valid spec");
let generator = DeterministicTerrainGenerator::new();
let grid = generator.generate(&spec).expect("generation succeeds");
assert_eq!(grid.width(), 4);
assert_eq!(grid.height(), 3);
}
#[test]
fn terrain_gen_deterministic_generator_is_stable_for_same_seed_and_size() {
let spec = TerrainGenerationSpec::new(42, 6, 5).expect("valid spec");
let generator = DeterministicTerrainGenerator::new();
let first = generator
.generate(&spec)
.expect("first generation succeeds");
let second = generator
.generate(&spec)
.expect("second generation succeeds");
assert_same_grid(&first, &second);
}
#[test]
fn terrain_gen_deterministic_generator_rejects_zero_dimensions() {
let err = TerrainGenerationSpec::new(7, 0, 4).unwrap_err();
assert_eq!(err, TerrainError::ZeroDimension);
let err = TerrainGenerationSpec::new(7, 4, 0).unwrap_err();
assert_eq!(err, TerrainError::ZeroDimension);
}