feat: wire shell placeholders to backend actions #12
@@ -9,3 +9,4 @@ pub mod scene;
|
||||
pub mod scene_file;
|
||||
pub mod script;
|
||||
pub mod terrain;
|
||||
pub mod terrain_gen;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user