feat: wire shell placeholders to backend actions #12
@@ -9,3 +9,4 @@ pub mod scene;
|
|||||||
pub mod scene_file;
|
pub mod scene_file;
|
||||||
pub mod script;
|
pub mod script;
|
||||||
pub mod terrain;
|
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