Skip to main content

proof_engine/terrain/
heightmap.rs

1//! Heightmap generation, erosion, post-processing, and analysis.
2//!
3//! This module provides multiple terrain generation algorithms, erosion
4//! simulations, post-processing filters, and analytical tools for working
5//! with 2D height fields used as terrain data.
6
7use glam::{Vec2, Vec3};
8
9// ── Seeded RNG (xoshiro256** — no external deps) ──────────────────────────────
10
11#[derive(Clone, Debug)]
12pub struct Rng {
13    state: [u64; 4],
14}
15
16impl Rng {
17    pub fn new(seed: u64) -> Self {
18        let mut s = seed;
19        let mut next = || {
20            s = s.wrapping_add(0x9e3779b97f4a7c15);
21            let mut z = s;
22            z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
23            z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
24            z ^ (z >> 31)
25        };
26        Self { state: [next(), next(), next(), next()] }
27    }
28
29    fn rol64(x: u64, k: u32) -> u64 { (x << k) | (x >> (64 - k)) }
30
31    fn next_u64(&mut self) -> u64 {
32        let result = Self::rol64(self.state[1].wrapping_mul(5), 7).wrapping_mul(9);
33        let t = self.state[1] << 17;
34        self.state[2] ^= self.state[0];
35        self.state[3] ^= self.state[1];
36        self.state[1] ^= self.state[2];
37        self.state[0] ^= self.state[3];
38        self.state[2] ^= t;
39        self.state[3] = Self::rol64(self.state[3], 45);
40        result
41    }
42
43    fn next_f32(&mut self) -> f32 {
44        (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32
45    }
46
47    fn next_f32_range(&mut self, lo: f32, hi: f32) -> f32 {
48        lo + self.next_f32() * (hi - lo)
49    }
50
51    fn next_usize(&mut self, n: usize) -> usize {
52        (self.next_u64() % n as u64) as usize
53    }
54
55    pub fn next_i32_range(&mut self, lo: i32, hi: i32) -> i32 {
56        if hi <= lo { return lo; }
57        lo + (self.next_u64() % (hi - lo) as u64) as i32
58    }
59}
60
61// ── Perlin-style gradient noise (internal, no external dep) ──────────────────
62
63fn fade(t: f32) -> f32 { t * t * t * (t * (t * 6.0 - 15.0) + 10.0) }
64fn lerp(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
65
66fn grad2(hash: u32, x: f32, y: f32) -> f32 {
67    match hash & 7 {
68        0 =>  x + y, 1 => -x + y, 2 =>  x - y, 3 => -x - y,
69        4 =>  x,     5 => -x,     6 =>  y,      _ => -y,
70    }
71}
72
73/// Minimal self-contained 2D value/gradient noise.
74pub struct GradientNoisePublic {
75    perm: [u8; 512],
76}
77
78impl GradientNoisePublic {
79    pub fn new(seed: u64) -> Self {
80        let inner = GradientNoise::new(seed);
81        Self { perm: inner.perm }
82    }
83    pub fn noise2d(&self, x: f32, y: f32) -> f32 {
84        let inner = GradientNoise { perm: self.perm };
85        inner.noise2d(x, y)
86    }
87}
88
89/// Minimal self-contained 2D value/gradient noise.
90struct GradientNoise {
91    perm: [u8; 512],
92}
93
94impl GradientNoise {
95    fn new(seed: u64) -> Self {
96        let mut rng = Rng::new(seed);
97        let mut p: [u8; 256] = [0u8; 256];
98        for (i, v) in p.iter_mut().enumerate() { *v = i as u8; }
99        // Fisher-Yates shuffle
100        for i in (1..256).rev() {
101            let j = rng.next_usize(i + 1);
102            p.swap(i, j);
103        }
104        let mut perm = [0u8; 512];
105        for i in 0..512 { perm[i] = p[i & 255]; }
106        Self { perm }
107    }
108
109    fn noise2d(&self, x: f32, y: f32) -> f32 {
110        let xi = x.floor() as i32;
111        let yi = y.floor() as i32;
112        let xf = x - x.floor();
113        let yf = y - y.floor();
114        let u = fade(xf);
115        let v = fade(yf);
116        let aa = self.perm[(self.perm[(xi & 255) as usize] as i32 + (yi & 255)) as usize & 511] as u32;
117        let ab = self.perm[(self.perm[(xi & 255) as usize] as i32 + (yi & 255) + 1) as usize & 511] as u32;
118        let ba = self.perm[(self.perm[((xi + 1) & 255) as usize] as i32 + (yi & 255)) as usize & 511] as u32;
119        let bb = self.perm[(self.perm[((xi + 1) & 255) as usize] as i32 + (yi & 255) + 1) as usize & 511] as u32;
120        let x1 = lerp(grad2(aa, xf, yf), grad2(ba, xf - 1.0, yf), u);
121        let x2 = lerp(grad2(ab, xf, yf - 1.0), grad2(bb, xf - 1.0, yf - 1.0), u);
122        (lerp(x1, x2, v) + 1.0) * 0.5
123    }
124}
125
126// ── HeightMap ─────────────────────────────────────────────────────────────────
127
128/// A 2D grid of f32 height values.
129///
130/// Heights are stored in row-major order: `data[y * width + x]`.
131/// Values are typically in [0, 1] but the API does not enforce this.
132#[derive(Clone, Debug)]
133pub struct HeightMap {
134    pub width: usize,
135    pub height: usize,
136    pub data: Vec<f32>,
137}
138
139impl HeightMap {
140    /// Create a new zero-filled heightmap.
141    pub fn new(width: usize, height: usize) -> Self {
142        Self { width, height, data: vec![0.0; width * height] }
143    }
144
145    /// Create from existing data. Panics if `data.len() != width * height`.
146    pub fn from_data(width: usize, height: usize, data: Vec<f32>) -> Self {
147        assert_eq!(data.len(), width * height, "data length mismatch");
148        Self { width, height, data }
149    }
150
151    /// Get height at integer coordinates. Returns 0.0 if out-of-bounds.
152    pub fn get(&self, x: usize, y: usize) -> f32 {
153        if x < self.width && y < self.height {
154            self.data[y * self.width + x]
155        } else {
156            0.0
157        }
158    }
159
160    /// Compute slope magnitude at (x, y) using central differences.
161    pub fn slope_at(&self, x: usize, y: usize) -> f32 {
162        let xl = if x > 0 { x - 1 } else { x };
163        let xr = if x + 1 < self.width { x + 1 } else { x };
164        let yd = if y > 0 { y - 1 } else { y };
165        let yu = if y + 1 < self.height { y + 1 } else { y };
166        let dx = self.get(xr, y) - self.get(xl, y);
167        let dy = self.get(x, yu) - self.get(x, yd);
168        (dx * dx + dy * dy).sqrt()
169    }
170
171    /// Set height at integer coordinates. No-op if out-of-bounds.
172    pub fn set(&mut self, x: usize, y: usize, v: f32) {
173        if x < self.width && y < self.height {
174            self.data[y * self.width + x] = v;
175        }
176    }
177
178    /// Sample with bilinear interpolation at floating-point coordinates.
179    /// Coordinates are clamped to valid range.
180    pub fn sample_bilinear(&self, x: f32, y: f32) -> f32 {
181        let cx = x.clamp(0.0, (self.width  - 1) as f32);
182        let cy = y.clamp(0.0, (self.height - 1) as f32);
183        let x0 = cx.floor() as usize;
184        let y0 = cy.floor() as usize;
185        let x1 = (x0 + 1).min(self.width  - 1);
186        let y1 = (y0 + 1).min(self.height - 1);
187        let tx = cx - x0 as f32;
188        let ty = cy - y0 as f32;
189        let h00 = self.get(x0, y0);
190        let h10 = self.get(x1, y0);
191        let h01 = self.get(x0, y1);
192        let h11 = self.get(x1, y1);
193        lerp(lerp(h00, h10, tx), lerp(h01, h11, tx), ty)
194    }
195
196    /// Sample with cubic (Catmull-Rom) interpolation.
197    pub fn sample_cubic(&self, x: f32, y: f32) -> f32 {
198        let cx = x.clamp(1.0, (self.width  - 2) as f32);
199        let cy = y.clamp(1.0, (self.height - 2) as f32);
200        let x1 = cx.floor() as usize;
201        let y1 = cy.floor() as usize;
202        let tx = cx - x1 as f32;
203        let ty = cy - y1 as f32;
204
205        let catmull = |p0: f32, p1: f32, p2: f32, p3: f32, t: f32| -> f32 {
206            let a = -0.5 * p0 + 1.5 * p1 - 1.5 * p2 + 0.5 * p3;
207            let b =        p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3;
208            let c = -0.5 * p0              + 0.5 * p2;
209            let d = p1;
210            a * t * t * t + b * t * t + c * t + d
211        };
212
213        let row = |yr: usize| {
214            let x0 = if x1 > 0 { x1 - 1 } else { 0 };
215            let x2 = (x1 + 1).min(self.width - 1);
216            let x3 = (x1 + 2).min(self.width - 1);
217            catmull(self.get(x0, yr), self.get(x1, yr), self.get(x2, yr), self.get(x3, yr), tx)
218        };
219
220        let y0 = if y1 > 0 { y1 - 1 } else { 0 };
221        let y2 = (y1 + 1).min(self.height - 1);
222        let y3 = (y1 + 2).min(self.height - 1);
223        catmull(row(y0), row(y1), row(y2), row(y3), ty)
224    }
225
226    /// Compute surface normal at (x, y) from height differences.
227    /// Returns a normalized Vec3 pointing upward.
228    pub fn normal_at(&self, x: usize, y: usize) -> Vec3 {
229        let x0 = if x > 0 { x - 1 } else { 0 };
230        let x2 = (x + 1).min(self.width  - 1);
231        let y0 = if y > 0 { y - 1 } else { 0 };
232        let y2 = (y + 1).min(self.height - 1);
233        let dzdx = (self.get(x2, y) - self.get(x0, y)) / 2.0;
234        let dzdy = (self.get(x, y2) - self.get(x, y0)) / 2.0;
235        Vec3::new(-dzdx, 1.0, -dzdy).normalize()
236    }
237
238    /// Minimum value in the map.
239    pub fn min_value(&self) -> f32 {
240        self.data.iter().cloned().fold(f32::INFINITY, f32::min)
241    }
242
243    /// Maximum value in the map.
244    pub fn max_value(&self) -> f32 {
245        self.data.iter().cloned().fold(f32::NEG_INFINITY, f32::max)
246    }
247
248    /// Normalize values so they span [0, 1].
249    pub fn normalize(&mut self) {
250        let mn = self.min_value();
251        let mx = self.max_value();
252        let range = mx - mn;
253        if range < 1e-9 { return; }
254        for v in self.data.iter_mut() { *v = (*v - mn) / range; }
255    }
256
257    /// Clamp all values to [min, max].
258    pub fn clamp_range(&mut self, min: f32, max: f32) {
259        for v in self.data.iter_mut() { *v = v.clamp(min, max); }
260    }
261
262    /// Box-blur with the given radius (integer cells).
263    pub fn blur(&mut self, radius: usize) {
264        if radius == 0 { return; }
265        let w = self.width;
266        let h = self.height;
267        let mut tmp = vec![0.0f32; w * h];
268        let r = radius as i32;
269        // Horizontal pass
270        for y in 0..h {
271            for x in 0..w {
272                let mut sum = 0.0f32;
273                let mut count = 0;
274                for dx in -r..=r {
275                    let nx = x as i32 + dx;
276                    if nx >= 0 && (nx as usize) < w {
277                        sum += self.data[y * w + nx as usize];
278                        count += 1;
279                    }
280                }
281                tmp[y * w + x] = sum / count as f32;
282            }
283        }
284        // Vertical pass
285        let mut out = vec![0.0f32; w * h];
286        for y in 0..h {
287            for x in 0..w {
288                let mut sum = 0.0f32;
289                let mut count = 0;
290                for dy in -r..=r {
291                    let ny = y as i32 + dy;
292                    if ny >= 0 && (ny as usize) < h {
293                        sum += tmp[ny as usize * w + x];
294                        count += 1;
295                    }
296                }
297                out[y * w + x] = sum / count as f32;
298            }
299        }
300        self.data = out;
301    }
302
303    /// Sharpen using unsharp mask: `original + amount * (original - blurred)`.
304    pub fn sharpen(&mut self, amount: f32) {
305        let mut blurred = self.clone();
306        blurred.blur(2);
307        for (v, b) in self.data.iter_mut().zip(blurred.data.iter()) {
308            *v = (*v + amount * (*v - b)).clamp(0.0, 1.0);
309        }
310    }
311
312    /// Terrace the heightmap into `levels` discrete steps.
313    pub fn terrace(&mut self, levels: usize) {
314        if levels < 2 { return; }
315        let levels_f = levels as f32;
316        for v in self.data.iter_mut() {
317            *v = (*v * levels_f).floor() / (levels_f - 1.0);
318            *v = v.clamp(0.0, 1.0);
319        }
320    }
321
322    /// Apply a radial island mask, fading edges to zero.
323    /// `falloff` controls how quickly edges fade (larger = sharper).
324    pub fn island_mask(&mut self, falloff: f32) {
325        let cx = (self.width  as f32 - 1.0) * 0.5;
326        let cy = (self.height as f32 - 1.0) * 0.5;
327        let max_dist = cx.min(cy);
328        for y in 0..self.height {
329            for x in 0..self.width {
330                let dx = (x as f32 - cx) / max_dist;
331                let dy = (y as f32 - cy) / max_dist;
332                let dist = (dx * dx + dy * dy).sqrt().clamp(0.0, 1.0);
333                let mask = (1.0 - dist.powf(falloff)).clamp(0.0, 1.0);
334                self.data[y * self.width + x] *= mask;
335            }
336        }
337    }
338
339    /// Compute ridge noise by folding the noise into ridges.
340    /// Modifies in place using the existing data as input.
341    pub fn ridge_noise(&mut self, octaves: usize) {
342        let noise = GradientNoise::new(42);
343        let w = self.width;
344        let h = self.height;
345        for y in 0..h {
346            for x in 0..w {
347                let mut amplitude = 1.0f32;
348                let mut frequency = 1.0f32;
349                let mut value = 0.0f32;
350                let mut max_amplitude = 0.0f32;
351                for _oct in 0..octaves {
352                    let nx = x as f32 / w as f32 * frequency;
353                    let ny = y as f32 / h as f32 * frequency;
354                    let n = noise.noise2d(nx * 8.0, ny * 8.0);
355                    // Fold: ridge = 1 - |2n - 1|
356                    let ridge = 1.0 - (2.0 * n - 1.0).abs();
357                    value += ridge * ridge * amplitude;
358                    max_amplitude += amplitude;
359                    amplitude *= 0.5;
360                    frequency *= 2.0;
361                }
362                let existing = self.data[y * w + x];
363                self.data[y * w + x] = (existing + value / max_amplitude).clamp(0.0, 1.0) * 0.5;
364            }
365        }
366    }
367
368    // ── Analysis ─────────────────────────────────────────────────────────────
369
370    /// Compute gradient magnitude (slope) at each cell.
371    pub fn slope_map(&self) -> HeightMap {
372        let mut out = HeightMap::new(self.width, self.height);
373        for y in 0..self.height {
374            for x in 0..self.width {
375                let n = self.normal_at(x, y);
376                // Slope is angle from vertical: 0 = flat, 1 = vertical
377                let slope = 1.0 - n.y.clamp(0.0, 1.0);
378                out.set(x, y, slope);
379            }
380        }
381        out
382    }
383
384    /// Compute curvature (Laplacian) at each cell.
385    /// Positive = convex (hill top), negative = concave (valley).
386    pub fn curvature_map(&self) -> HeightMap {
387        let mut out = HeightMap::new(self.width, self.height);
388        for y in 1..(self.height - 1) {
389            for x in 1..(self.width - 1) {
390                let center = self.get(x, y);
391                let laplacian = self.get(x - 1, y) + self.get(x + 1, y)
392                    + self.get(x, y - 1) + self.get(x, y + 1)
393                    - 4.0 * center;
394                // Normalize to [0,1] (laplacian typically in [-1, 1] range for [0,1] heights)
395                out.set(x, y, (laplacian * 0.5 + 0.5).clamp(0.0, 1.0));
396            }
397        }
398        out
399    }
400
401    /// Compute water flow direction map using D8 steepest descent.
402    /// Returns a map where value encodes direction (0-7 for 8 neighbors, 8 = flat).
403    pub fn flow_map(&self) -> HeightMap {
404        let mut out = HeightMap::new(self.width, self.height);
405        let dirs: [(i32, i32); 8] = [
406            (-1, -1), (0, -1), (1, -1),
407            (-1,  0),          (1,  0),
408            (-1,  1), (0,  1), (1,  1),
409        ];
410        for y in 0..self.height {
411            for x in 0..self.width {
412                let h = self.get(x, y);
413                let mut min_h = h;
414                let mut best_dir = 8usize;
415                for (d, (dx, dy)) in dirs.iter().enumerate() {
416                    let nx = x as i32 + dx;
417                    let ny = y as i32 + dy;
418                    if nx >= 0 && nx < self.width as i32 && ny >= 0 && ny < self.height as i32 {
419                        let nh = self.get(nx as usize, ny as usize);
420                        if nh < min_h {
421                            min_h = nh;
422                            best_dir = d;
423                        }
424                    }
425                }
426                out.set(x, y, best_dir as f32 / 8.0);
427            }
428        }
429        out
430    }
431
432    /// Compute a static shadow map given a sun direction (normalized Vec3).
433    /// Returns 1.0 = lit, 0.0 = shadowed.
434    pub fn shadow_map(&self, sun_dir: Vec3) -> HeightMap {
435        let mut out = HeightMap::new(self.width, self.height);
436        // Initialize all as lit
437        for v in out.data.iter_mut() { *v = 1.0; }
438
439        // Sun direction must go from terrain to sun: invert for ray march direction
440        let step_x = -sun_dir.x / sun_dir.y.abs().max(0.001);
441        let step_z = -sun_dir.z / sun_dir.y.abs().max(0.001);
442        let step_h = 1.0f32; // height units per step
443
444        for y in 0..self.height {
445            for x in 0..self.width {
446                let h0 = self.get(x, y);
447                let mut cx = x as f32;
448                let mut cy = y as f32;
449                let mut horizon_h = h0;
450                for _step in 0..256 {
451                    cx += step_x;
452                    cy += step_z;
453                    horizon_h += step_h * 0.01;
454                    if cx < 0.0 || cx >= self.width as f32 || cy < 0.0 || cy >= self.height as f32 {
455                        break;
456                    }
457                    let terrain_h = self.sample_bilinear(cx, cy);
458                    if terrain_h > horizon_h {
459                        out.set(x, y, 0.0);
460                        break;
461                    }
462                }
463            }
464        }
465        out
466    }
467
468    /// Compute viewshed: for each cell, how many other cells can see it from `observer_height`.
469    /// Returns a normalized visibility count map.
470    pub fn visibility_map(&self, observer_height: f32) -> HeightMap {
471        let mut out = HeightMap::new(self.width, self.height);
472        let w = self.width;
473        let h = self.height;
474        // For each source cell, cast rays to sample points
475        for y in 0..h {
476            for x in 0..w {
477                let eye_h = self.get(x, y) + observer_height;
478                let mut visible_count = 0usize;
479                let total = 64usize; // 64 sample directions
480                for i in 0..total {
481                    let angle = i as f32 * std::f32::consts::TAU / total as f32;
482                    let dir_x = angle.cos();
483                    let dir_y = angle.sin();
484                    let max_dist = (w.min(h) as f32) * 0.5;
485                    let steps = max_dist as usize;
486                    let mut visible = true;
487                    let mut max_slope = f32::NEG_INFINITY;
488                    for s in 1..steps {
489                        let sx = x as f32 + dir_x * s as f32;
490                        let sy = y as f32 + dir_y * s as f32;
491                        if sx < 0.0 || sx >= w as f32 || sy < 0.0 || sy >= h as f32 { break; }
492                        let th = self.sample_bilinear(sx, sy);
493                        let dist = (s as f32).max(1.0);
494                        let slope = (th - eye_h) / dist;
495                        if slope > max_slope {
496                            max_slope = slope;
497                        } else if slope < max_slope - 0.05 {
498                            visible = false;
499                            break;
500                        }
501                    }
502                    if visible { visible_count += 1; }
503                }
504                out.set(x, y, visible_count as f32 / total as f32);
505            }
506        }
507        out
508    }
509
510    // ── Import / Export ───────────────────────────────────────────────────────
511
512    /// Serialize to raw f32 binary (little-endian).
513    /// Format: [width: u32][height: u32][data: f32 * width * height]
514    pub fn to_raw_bytes(&self) -> Vec<u8> {
515        let mut out = Vec::with_capacity(8 + self.data.len() * 4);
516        out.extend_from_slice(&(self.width  as u32).to_le_bytes());
517        out.extend_from_slice(&(self.height as u32).to_le_bytes());
518        for &v in &self.data {
519            out.extend_from_slice(&v.to_le_bytes());
520        }
521        out
522    }
523
524    /// Deserialize from raw f32 binary.
525    pub fn from_raw_bytes(bytes: &[u8]) -> Option<Self> {
526        if bytes.len() < 8 { return None; }
527        let width  = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
528        let height = u32::from_le_bytes(bytes[4..8].try_into().ok()?) as usize;
529        if bytes.len() < 8 + width * height * 4 { return None; }
530        let mut data = Vec::with_capacity(width * height);
531        for i in 0..(width * height) {
532            let off = 8 + i * 4;
533            let v = f32::from_le_bytes(bytes[off..off + 4].try_into().ok()?);
534            data.push(v);
535        }
536        Some(Self { width, height, data })
537    }
538
539    /// Export as 8-bit grayscale PNG bytes (raw PNG, no external dep).
540    /// Uses a minimal PNG encoder.
541    pub fn to_png_8bit(&self) -> Vec<u8> {
542        let pixels: Vec<u8> = self.data.iter()
543            .map(|&v| (v.clamp(0.0, 1.0) * 255.0) as u8)
544            .collect();
545        encode_png_grayscale(self.width as u32, self.height as u32, &pixels, 8)
546    }
547
548    /// Export as 16-bit grayscale PNG bytes.
549    pub fn to_png_16bit(&self) -> Vec<u8> {
550        let pixels: Vec<u8> = self.data.iter().flat_map(|&v| {
551            let val = (v.clamp(0.0, 1.0) * 65535.0) as u16;
552            val.to_be_bytes()
553        }).collect();
554        encode_png_grayscale(self.width as u32, self.height as u32, &pixels, 16)
555    }
556
557    /// Import from 8-bit grayscale raw pixel data.
558    pub fn from_png_8bit(width: usize, height: usize, pixels: &[u8]) -> Self {
559        let data = pixels.iter().map(|&p| p as f32 / 255.0).collect();
560        Self { width, height, data }
561    }
562
563    /// Import from 16-bit big-endian grayscale pixel data.
564    pub fn from_png_16bit(width: usize, height: usize, pixels: &[u8]) -> Self {
565        let mut data = Vec::with_capacity(width * height);
566        for i in 0..(width * height) {
567            let hi = pixels[i * 2] as u16;
568            let lo = pixels[i * 2 + 1] as u16;
569            data.push(((hi << 8) | lo) as f32 / 65535.0);
570        }
571        Self { width, height, data }
572    }
573}
574
575/// Minimal PNG encoder for grayscale images (8 or 16 bit depth).
576/// Implements enough of PNG spec for valid output without external deps.
577fn encode_png_grayscale(width: u32, height: u32, pixels: &[u8], bit_depth: u8) -> Vec<u8> {
578    use std::io::Write;
579
580    // PNG signature
581    let mut out: Vec<u8> = vec![137, 80, 78, 71, 13, 10, 26, 10];
582
583    let write_chunk = |out: &mut Vec<u8>, tag: &[u8; 4], data: &[u8]| {
584        let len = data.len() as u32;
585        out.extend_from_slice(&len.to_be_bytes());
586        out.extend_from_slice(tag);
587        out.extend_from_slice(data);
588        let crc = png_crc(tag, data);
589        out.extend_from_slice(&crc.to_be_bytes());
590    };
591
592    // IHDR
593    let mut ihdr = Vec::new();
594    let _ = ihdr.write_all(&width.to_be_bytes());
595    let _ = ihdr.write_all(&height.to_be_bytes());
596    ihdr.push(bit_depth); // bit depth
597    ihdr.push(0);  // color type: grayscale
598    ihdr.push(0);  // compression
599    ihdr.push(0);  // filter
600    ihdr.push(0);  // interlace
601    write_chunk(&mut out, b"IHDR", &ihdr);
602
603    // IDAT: filter type 0 (None) for each row
604    let bytes_per_pixel = if bit_depth == 16 { 2 } else { 1 };
605    let row_bytes = width as usize * bytes_per_pixel;
606    let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
607    for row in 0..height as usize {
608        raw.push(0); // filter type None
609        raw.extend_from_slice(&pixels[row * row_bytes..(row + 1) * row_bytes]);
610    }
611    let compressed = deflate_no_compress(&raw);
612    write_chunk(&mut out, b"IDAT", &compressed);
613
614    // IEND
615    write_chunk(&mut out, b"IEND", &[]);
616
617    out
618}
619
620/// CRC32 for PNG chunks.
621fn png_crc(tag: &[u8], data: &[u8]) -> u32 {
622    let table = crc32_table();
623    let mut crc = 0xFFFF_FFFFu32;
624    for &b in tag.iter().chain(data.iter()) {
625        crc = (crc >> 8) ^ table[((crc ^ b as u32) & 0xFF) as usize];
626    }
627    !crc
628}
629
630fn crc32_table() -> [u32; 256] {
631    let mut table = [0u32; 256];
632    for n in 0..256u32 {
633        let mut c = n;
634        for _ in 0..8 { c = if c & 1 != 0 { 0xEDB88320 ^ (c >> 1) } else { c >> 1 }; }
635        table[n as usize] = c;
636    }
637    table
638}
639
640/// Minimal deflate "store" (no compression) implementation for PNG IDAT.
641fn deflate_no_compress(data: &[u8]) -> Vec<u8> {
642    // zlib header: CMF=0x78, FLG=0x01 (no dict, check bits)
643    let mut out: Vec<u8> = vec![0x78, 0x01];
644    const BLOCK_SIZE: usize = 65535;
645    let mut pos = 0;
646    while pos < data.len() {
647        let end = (pos + BLOCK_SIZE).min(data.len());
648        let is_last = end == data.len();
649        out.push(if is_last { 1 } else { 0 }); // BFINAL | (BTYPE=0 << 1)
650        let len = (end - pos) as u16;
651        let nlen = !len;
652        out.extend_from_slice(&len.to_le_bytes());
653        out.extend_from_slice(&nlen.to_le_bytes());
654        out.extend_from_slice(&data[pos..end]);
655        pos = end;
656    }
657    if data.is_empty() {
658        out.push(1); // final block
659        out.extend_from_slice(&[0, 0, 0xFF, 0xFF]); // len=0, nlen=~0
660    }
661    // Adler-32 checksum
662    let (s1, s2) = data.iter().fold((1u32, 0u32), |(s1, s2), &b| {
663        let s1 = (s1 + b as u32) % 65521;
664        ((s1 + s2) % 65521, (s1 + s2) % 65521)
665    });
666    let adler = (s2 << 16) | s1;
667    out.extend_from_slice(&adler.to_be_bytes());
668    out
669}
670
671// ── Diamond-Square Algorithm ──────────────────────────────────────────────────
672
673/// Generates terrain using the Diamond-Square (Midpoint Displacement) algorithm.
674pub struct DiamondSquare;
675
676impl DiamondSquare {
677    /// Generate a heightmap of size `(size+1) x (size+1)` where `size` must be a power of 2.
678    /// `roughness` controls fractal dimension (0.0 = smooth, 1.0 = rough).
679    pub fn generate(size: usize, roughness: f32, seed: u64) -> HeightMap {
680        let n = size + 1;
681        let mut rng = Rng::new(seed);
682        let mut map = HeightMap::new(n, n);
683
684        // Seed corners
685        map.set(0,    0,    rng.next_f32());
686        map.set(size, 0,    rng.next_f32());
687        map.set(0,    size, rng.next_f32());
688        map.set(size, size, rng.next_f32());
689
690        let mut step = size;
691        let mut scale = roughness;
692
693        while step > 1 {
694            let half = step / 2;
695
696            // Diamond step
697            let mut y = 0;
698            while y < size {
699                let mut x = 0;
700                while x < size {
701                    let avg = (map.get(x, y)
702                        + map.get(x + step, y)
703                        + map.get(x, y + step)
704                        + map.get(x + step, y + step)) * 0.25;
705                    let rand = rng.next_f32_range(-scale, scale);
706                    map.set(x + half, y + half, (avg + rand).clamp(0.0, 1.0));
707                    x += step;
708                }
709                y += step;
710            }
711
712            // Square step
713            let mut y = 0;
714            while y <= size {
715                let mut x = (y / half % 2) * half;
716                while x <= size {
717                    let mut sum = 0.0f32;
718                    let mut count = 0;
719                    if x >= half {
720                        sum += map.get(x - half, y);
721                        count += 1;
722                    }
723                    if x + half <= size {
724                        sum += map.get(x + half, y);
725                        count += 1;
726                    }
727                    if y >= half {
728                        sum += map.get(x, y - half);
729                        count += 1;
730                    }
731                    if y + half <= size {
732                        sum += map.get(x, y + half);
733                        count += 1;
734                    }
735                    let avg = if count > 0 { sum / count as f32 } else { 0.5 };
736                    let rand = rng.next_f32_range(-scale, scale);
737                    map.set(x, y, (avg + rand).clamp(0.0, 1.0));
738                    x += step;
739                }
740                y += half;
741            }
742
743            step = half;
744            scale *= 2.0f32.powf(-roughness);
745        }
746        map
747    }
748}
749
750// ── Fractal Noise ─────────────────────────────────────────────────────────────
751
752/// Generates terrain using layered (fractal) gradient noise.
753pub struct FractalNoise;
754
755impl FractalNoise {
756    /// Generate a heightmap.
757    ///
758    /// - `octaves`: number of noise layers (4–8 typical)
759    /// - `lacunarity`: frequency multiplier per octave (2.0 typical)
760    /// - `persistence`: amplitude multiplier per octave (0.5 typical)
761    /// - `scale`: overall frequency scale
762    pub fn generate(
763        width: usize,
764        height: usize,
765        octaves: usize,
766        lacunarity: f32,
767        persistence: f32,
768        scale: f32,
769        seed: u64,
770    ) -> HeightMap {
771        let noise = GradientNoise::new(seed);
772        let mut map = HeightMap::new(width, height);
773        for y in 0..height {
774            for x in 0..width {
775                let mut value = 0.0f32;
776                let mut amplitude = 1.0f32;
777                let mut frequency = scale;
778                let mut max_val = 0.0f32;
779                for _oct in 0..octaves {
780                    let nx = x as f32 / width  as f32 * frequency;
781                    let ny = y as f32 / height as f32 * frequency;
782                    value += noise.noise2d(nx, ny) * amplitude;
783                    max_val += amplitude;
784                    amplitude *= persistence;
785                    frequency *= lacunarity;
786                }
787                map.set(x, y, (value / max_val).clamp(0.0, 1.0));
788            }
789        }
790        map
791    }
792
793    /// Generate fractal Brownian motion terrain with domain warping.
794    pub fn generate_warped(
795        width: usize,
796        height: usize,
797        octaves: usize,
798        seed: u64,
799    ) -> HeightMap {
800        let noise1 = GradientNoise::new(seed);
801        let noise2 = GradientNoise::new(seed.wrapping_add(137));
802        let noise3 = GradientNoise::new(seed.wrapping_add(271));
803        let mut map = HeightMap::new(width, height);
804        for y in 0..height {
805            for x in 0..width {
806                let px = x as f32 / width  as f32 * 4.0;
807                let py = y as f32 / height as f32 * 4.0;
808                // First warp layer
809                let wx = noise1.noise2d(px, py) * 2.0 - 1.0;
810                let wy = noise2.noise2d(px + 5.2, py + 1.3) * 2.0 - 1.0;
811                // Second warp layer
812                let wx2 = noise2.noise2d(px + wx, py + wy);
813                let wy2 = noise3.noise2d(px + wx + 1.7, py + wy + 9.2);
814                // Final sample
815                let mut value = 0.0f32;
816                let mut amp = 1.0f32;
817                let mut freq = 1.0f32;
818                let mut max_amp = 0.0f32;
819                for _oct in 0..octaves {
820                    let sx = (px + wx2) * freq;
821                    let sy = (py + wy2) * freq;
822                    value += noise3.noise2d(sx, sy) * amp;
823                    max_amp += amp;
824                    amp *= 0.5;
825                    freq *= 2.0;
826                }
827                map.set(x, y, (value / max_amp).clamp(0.0, 1.0));
828            }
829        }
830        map
831    }
832}
833
834// ── Voronoi Plates ────────────────────────────────────────────────────────────
835
836/// Simulates tectonic plates using Voronoi diagrams.
837pub struct VoronoiPlates;
838
839impl VoronoiPlates {
840    /// Generate tectonic plate terrain.
841    ///
842    /// `num_plates`: number of tectonic plates (8–32 typical).
843    /// Plate boundaries create mountain ranges, interiors become plains/oceans.
844    pub fn generate(width: usize, height: usize, num_plates: usize, seed: u64) -> HeightMap {
845        let mut rng = Rng::new(seed);
846
847        // Generate plate centers with random positions
848        let mut centers: Vec<(f32, f32)> = (0..num_plates)
849            .map(|_| (rng.next_f32() * width as f32, rng.next_f32() * height as f32))
850            .collect();
851
852        // Assign each plate a base elevation (ocean vs continent)
853        let plate_elevations: Vec<f32> = (0..num_plates)
854            .map(|_| if rng.next_f32() < 0.4 { rng.next_f32_range(0.0, 0.35) } else { rng.next_f32_range(0.4, 0.7) })
855            .collect();
856
857        // Assign plate movement vectors
858        let plate_velocities: Vec<(f32, f32)> = (0..num_plates)
859            .map(|_| {
860                let angle = rng.next_f32() * std::f32::consts::TAU;
861                (angle.cos() * 0.5, angle.sin() * 0.5)
862            })
863            .collect();
864
865        // For each cell, find nearest plate and second-nearest for boundaries
866        let mut map = HeightMap::new(width, height);
867        for y in 0..height {
868            for x in 0..width {
869                let px = x as f32;
870                let py = y as f32;
871                let mut dists: Vec<(f32, usize)> = centers.iter().enumerate()
872                    .map(|(i, &(cx, cy))| {
873                        let dx = px - cx;
874                        let dy = py - cy;
875                        (dx * dx + dy * dy, i)
876                    })
877                    .collect();
878                dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
879                let (d0, p0) = dists[0];
880                let (d1, _p1) = dists[1];
881                let base_h = plate_elevations[p0];
882                // Boundary = close to Voronoi edge
883                let boundary_t = 1.0 - ((d1.sqrt() - d0.sqrt()) / (width.min(height) as f32 * 0.05)).clamp(0.0, 1.0);
884                // Convergent boundary → mountains, divergent → rifts
885                let v0 = plate_velocities[p0];
886                let dir = ((px - centers[p0].0).signum(), (py - centers[p0].1).signum());
887                let convergence = -(v0.0 * dir.0 + v0.1 * dir.1).clamp(-1.0, 1.0);
888                let mountain_bonus = boundary_t * convergence.max(0.0) * 0.4;
889                let rift_penalty   = boundary_t * (-convergence).max(0.0) * 0.15;
890                let h = (base_h + mountain_bonus - rift_penalty).clamp(0.0, 1.0);
891                map.set(x, y, h);
892            }
893        }
894
895        // Slight blur for natural-looking plates
896        map.blur(3);
897        map.normalize();
898        map
899    }
900}
901
902// ── Perlin Terrain ────────────────────────────────────────────────────────────
903
904/// Multi-octave Perlin terrain with additional continental shaping.
905pub struct PerlinTerrain;
906
907impl PerlinTerrain {
908    /// Generate terrain by combining multiple Perlin noise octaves.
909    pub fn generate(
910        width: usize,
911        height: usize,
912        octaves: usize,
913        scale: f32,
914        seed: u64,
915    ) -> HeightMap {
916        // Continental base: low-frequency shape
917        let base = FractalNoise::generate(width, height, 2, 2.0, 0.5, scale * 0.2, seed);
918        // Detail: higher frequency features
919        let detail = FractalNoise::generate(width, height, octaves, 2.1, 0.48, scale, seed.wrapping_add(9999));
920        let mut map = HeightMap::new(width, height);
921        for i in 0..(width * height) {
922            // Continental shape dominates, detail adds local variation
923            map.data[i] = (base.data[i] * 0.6 + detail.data[i] * 0.4).clamp(0.0, 1.0);
924        }
925        map.normalize();
926        map
927    }
928
929    /// Generate terrain with mountain ridges using warped noise + ridge fold.
930    pub fn generate_with_ridges(width: usize, height: usize, seed: u64) -> HeightMap {
931        let mut base = FractalNoise::generate_warped(width, height, 6, seed);
932        base.ridge_noise(4);
933        base.normalize();
934        base
935    }
936}
937
938// ── Hydraulic Erosion ─────────────────────────────────────────────────────────
939
940/// Simulates water-driven erosion.
941pub struct HydraulicErosion;
942
943impl HydraulicErosion {
944    /// Erode a heightmap using particle-based hydraulic erosion.
945    ///
946    /// Each iteration simulates a water droplet that flows downhill,
947    /// eroding and depositing sediment.
948    pub fn erode(
949        map: &mut HeightMap,
950        iterations: usize,
951        rain_amount: f32,
952        sediment_capacity: f32,
953        evaporation: f32,
954        seed: u64,
955    ) {
956        let mut rng = Rng::new(seed);
957        let w = map.width;
958        let h = map.height;
959
960        for _ in 0..iterations {
961            // Spawn droplet at random position
962            let mut px = rng.next_f32() * (w - 2) as f32 + 1.0;
963            let mut py = rng.next_f32() * (h - 2) as f32 + 1.0;
964            let mut water = rain_amount;
965            let mut sediment = 0.0f32;
966            let mut vel_x = 0.0f32;
967            let mut vel_y = 0.0f32;
968            let inertia = 0.3f32;
969            let gravity = 4.0f32;
970            let erosion_speed = 0.3f32;
971            let deposit_speed = 0.3f32;
972
973            for _step in 0..128 {
974                let xi = px as usize;
975                let yi = py as usize;
976                if xi + 1 >= w || yi + 1 >= h { break; }
977
978                // Compute gradient
979                let tx = px - xi as f32;
980                let ty = py - yi as f32;
981                let h00 = map.get(xi, yi);
982                let h10 = map.get(xi + 1, yi);
983                let h01 = map.get(xi, yi + 1);
984                let h11 = map.get(xi + 1, yi + 1);
985                let gx = (h10 - h00) * (1.0 - ty) + (h11 - h01) * ty;
986                let gy = (h01 - h00) * (1.0 - tx) + (h11 - h10) * tx;
987                let height_at = h00 * (1.0 - tx) * (1.0 - ty)
988                    + h10 * tx * (1.0 - ty)
989                    + h01 * (1.0 - tx) * ty
990                    + h11 * tx * ty;
991
992                // Update velocity
993                vel_x = vel_x * inertia - gx * (1.0 - inertia) * gravity;
994                vel_y = vel_y * inertia - gy * (1.0 - inertia) * gravity;
995                let speed = (vel_x * vel_x + vel_y * vel_y).sqrt().max(0.001);
996                vel_x /= speed;
997                vel_y /= speed;
998
999                let new_px = px + vel_x;
1000                let new_py = py + vel_y;
1001                if new_px < 1.0 || new_px >= (w - 1) as f32 || new_py < 1.0 || new_py >= (h - 1) as f32 { break; }
1002
1003                let new_h = map.sample_bilinear(new_px, new_py);
1004                let delta_h = new_h - height_at;
1005
1006                let capacity = sediment_capacity * speed * water * (-delta_h).max(0.001);
1007                if sediment > capacity || delta_h > 0.0 {
1008                    // Deposit sediment
1009                    let deposit = if delta_h > 0.0 {
1010                        delta_h.min(sediment)
1011                    } else {
1012                        (sediment - capacity) * deposit_speed
1013                    };
1014                    sediment -= deposit;
1015                    // Spread deposit around current cell
1016                    let w0 = (1.0 - tx) * (1.0 - ty);
1017                    let w1 = tx * (1.0 - ty);
1018                    let w2 = (1.0 - tx) * ty;
1019                    let w3 = tx * ty;
1020                    let cur00 = map.get(xi, yi);
1021                    let cur10 = map.get(xi + 1, yi);
1022                    let cur01 = map.get(xi, yi + 1);
1023                    let cur11 = map.get(xi + 1, yi + 1);
1024                    map.set(xi,     yi,     (cur00 + deposit * w0).clamp(0.0, 1.0));
1025                    map.set(xi + 1, yi,     (cur10 + deposit * w1).clamp(0.0, 1.0));
1026                    map.set(xi,     yi + 1, (cur01 + deposit * w2).clamp(0.0, 1.0));
1027                    map.set(xi + 1, yi + 1, (cur11 + deposit * w3).clamp(0.0, 1.0));
1028                } else {
1029                    // Erode
1030                    let erode_amount = (capacity - sediment) * erosion_speed;
1031                    let erode_radius = 2usize;
1032                    let mut total_weight = 0.0f32;
1033                    let mut weights = [[0.0f32; 5]; 5];
1034                    for dy in 0..=erode_radius * 2 {
1035                        for dx in 0..=erode_radius * 2 {
1036                            let ddx = dx as i32 - erode_radius as i32;
1037                            let ddy = dy as i32 - erode_radius as i32;
1038                            let w = (1.0 - (ddx * ddx + ddy * ddy) as f32 / (erode_radius * erode_radius + 1) as f32).max(0.0);
1039                            weights[dy][dx] = w;
1040                            total_weight += w;
1041                        }
1042                    }
1043                    for dy in 0..=erode_radius * 2 {
1044                        for dx in 0..=erode_radius * 2 {
1045                            let ddx = dx as i32 - erode_radius as i32;
1046                            let ddy = dy as i32 - erode_radius as i32;
1047                            let nx = (xi as i32 + ddx) as usize;
1048                            let ny = (yi as i32 + ddy) as usize;
1049                            if nx < w && ny < h {
1050                                let w = weights[dy][dx] / total_weight;
1051                                let cur = map.get(nx, ny);
1052                                map.set(nx, ny, (cur - erode_amount * w).clamp(0.0, 1.0));
1053                            }
1054                        }
1055                    }
1056                    sediment += erode_amount;
1057                }
1058
1059                water *= 1.0 - evaporation;
1060                if water < 0.001 { break; }
1061                px = new_px;
1062                py = new_py;
1063            }
1064        }
1065    }
1066}
1067
1068// ── Thermal Erosion ───────────────────────────────────────────────────────────
1069
1070/// Simulates thermal weathering (slope-driven material movement).
1071pub struct ThermalErosion;
1072
1073impl ThermalErosion {
1074    /// Erode by moving material downhill when slope exceeds `talus_angle` (in height units).
1075    pub fn erode(map: &mut HeightMap, iterations: usize, talus_angle: f32) {
1076        let w = map.width;
1077        let h = map.height;
1078        let dirs: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
1079        for _ in 0..iterations {
1080            for y in 0..h {
1081                for x in 0..w {
1082                    let h0 = map.get(x, y);
1083                    let mut total_diff = 0.0f32;
1084                    let mut max_diff = 0.0f32;
1085                    for (dx, dy) in &dirs {
1086                        let nx = x as i32 + dx;
1087                        let ny = y as i32 + dy;
1088                        if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1089                            let nh = map.get(nx as usize, ny as usize);
1090                            let diff = h0 - nh;
1091                            if diff > talus_angle {
1092                                total_diff += diff - talus_angle;
1093                                if diff > max_diff { max_diff = diff; }
1094                            }
1095                        }
1096                    }
1097                    if total_diff <= 0.0 { continue; }
1098                    for (dx, dy) in &dirs {
1099                        let nx = x as i32 + dx;
1100                        let ny = y as i32 + dy;
1101                        if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1102                            let nh = map.get(nx as usize, ny as usize);
1103                            let diff = h0 - nh;
1104                            if diff > talus_angle {
1105                                let frac = (diff - talus_angle) / total_diff;
1106                                let transfer = frac * (diff - talus_angle) * 0.5;
1107                                let cur0 = map.get(x, y);
1108                                let cur_n = map.get(nx as usize, ny as usize);
1109                                map.set(x, y, (cur0 - transfer).clamp(0.0, 1.0));
1110                                map.set(nx as usize, ny as usize, (cur_n + transfer).clamp(0.0, 1.0));
1111                            }
1112                        }
1113                    }
1114                }
1115            }
1116        }
1117    }
1118}
1119
1120// ── Wind Erosion ──────────────────────────────────────────────────────────────
1121
1122/// Simulates aeolian (wind-driven) erosion and deposition.
1123pub struct WindErosion;
1124
1125impl WindErosion {
1126    /// Erode map with wind coming from `wind_dir` direction.
1127    /// Wind picks up material from windward slopes and deposits on lee slopes.
1128    pub fn erode(map: &mut HeightMap, wind_dir: Vec2, iterations: usize) {
1129        let w = map.width;
1130        let h = map.height;
1131        let wind = wind_dir.normalize_or_zero();
1132        let step_x = wind.x;
1133        let step_y = wind.y;
1134        let saltation_dist = 3usize;
1135        let erosion_rate = 0.002f32;
1136        let deposition_rate = 0.003f32;
1137
1138        for _ in 0..iterations {
1139            let mut delta = vec![0.0f32; w * h];
1140            for y in 0..h {
1141                for x in 0..w {
1142                    let h0 = map.get(x, y);
1143                    // Check upwind cell
1144                    let ux = x as f32 - step_x * saltation_dist as f32;
1145                    let uy = y as f32 - step_y * saltation_dist as f32;
1146                    if ux >= 0.0 && ux < w as f32 && uy >= 0.0 && uy < h as f32 {
1147                        let upwind_h = map.sample_bilinear(ux, uy);
1148                        if upwind_h > h0 {
1149                            // Pick up material from upwind
1150                            let source_xi = ux as usize;
1151                            let source_yi = uy as usize;
1152                            let erode = erosion_rate * (upwind_h - h0);
1153                            if source_xi < w && source_yi < h {
1154                                delta[source_yi * w + source_xi] -= erode;
1155                                delta[y * w + x] += erode * (1.0 - deposition_rate);
1156                            }
1157                        }
1158                    }
1159                }
1160            }
1161            for i in 0..(w * h) {
1162                map.data[i] = (map.data[i] + delta[i]).clamp(0.0, 1.0);
1163            }
1164        }
1165        map.blur(1);
1166    }
1167}
1168
1169// ── Tests ─────────────────────────────────────────────────────────────────────
1170
1171#[cfg(test)]
1172mod tests {
1173    use super::*;
1174
1175    #[test]
1176    fn test_heightmap_new() {
1177        let m = HeightMap::new(64, 64);
1178        assert_eq!(m.data.len(), 64 * 64);
1179        assert!(m.data.iter().all(|&v| v == 0.0));
1180    }
1181
1182    #[test]
1183    fn test_heightmap_get_set() {
1184        let mut m = HeightMap::new(16, 16);
1185        m.set(3, 7, 0.75);
1186        assert_eq!(m.get(3, 7), 0.75);
1187        assert_eq!(m.get(100, 100), 0.0); // out of bounds
1188    }
1189
1190    #[test]
1191    fn test_bilinear_sampling() {
1192        let mut m = HeightMap::new(4, 4);
1193        m.set(1, 1, 1.0);
1194        let v = m.sample_bilinear(1.5, 1.5);
1195        assert!(v > 0.0 && v <= 1.0);
1196    }
1197
1198    #[test]
1199    fn test_cubic_sampling() {
1200        let mut m = HeightMap::new(8, 8);
1201        m.set(3, 3, 0.8);
1202        let v = m.sample_cubic(3.2, 3.2);
1203        // Just check it doesn't panic and is in reasonable range
1204        assert!(v >= -0.5 && v <= 1.5);
1205    }
1206
1207    #[test]
1208    fn test_normalize() {
1209        let mut m = HeightMap::new(4, 4);
1210        for (i, v) in m.data.iter_mut().enumerate() { *v = i as f32; }
1211        m.normalize();
1212        let mn = m.min_value();
1213        let mx = m.max_value();
1214        assert!((mn - 0.0).abs() < 1e-5);
1215        assert!((mx - 1.0).abs() < 1e-5);
1216    }
1217
1218    #[test]
1219    fn test_blur() {
1220        let mut m = HeightMap::new(32, 32);
1221        m.data[16 * 32 + 16] = 1.0;
1222        m.blur(3);
1223        // After blur, center should be reduced and neighbors increased
1224        assert!(m.get(16, 16) < 1.0);
1225        assert!(m.get(15, 16) > 0.0);
1226    }
1227
1228    #[test]
1229    fn test_terrace() {
1230        let mut m = HeightMap::new(16, 16);
1231        for (i, v) in m.data.iter_mut().enumerate() { *v = i as f32 / (16 * 16) as f32; }
1232        m.terrace(4);
1233        let unique: Vec<f32> = {
1234            let mut vals: Vec<f32> = m.data.clone();
1235            vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
1236            vals.dedup();
1237            vals
1238        };
1239        assert!(unique.len() <= 5, "terrace should produce at most levels+1 unique values");
1240    }
1241
1242    #[test]
1243    fn test_island_mask() {
1244        let mut m = HeightMap::new(64, 64);
1245        for v in m.data.iter_mut() { *v = 1.0; }
1246        m.island_mask(2.0);
1247        assert!(m.get(0, 0) < 0.1);
1248        // Center should remain high
1249        assert!(m.get(32, 32) > 0.5);
1250    }
1251
1252    #[test]
1253    fn test_diamond_square() {
1254        let m = DiamondSquare::generate(64, 0.7, 42);
1255        assert_eq!(m.width, 65);
1256        assert_eq!(m.height, 65);
1257        let mn = m.min_value();
1258        let mx = m.max_value();
1259        assert!(mn >= 0.0 && mx <= 1.0);
1260    }
1261
1262    #[test]
1263    fn test_fractal_noise() {
1264        let m = FractalNoise::generate(64, 64, 6, 2.0, 0.5, 4.0, 12345);
1265        assert_eq!(m.data.len(), 64 * 64);
1266        assert!(m.min_value() >= 0.0);
1267        assert!(m.max_value() <= 1.0);
1268    }
1269
1270    #[test]
1271    fn test_voronoi_plates() {
1272        let m = VoronoiPlates::generate(64, 64, 8, 99);
1273        assert_eq!(m.data.len(), 64 * 64);
1274        let mn = m.min_value();
1275        let mx = m.max_value();
1276        assert!(mn >= 0.0 && mx <= 1.0);
1277    }
1278
1279    #[test]
1280    fn test_perlin_terrain() {
1281        let m = PerlinTerrain::generate(64, 64, 6, 4.0, 7);
1282        assert_eq!(m.data.len(), 64 * 64);
1283        let mn = m.min_value();
1284        let mx = m.max_value();
1285        assert!((mn - 0.0).abs() < 1e-4);
1286        assert!((mx - 1.0).abs() < 1e-4);
1287    }
1288
1289    #[test]
1290    fn test_hydraulic_erosion() {
1291        let mut m = DiamondSquare::generate(32, 0.8, 1);
1292        let before: f32 = m.data.iter().sum();
1293        HydraulicErosion::erode(&mut m, 500, 1.0, 8.0, 0.05, 77);
1294        let after: f32 = m.data.iter().sum();
1295        // Erosion should generally reduce total height (more deposition than formation)
1296        // Just check it doesn't panic and values remain valid
1297        assert!(m.min_value() >= 0.0);
1298        assert!(m.max_value() <= 1.0);
1299        let _ = (before, after);
1300    }
1301
1302    #[test]
1303    fn test_thermal_erosion() {
1304        let mut m = DiamondSquare::generate(32, 0.9, 5);
1305        ThermalErosion::erode(&mut m, 20, 0.05);
1306        assert!(m.min_value() >= 0.0);
1307        assert!(m.max_value() <= 1.0);
1308    }
1309
1310    #[test]
1311    fn test_wind_erosion() {
1312        let mut m = DiamondSquare::generate(32, 0.7, 3);
1313        WindErosion::erode(&mut m, Vec2::new(1.0, 0.3), 10);
1314        assert!(m.min_value() >= 0.0);
1315        assert!(m.max_value() <= 1.0);
1316    }
1317
1318    #[test]
1319    fn test_slope_map() {
1320        let m = DiamondSquare::generate(32, 0.7, 42);
1321        let s = m.slope_map();
1322        assert_eq!(s.data.len(), m.data.len());
1323        assert!(s.min_value() >= 0.0);
1324        assert!(s.max_value() <= 1.0);
1325    }
1326
1327    #[test]
1328    fn test_raw_bytes_roundtrip() {
1329        let m = FractalNoise::generate(16, 16, 4, 2.0, 0.5, 3.0, 42);
1330        let bytes = m.to_raw_bytes();
1331        let m2 = HeightMap::from_raw_bytes(&bytes).unwrap();
1332        assert_eq!(m.width, m2.width);
1333        assert_eq!(m.height, m2.height);
1334        for (a, b) in m.data.iter().zip(m2.data.iter()) {
1335            assert!((a - b).abs() < 1e-6);
1336        }
1337    }
1338
1339    #[test]
1340    fn test_png_8bit() {
1341        let m = FractalNoise::generate(16, 16, 4, 2.0, 0.5, 3.0, 42);
1342        let png = m.to_png_8bit();
1343        // PNG signature is 8 bytes
1344        assert!(png.len() > 8);
1345        assert_eq!(&png[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
1346    }
1347
1348    #[test]
1349    fn test_normal_at() {
1350        let m = DiamondSquare::generate(32, 0.7, 42);
1351        let n = m.normal_at(16, 16);
1352        assert!((n.length() - 1.0).abs() < 1e-4);
1353    }
1354}
1355
1356// ── Extended HeightMap Operations ─────────────────────────────────────────────
1357
1358impl HeightMap {
1359    /// Compute the mean (average) value across all cells.
1360    pub fn mean(&self) -> f32 {
1361        if self.data.is_empty() { return 0.0; }
1362        self.data.iter().sum::<f32>() / self.data.len() as f32
1363    }
1364
1365    /// Compute the variance of height values.
1366    pub fn variance(&self) -> f32 {
1367        if self.data.is_empty() { return 0.0; }
1368        let mean = self.mean();
1369        self.data.iter().map(|&v| (v - mean).powi(2)).sum::<f32>() / self.data.len() as f32
1370    }
1371
1372    /// Standard deviation of height values.
1373    pub fn std_dev(&self) -> f32 { self.variance().sqrt() }
1374
1375    /// Invert heights: new_value = 1.0 - old_value.
1376    pub fn invert(&mut self) {
1377        for v in self.data.iter_mut() { *v = 1.0 - *v; }
1378    }
1379
1380    /// Add a constant offset to all values (clamped to [0,1]).
1381    pub fn offset(&mut self, delta: f32) {
1382        for v in self.data.iter_mut() { *v = (*v + delta).clamp(0.0, 1.0); }
1383    }
1384
1385    /// Scale all values by a multiplier (clamped to [0,1]).
1386    pub fn scale(&mut self, factor: f32) {
1387        for v in self.data.iter_mut() { *v = (*v * factor).clamp(0.0, 1.0); }
1388    }
1389
1390    /// Element-wise addition of another heightmap. Maps must be same size.
1391    pub fn add(&mut self, other: &HeightMap, weight: f32) {
1392        assert_eq!(self.data.len(), other.data.len(), "HeightMap::add size mismatch");
1393        for (a, &b) in self.data.iter_mut().zip(other.data.iter()) {
1394            *a = (*a + b * weight).clamp(0.0, 1.0);
1395        }
1396    }
1397
1398    /// Element-wise multiplication (mask).
1399    pub fn multiply(&mut self, other: &HeightMap) {
1400        assert_eq!(self.data.len(), other.data.len(), "HeightMap::multiply size mismatch");
1401        for (a, &b) in self.data.iter_mut().zip(other.data.iter()) {
1402            *a = (*a * b).clamp(0.0, 1.0);
1403        }
1404    }
1405
1406    /// Compute percentile value (e.g., 0.5 = median).
1407    pub fn percentile(&self, p: f32) -> f32 {
1408        if self.data.is_empty() { return 0.0; }
1409        let mut sorted = self.data.clone();
1410        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1411        let idx = ((p.clamp(0.0, 1.0)) * (sorted.len() - 1) as f32) as usize;
1412        sorted[idx]
1413    }
1414
1415    /// Apply a histogram equalization to redistribute height values.
1416    pub fn equalize(&mut self) {
1417        let n = self.data.len();
1418        if n == 0 { return; }
1419        let mut indexed: Vec<(f32, usize)> = self.data.iter().copied().enumerate().map(|(i, v)| (v, i)).collect();
1420        indexed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
1421        for (rank, (_, idx)) in indexed.iter().enumerate() {
1422            self.data[*idx] = rank as f32 / (n - 1) as f32;
1423        }
1424    }
1425
1426    /// Compute a height histogram with `bins` buckets. Returns (bin_edges, counts).
1427    pub fn histogram(&self, bins: usize) -> Vec<usize> {
1428        let mut counts = vec![0usize; bins];
1429        for &v in &self.data {
1430            let bin = ((v.clamp(0.0, 1.0)) * (bins - 1) as f32) as usize;
1431            counts[bin] += 1;
1432        }
1433        counts
1434    }
1435
1436    /// Erode using a simple "drop" model: any cell higher than its lowest neighbor
1437    /// by more than `threshold` transfers `rate` of material downhill.
1438    pub fn simple_erode(&mut self, iterations: usize, threshold: f32, rate: f32) {
1439        let w = self.width;
1440        let h = self.height;
1441        let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1442        for _ in 0..iterations {
1443            for y in 0..h {
1444                for x in 0..w {
1445                    let cur = self.get(x, y);
1446                    let mut lowest_h = cur;
1447                    let mut lowest_nx = x as i32;
1448                    let mut lowest_ny = y as i32;
1449                    for (dx, dy) in &dirs {
1450                        let nx = x as i32 + dx;
1451                        let ny = y as i32 + dy;
1452                        if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1453                            let nh = self.get(nx as usize, ny as usize);
1454                            if nh < lowest_h {
1455                                lowest_h = nh;
1456                                lowest_nx = nx;
1457                                lowest_ny = ny;
1458                            }
1459                        }
1460                    }
1461                    let diff = cur - lowest_h;
1462                    if diff > threshold {
1463                        let transfer = diff * rate;
1464                        self.set(x, y, (cur - transfer).clamp(0.0, 1.0));
1465                        self.set(lowest_nx as usize, lowest_ny as usize,
1466                            (lowest_h + transfer).clamp(0.0, 1.0));
1467                    }
1468                }
1469            }
1470        }
1471    }
1472
1473    /// Generate a color-mapped image of the heightmap as RGB bytes.
1474    /// Uses a standard terrain color ramp: deep water → water → sand → grass → rock → snow.
1475    pub fn to_color_bytes(&self) -> Vec<u8> {
1476        let ramp: &[(f32, (u8, u8, u8))] = &[
1477            (0.00, (0,   0,   80)),
1478            (0.10, (0,   50,  170)),
1479            (0.15, (60,  120, 200)),
1480            (0.20, (240, 220, 130)),
1481            (0.35, (80,  160, 40)),
1482            (0.55, (60,  130, 30)),
1483            (0.70, (120, 100, 80)),
1484            (0.85, (140, 140, 140)),
1485            (1.00, (250, 255, 255)),
1486        ];
1487        let mut pixels = Vec::with_capacity(self.data.len() * 3);
1488        for &v in &self.data {
1489            let (r, g, b) = sample_color_ramp(ramp, v.clamp(0.0, 1.0));
1490            pixels.push(r);
1491            pixels.push(g);
1492            pixels.push(b);
1493        }
1494        pixels
1495    }
1496
1497    /// Compute an ambient occlusion approximation using height-based sampling.
1498    /// Returns a map where 0 = fully occluded, 1 = fully lit.
1499    pub fn ambient_occlusion(&self, radius: usize, samples: usize) -> HeightMap {
1500        let mut ao = HeightMap::new(self.width, self.height);
1501        let r = radius as f32;
1502        let angle_step = std::f32::consts::TAU / samples as f32;
1503        for y in 0..self.height {
1504            for x in 0..self.width {
1505                let h0 = self.get(x, y);
1506                let mut occluded = 0.0f32;
1507                for s in 0..samples {
1508                    let angle = s as f32 * angle_step;
1509                    let dx = angle.cos();
1510                    let dy = angle.sin();
1511                    let mut max_elev_angle = 0.0f32;
1512                    for step in 1..=radius {
1513                        let sx = x as f32 + dx * step as f32;
1514                        let sy = y as f32 + dy * step as f32;
1515                        if sx < 0.0 || sx >= self.width as f32 || sy < 0.0 || sy >= self.height as f32 { break; }
1516                        let sh = self.sample_bilinear(sx, sy);
1517                        let dist = step as f32;
1518                        let elev = (sh - h0) / dist;
1519                        if elev > max_elev_angle { max_elev_angle = elev; }
1520                    }
1521                    let horizon_angle = (max_elev_angle * r).atan() / std::f32::consts::FRAC_PI_2;
1522                    occluded += horizon_angle.clamp(0.0, 1.0);
1523                }
1524                ao.set(x, y, 1.0 - (occluded / samples as f32).clamp(0.0, 1.0));
1525            }
1526        }
1527        ao
1528    }
1529
1530    /// Detect ridgelines: cells that are local maxima in at least one axis direction.
1531    pub fn ridge_mask(&self, threshold: f32) -> HeightMap {
1532        let mut out = HeightMap::new(self.width, self.height);
1533        for y in 1..(self.height - 1) {
1534            for x in 1..(self.width - 1) {
1535                let c = self.get(x, y);
1536                let ridge_x = self.get(x-1, y) < c && self.get(x+1, y) < c;
1537                let ridge_z = self.get(x, y-1) < c && self.get(x, y+1) < c;
1538                if (ridge_x || ridge_z) && c > threshold {
1539                    out.set(x, y, c);
1540                }
1541            }
1542        }
1543        out
1544    }
1545
1546    /// Detect valley lines: cells that are local minima in at least one axis.
1547    pub fn valley_mask(&self, threshold: f32) -> HeightMap {
1548        let mut out = HeightMap::new(self.width, self.height);
1549        for y in 1..(self.height - 1) {
1550            for x in 1..(self.width - 1) {
1551                let c = self.get(x, y);
1552                let valley_x = self.get(x-1, y) > c && self.get(x+1, y) > c;
1553                let valley_z = self.get(x, y-1) > c && self.get(x, y+1) > c;
1554                if (valley_x || valley_z) && c < threshold {
1555                    out.set(x, y, 1.0 - c);
1556                }
1557            }
1558        }
1559        out
1560    }
1561
1562    /// Compute a distance field: each cell stores distance to the nearest cell
1563    /// with value >= `threshold`, normalized to [0,1].
1564    pub fn distance_field(&self, threshold: f32) -> HeightMap {
1565        let w = self.width;
1566        let h = self.height;
1567        let mut dist = vec![f32::INFINITY; w * h];
1568        // BFS from all seed cells (value >= threshold)
1569        let mut queue: VecDeque<(usize, usize)> = VecDeque::new();
1570        for y in 0..h {
1571            for x in 0..w {
1572                if self.get(x, y) >= threshold {
1573                    dist[y * w + x] = 0.0;
1574                    queue.push_back((x, y));
1575                }
1576            }
1577        }
1578        let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1579        while let Some((cx, cy)) = queue.pop_front() {
1580            let d = dist[cy * w + cx];
1581            for (dx, dy) in &dirs {
1582                let nx = cx as i32 + dx;
1583                let ny = cy as i32 + dy;
1584                if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1585                    let ni = ny as usize * w + nx as usize;
1586                    if dist[ni] > d + 1.0 {
1587                        dist[ni] = d + 1.0;
1588                        queue.push_back((nx as usize, ny as usize));
1589                    }
1590                }
1591            }
1592        }
1593        let max_d = dist.iter().cloned().fold(0.0f32, f32::max);
1594        let data = if max_d > 0.0 {
1595            dist.iter().map(|&d| if d.is_infinite() { 1.0 } else { d / max_d }).collect()
1596        } else {
1597            vec![0.0; w * h]
1598        };
1599        HeightMap { width: w, height: h, data }
1600    }
1601
1602    /// Overlay a circle (Gaussian bump) at world-space (x, y) with given radius and strength.
1603    pub fn paint_circle(&mut self, cx: f32, cy: f32, radius: f32, strength: f32, add: bool) {
1604        let r2 = radius * radius;
1605        let x0 = ((cx - radius).floor() as i32).max(0) as usize;
1606        let y0 = ((cy - radius).floor() as i32).max(0) as usize;
1607        let x1 = ((cx + radius).ceil()  as i32).min(self.width  as i32 - 1) as usize;
1608        let y1 = ((cy + radius).ceil()  as i32).min(self.height as i32 - 1) as usize;
1609        for y in y0..=y1 {
1610            for x in x0..=x1 {
1611                let dx = x as f32 - cx;
1612                let dy = y as f32 - cy;
1613                let d2 = dx * dx + dy * dy;
1614                if d2 < r2 {
1615                    let t = 1.0 - (d2 / r2).sqrt();
1616                    let falloff = t * t * (3.0 - 2.0 * t); // smooth step
1617                    let delta = strength * falloff;
1618                    let cur = self.get(x, y);
1619                    self.set(x, y, if add { (cur + delta).clamp(0.0, 1.0) } else { (cur - delta).clamp(0.0, 1.0) });
1620                }
1621            }
1622        }
1623    }
1624
1625    /// Flatten a circular area to a target height.
1626    pub fn flatten_circle(&mut self, cx: f32, cy: f32, radius: f32, target: f32, strength: f32) {
1627        let r2 = radius * radius;
1628        let x0 = ((cx - radius).floor() as i32).max(0) as usize;
1629        let y0 = ((cy - radius).floor() as i32).max(0) as usize;
1630        let x1 = ((cx + radius).ceil()  as i32).min(self.width  as i32 - 1) as usize;
1631        let y1 = ((cy + radius).ceil()  as i32).min(self.height as i32 - 1) as usize;
1632        for y in y0..=y1 {
1633            for x in x0..=x1 {
1634                let dx = x as f32 - cx;
1635                let dy = y as f32 - cy;
1636                if dx * dx + dy * dy < r2 {
1637                    let cur = self.get(x, y);
1638                    self.set(x, y, (cur + (target - cur) * strength).clamp(0.0, 1.0));
1639                }
1640            }
1641        }
1642    }
1643
1644    /// Resample the heightmap to a new resolution using bilinear interpolation.
1645    pub fn resample(&self, new_width: usize, new_height: usize) -> HeightMap {
1646        let mut out = HeightMap::new(new_width, new_height);
1647        let sx = (self.width  - 1) as f32 / (new_width  - 1).max(1) as f32;
1648        let sy = (self.height - 1) as f32 / (new_height - 1).max(1) as f32;
1649        for y in 0..new_height {
1650            for x in 0..new_width {
1651                out.set(x, y, self.sample_bilinear(x as f32 * sx, y as f32 * sy));
1652            }
1653        }
1654        out
1655    }
1656
1657    /// Crop a rectangular region from the heightmap.
1658    pub fn crop(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> HeightMap {
1659        let nw = x1.saturating_sub(x0).min(self.width  - x0);
1660        let nh = y1.saturating_sub(y0).min(self.height - y0);
1661        let mut out = HeightMap::new(nw, nh);
1662        for y in 0..nh {
1663            for x in 0..nw {
1664                out.set(x, y, self.get(x0 + x, y0 + y));
1665            }
1666        }
1667        out
1668    }
1669
1670    /// Tile two heightmaps side by side (horizontal).
1671    pub fn tile_horizontal(&self, other: &HeightMap) -> HeightMap {
1672        assert_eq!(self.height, other.height, "heights must match for horizontal tiling");
1673        let nw = self.width + other.width;
1674        let mut out = HeightMap::new(nw, self.height);
1675        for y in 0..self.height {
1676            for x in 0..self.width {
1677                out.set(x, y, self.get(x, y));
1678            }
1679            for x in 0..other.width {
1680                out.set(self.width + x, y, other.get(x, y));
1681            }
1682        }
1683        out
1684    }
1685}
1686
1687fn sample_color_ramp(ramp: &[(f32, (u8, u8, u8))], t: f32) -> (u8, u8, u8) {
1688    if ramp.is_empty() { return (128, 128, 128); }
1689    if t <= ramp[0].0 { return ramp[0].1; }
1690    if t >= ramp[ramp.len()-1].0 { return ramp[ramp.len()-1].1; }
1691    for i in 0..ramp.len()-1 {
1692        let (t0, c0) = ramp[i];
1693        let (t1, c1) = ramp[i+1];
1694        if t >= t0 && t <= t1 {
1695            let f = (t - t0) / (t1 - t0);
1696            let lerp_c = |a: u8, b: u8| -> u8 { (a as f32 + (b as f32 - a as f32) * f) as u8 };
1697            return (lerp_c(c0.0, c1.0), lerp_c(c0.1, c1.1), lerp_c(c0.2, c1.2));
1698        }
1699    }
1700    ramp[ramp.len()-1].1
1701}
1702
1703use std::collections::VecDeque;
1704
1705// ── Worley / Cellular Noise Terrain ───────────────────────────────────────────
1706
1707/// Generates terrain using Worley (cellular / Voronoi) noise.
1708/// Produces cracked-earth, rocky, or cell-structured terrain.
1709pub struct WorleyTerrain;
1710
1711impl WorleyTerrain {
1712    /// Generate terrain using F1 Worley noise (distance to nearest feature point).
1713    /// `num_points` controls density of feature points.
1714    pub fn generate(width: usize, height: usize, num_points: usize, seed: u64) -> HeightMap {
1715        let mut rng = Rng::new(seed);
1716        let points: Vec<(f32, f32)> = (0..num_points)
1717            .map(|_| (rng.next_f32() * width as f32, rng.next_f32() * height as f32))
1718            .collect();
1719        let mut map = HeightMap::new(width, height);
1720        let max_dist = (width * width + height * height) as f32;
1721        for y in 0..height {
1722            for x in 0..width {
1723                let px = x as f32;
1724                let py = y as f32;
1725                let mut d1 = f32::INFINITY;
1726                let mut d2 = f32::INFINITY;
1727                for &(qx, qy) in &points {
1728                    let dx = px - qx;
1729                    let dy = py - qy;
1730                    let d = dx * dx + dy * dy;
1731                    if d < d1 { d2 = d1; d1 = d; }
1732                    else if d < d2 { d2 = d; }
1733                }
1734                // F2 - F1 creates cell boundaries
1735                let val = ((d2.sqrt() - d1.sqrt()) / width.min(height) as f32).clamp(0.0, 1.0);
1736                map.set(x, y, val);
1737            }
1738        }
1739        map.normalize();
1740        map
1741    }
1742
1743    /// Invert Worley noise to get bumpy hills instead of cracked surfaces.
1744    pub fn generate_inverted(width: usize, height: usize, num_points: usize, seed: u64) -> HeightMap {
1745        let mut m = Self::generate(width, height, num_points, seed);
1746        m.invert();
1747        m
1748    }
1749}
1750
1751// ── Gradient Domain Operations ────────────────────────────────────────────────
1752
1753/// Operations that work in gradient/frequency domain.
1754pub struct HeightMapFilter;
1755
1756impl HeightMapFilter {
1757    /// Apply a high-pass filter (removes low frequencies).
1758    pub fn high_pass(map: &HeightMap, radius: usize) -> HeightMap {
1759        let mut low = map.clone();
1760        low.blur(radius);
1761        let mut out = map.clone();
1762        for i in 0..map.data.len() {
1763            out.data[i] = (map.data[i] - low.data[i] + 0.5).clamp(0.0, 1.0);
1764        }
1765        out
1766    }
1767
1768    /// Apply a low-pass filter (removes high frequencies / smoothing).
1769    pub fn low_pass(map: &HeightMap, radius: usize) -> HeightMap {
1770        let mut out = map.clone();
1771        out.blur(radius);
1772        out
1773    }
1774
1775    /// Band-pass filter: keeps frequencies between low_radius and high_radius.
1776    pub fn band_pass(map: &HeightMap, low_radius: usize, high_radius: usize) -> HeightMap {
1777        let lp = Self::low_pass(map, low_radius);
1778        let hp = Self::high_pass(map, high_radius);
1779        let mut out = HeightMap::new(map.width, map.height);
1780        for i in 0..map.data.len() {
1781            out.data[i] = ((lp.data[i] + hp.data[i]) * 0.5).clamp(0.0, 1.0);
1782        }
1783        out
1784    }
1785
1786    /// Emboss filter: emphasizes edges to create a metallic/3D impression.
1787    pub fn emboss(map: &HeightMap) -> HeightMap {
1788        let w = map.width;
1789        let h = map.height;
1790        let mut out = HeightMap::new(w, h);
1791        for y in 1..(h-1) {
1792            for x in 1..(w-1) {
1793                let tl = map.get(x-1, y-1);
1794                let br = map.get(x+1, y+1);
1795                let emboss = (br - tl + 1.0) * 0.5;
1796                out.set(x, y, emboss.clamp(0.0, 1.0));
1797            }
1798        }
1799        out
1800    }
1801
1802    /// Erosion morphological operator: each cell takes the minimum of its neighborhood.
1803    pub fn morphological_erosion(map: &HeightMap, radius: usize) -> HeightMap {
1804        let mut out = HeightMap::new(map.width, map.height);
1805        for y in 0..map.height {
1806            for x in 0..map.width {
1807                let mut mn = 1.0f32;
1808                for dy in -(radius as i32)..=(radius as i32) {
1809                    for dx in -(radius as i32)..=(radius as i32) {
1810                        let nx = (x as i32 + dx).clamp(0, map.width  as i32 - 1) as usize;
1811                        let ny = (y as i32 + dy).clamp(0, map.height as i32 - 1) as usize;
1812                        mn = mn.min(map.get(nx, ny));
1813                    }
1814                }
1815                out.set(x, y, mn);
1816            }
1817        }
1818        out
1819    }
1820
1821    /// Dilation morphological operator: each cell takes the maximum of its neighborhood.
1822    pub fn morphological_dilation(map: &HeightMap, radius: usize) -> HeightMap {
1823        let mut out = HeightMap::new(map.width, map.height);
1824        for y in 0..map.height {
1825            for x in 0..map.width {
1826                let mut mx = 0.0f32;
1827                for dy in -(radius as i32)..=(radius as i32) {
1828                    for dx in -(radius as i32)..=(radius as i32) {
1829                        let nx = (x as i32 + dx).clamp(0, map.width  as i32 - 1) as usize;
1830                        let ny = (y as i32 + dy).clamp(0, map.height as i32 - 1) as usize;
1831                        mx = mx.max(map.get(nx, ny));
1832                    }
1833                }
1834                out.set(x, y, mx);
1835            }
1836        }
1837        out
1838    }
1839
1840    /// Opening: erosion followed by dilation (removes small peaks).
1841    pub fn morphological_open(map: &HeightMap, radius: usize) -> HeightMap {
1842        let eroded = Self::morphological_erosion(map, radius);
1843        Self::morphological_dilation(&eroded, radius)
1844    }
1845
1846    /// Closing: dilation followed by erosion (fills small valleys).
1847    pub fn morphological_close(map: &HeightMap, radius: usize) -> HeightMap {
1848        let dilated = Self::morphological_dilation(map, radius);
1849        Self::morphological_erosion(&dilated, radius)
1850    }
1851}
1852
1853// ── Tectonic Simulation (Extended) ───────────────────────────────────────────
1854
1855/// Extended tectonic simulation with plate movement and collision.
1856pub struct TectonicSimulation {
1857    pub width:       usize,
1858    pub height:      usize,
1859    pub num_plates:  usize,
1860    pub seed:        u64,
1861    /// Accumulated heightmap from simulation steps.
1862    pub heightmap:   HeightMap,
1863    /// Per-cell plate assignment.
1864    plate_ids:       Vec<usize>,
1865    /// Plate base elevations.
1866    plate_elevations: Vec<f32>,
1867    /// Plate velocity vectors.
1868    plate_velocities: Vec<(f32, f32)>,
1869    /// Accumulated stress per cell.
1870    stress:          Vec<f32>,
1871}
1872
1873impl TectonicSimulation {
1874    /// Initialize a new tectonic simulation.
1875    pub fn new(width: usize, height: usize, num_plates: usize, seed: u64) -> Self {
1876        let mut rng = Rng::new(seed);
1877        let centers: Vec<(f32, f32)> = (0..num_plates)
1878            .map(|_| (rng.next_f32() * width as f32, rng.next_f32() * height as f32))
1879            .collect();
1880        let plate_elevations: Vec<f32> = (0..num_plates)
1881            .map(|_| if rng.next_f32() < 0.45 { rng.next_f32_range(0.0, 0.3) }
1882                     else { rng.next_f32_range(0.4, 0.65) })
1883            .collect();
1884        let plate_velocities: Vec<(f32, f32)> = (0..num_plates)
1885            .map(|_| {
1886                let a = rng.next_f32() * std::f32::consts::TAU;
1887                (a.cos() * 0.3, a.sin() * 0.3)
1888            })
1889            .collect();
1890        // Assign plate IDs via nearest-center Voronoi
1891        let mut plate_ids = vec![0usize; width * height];
1892        for y in 0..height {
1893            for x in 0..width {
1894                let mut best = 0;
1895                let mut best_d = f32::INFINITY;
1896                for (i, &(cx, cy)) in centers.iter().enumerate() {
1897                    let d = (x as f32 - cx).powi(2) + (y as f32 - cy).powi(2);
1898                    if d < best_d { best_d = d; best = i; }
1899                }
1900                plate_ids[y * width + x] = best;
1901            }
1902        }
1903        let stress = vec![0.0f32; width * height];
1904        let heightmap = HeightMap::new(width, height);
1905        Self { width, height, num_plates, seed, heightmap, plate_ids, plate_elevations, plate_velocities, stress }
1906    }
1907
1908    /// Run one simulation step: move plates, compute stress, update heights.
1909    pub fn step(&mut self) {
1910        let w = self.width;
1911        let h = self.height;
1912        // Update heights based on plate elevations and stress
1913        for y in 0..h {
1914            for x in 0..w {
1915                let pid = self.plate_ids[y * w + x];
1916                let base = self.plate_elevations[pid];
1917                let stress = self.stress[y * w + x];
1918                self.heightmap.data[y * w + x] = (base + stress * 0.3).clamp(0.0, 1.0);
1919            }
1920        }
1921        // Compute boundary stress from plate velocity differences
1922        let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1923        for y in 0..h {
1924            for x in 0..w {
1925                let pid = self.plate_ids[y * w + x];
1926                let pv = self.plate_velocities[pid];
1927                let mut new_stress = self.stress[y * w + x] * 0.95;
1928                for (dx, dy) in &dirs {
1929                    let nx = x as i32 + dx;
1930                    let ny = y as i32 + dy;
1931                    if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1932                        let npid = self.plate_ids[ny as usize * w + nx as usize];
1933                        if npid != pid {
1934                            let nv = self.plate_velocities[npid];
1935                            // Convergence = dot product of relative velocity and boundary normal
1936                            let rel_vx = pv.0 - nv.0;
1937                            let rel_vy = pv.1 - nv.1;
1938                            let boundary_nx = *dx as f32;
1939                            let boundary_ny = *dy as f32;
1940                            let convergence = rel_vx * boundary_nx + rel_vy * boundary_ny;
1941                            if convergence < 0.0 {
1942                                // Compressing boundary → stress builds up
1943                                new_stress += (-convergence) * 0.05;
1944                            }
1945                        }
1946                    }
1947                }
1948                self.stress[y * w + x] = new_stress.clamp(0.0, 1.0);
1949            }
1950        }
1951    }
1952
1953    /// Run `n` simulation steps and return the resulting heightmap.
1954    pub fn simulate(&mut self, steps: usize) -> &HeightMap {
1955        for _ in 0..steps {
1956            self.step();
1957        }
1958        self.heightmap.normalize();
1959        &self.heightmap
1960    }
1961}
1962
1963// ── Extended Erosion: Full Hydraulic Detail ────────────────────────────────────
1964
1965/// Advanced hydraulic erosion with explicit water table tracking.
1966pub struct AdvancedHydraulicErosion;
1967
1968impl AdvancedHydraulicErosion {
1969    /// Full simulation with water table, springs, and river formation.
1970    pub fn erode_full(
1971        map:              &mut HeightMap,
1972        iterations:       usize,
1973        rain_per_cell:    f32,
1974        evaporation_rate: f32,
1975        erosion_rate:     f32,
1976        deposition_rate:  f32,
1977        seed:             u64,
1978    ) {
1979        let w = map.width;
1980        let h = map.height;
1981        let mut water  = vec![0.0f32; w * h];
1982        let mut sediment = vec![0.0f32; w * h];
1983        let mut rng = Rng::new(seed);
1984        let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1985
1986        for _iter in 0..iterations {
1987            // Rain: add water to all cells
1988            for cell in water.iter_mut() {
1989                *cell += rain_per_cell * rng.next_f32_range(0.5, 1.5);
1990            }
1991
1992            // Flow: move water + sediment downhill
1993            let terrain_plus_water: Vec<f32> = (0..w*h)
1994                .map(|i| map.data[i] + water[i])
1995                .collect();
1996
1997            let mut d_water   = vec![0.0f32; w * h];
1998            let mut d_sediment = vec![0.0f32; w * h];
1999
2000            for y in 0..h {
2001                for x in 0..w {
2002                    let idx = y * w + x;
2003                    let h_cur = terrain_plus_water[idx];
2004                    let w_cur = water[idx];
2005                    if w_cur < 0.0001 { continue; }
2006
2007                    let mut total_flow = 0.0f32;
2008                    let mut flows = [(0.0f32, 0i32, 0i32); 4];
2009                    for (k, (dx, dy)) in dirs.iter().enumerate() {
2010                        let nx = x as i32 + dx;
2011                        let ny = y as i32 + dy;
2012                        if nx < 0 || nx >= w as i32 || ny < 0 || ny >= h as i32 { continue; }
2013                        let nidx = ny as usize * w + nx as usize;
2014                        let h_n = terrain_plus_water[nidx];
2015                        let diff = h_cur - h_n;
2016                        if diff > 0.0 {
2017                            flows[k] = (diff, nx, ny);
2018                            total_flow += diff;
2019                        }
2020                    }
2021                    if total_flow < 0.0001 { continue; }
2022
2023                    for (diff, nx, ny) in flows.iter() {
2024                        if *diff <= 0.0 { continue; }
2025                        let frac = diff / total_flow;
2026                        let flow_w = (w_cur * frac * 0.5).min(w_cur);
2027                        let flow_s = sediment[idx] * frac * 0.5;
2028                        let nidx = *ny as usize * w + *nx as usize;
2029                        d_water[idx] -= flow_w;
2030                        d_water[nidx] += flow_w;
2031                        d_sediment[idx] -= flow_s;
2032                        d_sediment[nidx] += flow_s;
2033
2034                        // Erosion proportional to flow
2035                        let erode = erosion_rate * flow_w * diff;
2036                        map.data[idx] = (map.data[idx] - erode).clamp(0.0, 1.0);
2037                        d_sediment[idx] += erode;
2038                    }
2039                }
2040            }
2041
2042            // Apply deltas
2043            for i in 0..(w*h) {
2044                water[i]    = (water[i] + d_water[i]).max(0.0);
2045                sediment[i] = (sediment[i] + d_sediment[i]).max(0.0);
2046            }
2047
2048            // Evaporation + sediment deposition
2049            for i in 0..(w*h) {
2050                water[i] *= 1.0 - evaporation_rate;
2051                let deposit = sediment[i] * deposition_rate;
2052                sediment[i] -= deposit;
2053                map.data[i] = (map.data[i] + deposit).clamp(0.0, 1.0);
2054            }
2055        }
2056    }
2057}
2058
2059// ── HeightMap Compositor ──────────────────────────────────────────────────────
2060
2061/// Composites multiple heightmaps using layered blending operations.
2062pub struct HeightMapCompositor {
2063    layers: Vec<CompositeLayer>,
2064}
2065
2066/// A single layer in a compositor.
2067pub struct CompositeLayer {
2068    pub map:     HeightMap,
2069    pub blend:   CompositeBlendMode,
2070    pub weight:  f32,
2071    pub mask:    Option<HeightMap>,
2072}
2073
2074/// How a layer blends with layers below it.
2075#[derive(Clone, Copy, Debug)]
2076pub enum CompositeBlendMode {
2077    Normal,
2078    Add,
2079    Subtract,
2080    Multiply,
2081    Screen,
2082    Overlay,
2083    Max,
2084    Min,
2085}
2086
2087impl HeightMapCompositor {
2088    pub fn new() -> Self { Self { layers: Vec::new() } }
2089
2090    pub fn add_layer(&mut self, map: HeightMap, blend: CompositeBlendMode, weight: f32) {
2091        self.layers.push(CompositeLayer { map, blend, weight, mask: None });
2092    }
2093
2094    pub fn add_layer_with_mask(&mut self, map: HeightMap, mask: HeightMap, blend: CompositeBlendMode, weight: f32) {
2095        self.layers.push(CompositeLayer { map, blend, weight, mask: Some(mask) });
2096    }
2097
2098    /// Flatten all layers into a single heightmap.
2099    pub fn flatten(&self) -> Option<HeightMap> {
2100        if self.layers.is_empty() { return None; }
2101        let w = self.layers[0].map.width;
2102        let h = self.layers[0].map.height;
2103        let mut result = HeightMap::new(w, h);
2104
2105        for layer in &self.layers {
2106            let lm = &layer.map;
2107            for y in 0..h {
2108                for x in 0..w {
2109                    let src = lm.get(x.min(lm.width-1), y.min(lm.height-1));
2110                    let dst = result.get(x, y);
2111                    let mask_w = layer.mask.as_ref()
2112                        .map(|m| m.get(x.min(m.width-1), y.min(m.height-1)))
2113                        .unwrap_or(1.0);
2114                    let blended = Self::blend(dst, src, layer.blend);
2115                    let final_v = dst + (blended - dst) * layer.weight * mask_w;
2116                    result.set(x, y, final_v.clamp(0.0, 1.0));
2117                }
2118            }
2119        }
2120        Some(result)
2121    }
2122
2123    fn blend(dst: f32, src: f32, mode: CompositeBlendMode) -> f32 {
2124        match mode {
2125            CompositeBlendMode::Normal    => src,
2126            CompositeBlendMode::Add       => (dst + src).clamp(0.0, 1.0),
2127            CompositeBlendMode::Subtract  => (dst - src).clamp(0.0, 1.0),
2128            CompositeBlendMode::Multiply  => dst * src,
2129            CompositeBlendMode::Screen    => 1.0 - (1.0 - dst) * (1.0 - src),
2130            CompositeBlendMode::Overlay   => {
2131                if dst < 0.5 { 2.0 * dst * src }
2132                else { 1.0 - 2.0 * (1.0 - dst) * (1.0 - src) }
2133            }
2134            CompositeBlendMode::Max       => dst.max(src),
2135            CompositeBlendMode::Min       => dst.min(src),
2136        }
2137    }
2138}
2139
2140// ── HeightMap Warp ────────────────────────────────────────────────────────────
2141
2142/// Warp (distort) a heightmap using a displacement field.
2143pub struct DisplacementWarp {
2144    /// Horizontal displacement field.
2145    pub warp_x: HeightMap,
2146    /// Vertical displacement field.
2147    pub warp_y: HeightMap,
2148    /// Maximum displacement in pixels.
2149    pub strength: f32,
2150}
2151
2152impl DisplacementWarp {
2153    /// Create a warp using noise-derived displacement.
2154    pub fn from_noise(width: usize, height: usize, strength: f32, seed: u64) -> Self {
2155        let warp_x = FractalNoise::generate(width, height, 4, 2.0, 0.5, 3.0, seed);
2156        let warp_y = FractalNoise::generate(width, height, 4, 2.0, 0.5, 3.0, seed.wrapping_add(137));
2157        Self { warp_x, warp_y, strength }
2158    }
2159
2160    /// Apply warp to a heightmap.
2161    pub fn apply(&self, map: &HeightMap) -> HeightMap {
2162        let w = map.width;
2163        let h = map.height;
2164        let mut out = HeightMap::new(w, h);
2165        for y in 0..h {
2166            for x in 0..w {
2167                let wx = (self.warp_x.get(x, y) * 2.0 - 1.0) * self.strength;
2168                let wy = (self.warp_y.get(x, y) * 2.0 - 1.0) * self.strength;
2169                let sx = (x as f32 + wx).clamp(0.0, w as f32 - 1.0);
2170                let sy = (y as f32 + wy).clamp(0.0, h as f32 - 1.0);
2171                out.set(x, y, map.sample_bilinear(sx, sy));
2172            }
2173        }
2174        out
2175    }
2176}
2177
2178// ── Extended Tests ─────────────────────────────────────────────────────────────
2179
2180#[cfg(test)]
2181mod extended_tests {
2182    use super::*;
2183
2184    #[test]
2185    fn test_heightmap_mean_variance() {
2186        let mut m = HeightMap::new(4, 4);
2187        for v in m.data.iter_mut() { *v = 0.5; }
2188        assert!((m.mean() - 0.5).abs() < 1e-5);
2189        assert!(m.variance().abs() < 1e-5);
2190    }
2191
2192    #[test]
2193    fn test_heightmap_invert() {
2194        let mut m = HeightMap::new(4, 4);
2195        for v in m.data.iter_mut() { *v = 0.3; }
2196        m.invert();
2197        assert!((m.data[0] - 0.7).abs() < 1e-5);
2198    }
2199
2200    #[test]
2201    fn test_heightmap_add_multiply() {
2202        let mut a = HeightMap::new(4, 4);
2203        let mut b = HeightMap::new(4, 4);
2204        for v in a.data.iter_mut() { *v = 0.4; }
2205        for v in b.data.iter_mut() { *v = 0.3; }
2206        a.add(&b, 1.0);
2207        assert!((a.data[0] - 0.7).abs() < 0.01);
2208        let mut c = HeightMap::new(4, 4);
2209        let mut d = HeightMap::new(4, 4);
2210        for v in c.data.iter_mut() { *v = 0.5; }
2211        for v in d.data.iter_mut() { *v = 0.4; }
2212        c.multiply(&d);
2213        assert!((c.data[0] - 0.2).abs() < 0.01);
2214    }
2215
2216    #[test]
2217    fn test_heightmap_equalize() {
2218        let mut m = HeightMap::new(8, 8);
2219        for (i, v) in m.data.iter_mut().enumerate() { *v = (i % 4) as f32 * 0.1; }
2220        m.equalize();
2221        let mn = m.min_value();
2222        let mx = m.max_value();
2223        assert!((mn - 0.0).abs() < 1e-4);
2224        assert!((mx - 1.0).abs() < 1e-4);
2225    }
2226
2227    #[test]
2228    fn test_heightmap_histogram() {
2229        let mut m = HeightMap::new(16, 16);
2230        m.normalize(); // all zeros after new
2231        for v in m.data.iter_mut() { *v = 0.0; }
2232        let hist = m.histogram(10);
2233        assert_eq!(hist[0], 256); // all in first bin
2234        assert_eq!(hist.iter().sum::<usize>(), 256);
2235    }
2236
2237    #[test]
2238    fn test_worley_terrain() {
2239        let m = WorleyTerrain::generate(32, 32, 20, 42);
2240        assert_eq!(m.data.len(), 32 * 32);
2241        let mn = m.min_value();
2242        let mx = m.max_value();
2243        assert!((mn - 0.0).abs() < 1e-4);
2244        assert!((mx - 1.0).abs() < 1e-4);
2245    }
2246
2247    #[test]
2248    fn test_heightmap_filter_high_pass() {
2249        let m = DiamondSquare::generate(32, 0.7, 42);
2250        let hp = HeightMapFilter::high_pass(&m, 3);
2251        assert_eq!(hp.data.len(), m.data.len());
2252    }
2253
2254    #[test]
2255    fn test_heightmap_filter_morphological() {
2256        let m = DiamondSquare::generate(16, 0.7, 42);
2257        let eroded = HeightMapFilter::morphological_erosion(&m, 1);
2258        // Erosion should reduce max value
2259        assert!(eroded.max_value() <= m.max_value() + 1e-5);
2260    }
2261
2262    #[test]
2263    fn test_tectonic_simulation() {
2264        let mut sim = TectonicSimulation::new(32, 32, 6, 42);
2265        sim.simulate(5);
2266        assert_eq!(sim.heightmap.data.len(), 32 * 32);
2267        let mn = sim.heightmap.min_value();
2268        let mx = sim.heightmap.max_value();
2269        assert!(mn >= 0.0 && mx <= 1.0);
2270    }
2271
2272    #[test]
2273    fn test_heightmap_resample() {
2274        let m = DiamondSquare::generate(32, 0.7, 42);
2275        let r = m.resample(16, 16);
2276        assert_eq!(r.width, 16);
2277        assert_eq!(r.height, 16);
2278    }
2279
2280    #[test]
2281    fn test_heightmap_crop() {
2282        let m = DiamondSquare::generate(32, 0.7, 42);
2283        let c = m.crop(8, 8, 24, 24);
2284        assert_eq!(c.width, 16);
2285        assert_eq!(c.height, 16);
2286    }
2287
2288    #[test]
2289    fn test_heightmap_distance_field() {
2290        let mut m = HeightMap::new(16, 16);
2291        m.set(8, 8, 1.0); // single seed
2292        let df = m.distance_field(0.5);
2293        assert_eq!(df.get(8, 8), 0.0);
2294        assert!(df.get(0, 0) > 0.0);
2295    }
2296
2297    #[test]
2298    fn test_heightmap_paint_circle() {
2299        let mut m = HeightMap::new(64, 64);
2300        m.paint_circle(32.0, 32.0, 10.0, 0.5, true);
2301        assert!(m.get(32, 32) > 0.0);
2302        assert_eq!(m.get(0, 0), 0.0);
2303    }
2304
2305    #[test]
2306    fn test_compositor_flatten() {
2307        let a = FractalNoise::generate(16, 16, 4, 2.0, 0.5, 3.0, 42);
2308        let b = FractalNoise::generate(16, 16, 4, 2.0, 0.5, 3.0, 99);
2309        let mut comp = HeightMapCompositor::new();
2310        comp.add_layer(a, CompositeBlendMode::Normal, 0.5);
2311        comp.add_layer(b, CompositeBlendMode::Add, 0.3);
2312        let result = comp.flatten().expect("compositor should produce output");
2313        assert_eq!(result.data.len(), 16 * 16);
2314    }
2315
2316    #[test]
2317    fn test_displacement_warp() {
2318        let m = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
2319        let warp = DisplacementWarp::from_noise(32, 32, 5.0, 77);
2320        let warped = warp.apply(&m);
2321        assert_eq!(warped.data.len(), m.data.len());
2322    }
2323
2324    #[test]
2325    fn test_advanced_hydraulic_erosion() {
2326        let mut m = DiamondSquare::generate(32, 0.8, 1);
2327        AdvancedHydraulicErosion::erode_full(&mut m, 50, 0.01, 0.02, 0.01, 0.05, 42);
2328        assert!(m.min_value() >= 0.0);
2329        assert!(m.max_value() <= 1.0);
2330    }
2331
2332    #[test]
2333    fn test_color_bytes() {
2334        let m = FractalNoise::generate(16, 16, 4, 2.0, 0.5, 3.0, 42);
2335        let bytes = m.to_color_bytes();
2336        assert_eq!(bytes.len(), 16 * 16 * 3);
2337    }
2338
2339    #[test]
2340    fn test_ambient_occlusion() {
2341        let m = DiamondSquare::generate(16, 0.7, 42);
2342        let ao = m.ambient_occlusion(4, 8);
2343        assert_eq!(ao.data.len(), m.data.len());
2344        assert!(ao.min_value() >= 0.0 && ao.max_value() <= 1.0);
2345    }
2346}