Skip to main content

proof_engine/editor/
world_editor.rs

1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6// ============================================================
7//  FUNDAMENTAL CONSTANTS
8// ============================================================
9
10pub const PI: f32      = std::f32::consts::PI;
11pub const TWO_PI: f32  = 2.0 * PI;
12pub const HALF_PI: f32 = PI * 0.5;
13pub const DEG2RAD: f32 = PI / 180.0;
14pub const RAD2DEG: f32 = 180.0 / PI;
15pub const SQRT3: f32   = 1.732_050_8;
16pub const F3: f32      = 1.0 / 3.0;
17pub const G3: f32      = 1.0 / 6.0;
18
19// Atmospheric physics constants
20pub const RAYLEIGH_SCALE_HEIGHT: f64 = 8.5;    // km
21pub const MIE_SCALE_HEIGHT: f64      = 1.2;    // km
22pub const RAYLEIGH_R: f64            = 5.8e-6;
23pub const RAYLEIGH_G: f64            = 13.5e-6;
24pub const RAYLEIGH_B: f64            = 33.1e-6;
25pub const MIE_COEFF: f64             = 21.0e-6;
26pub const MIE_G: f64                 = 0.758;  // asymmetry factor
27pub const EARTH_RADIUS: f64          = 6371.0; // km
28pub const ATMO_RADIUS: f64           = 6471.0; // km (100 km atmosphere)
29
30// Solar
31pub const SOLAR_OBLIQUITY: f64 = 23.45; // degrees
32
33// Hydraulic erosion defaults
34pub const EROSION_INERTIA: f32            = 0.05;
35pub const EROSION_CAPACITY: f32           = 4.0;
36pub const EROSION_DEPOSITION: f32         = 0.3;
37pub const EROSION_EROSION_SPEED: f32      = 0.3;
38pub const EROSION_EVAPORATION: f32        = 0.02;
39pub const EROSION_MIN_SLOPE: f32          = 0.01;
40pub const EROSION_GRAVITY: f32            = 4.0;
41pub const EROSION_MAX_STEPS: usize        = 64;
42
43// ============================================================
44//  PERMUTATION TABLE (512 elements for Perlin/Simplex noise)
45// ============================================================
46
47pub const PERM: [u8; 512] = [
48    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
49    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
50    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
51     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
52     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
53     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
54     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
55    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
56     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
57    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
58    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
59    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
60    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
61     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
62    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
63    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
64    151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
65    140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
66    247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
67     57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
68     74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
69     60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
70     65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
71    200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
72     52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
73    207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
74    119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
75    129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
76    218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
77     81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
78    184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
79    222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180,
80];
81
82/// 3-D gradient vectors (Perlin)
83pub const GRAD3: [[f32; 3]; 16] = [
84    [ 1.0, 1.0, 0.0], [-1.0, 1.0, 0.0], [ 1.0,-1.0, 0.0], [-1.0,-1.0, 0.0],
85    [ 1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [ 1.0, 0.0,-1.0], [-1.0, 0.0,-1.0],
86    [ 0.0, 1.0, 1.0], [ 0.0,-1.0, 1.0], [ 0.0, 1.0,-1.0], [ 0.0,-1.0,-1.0],
87    [ 1.0, 1.0, 0.0], [-1.0, 1.0, 0.0], [ 0.0,-1.0, 1.0], [ 0.0,-1.0,-1.0],
88];
89
90// Simplex noise 4-D gradient
91pub const GRAD4: [[f32; 4]; 32] = [
92    [ 0.0, 1.0, 1.0, 1.0],[ 0.0, 1.0, 1.0,-1.0],[ 0.0, 1.0,-1.0, 1.0],[ 0.0, 1.0,-1.0,-1.0],
93    [ 0.0,-1.0, 1.0, 1.0],[ 0.0,-1.0, 1.0,-1.0],[ 0.0,-1.0,-1.0, 1.0],[ 0.0,-1.0,-1.0,-1.0],
94    [ 1.0, 0.0, 1.0, 1.0],[ 1.0, 0.0, 1.0,-1.0],[ 1.0, 0.0,-1.0, 1.0],[ 1.0, 0.0,-1.0,-1.0],
95    [-1.0, 0.0, 1.0, 1.0],[-1.0, 0.0, 1.0,-1.0],[-1.0, 0.0,-1.0, 1.0],[-1.0, 0.0,-1.0,-1.0],
96    [ 1.0, 1.0, 0.0, 1.0],[ 1.0, 1.0, 0.0,-1.0],[ 1.0,-1.0, 0.0, 1.0],[ 1.0,-1.0, 0.0,-1.0],
97    [-1.0, 1.0, 0.0, 1.0],[-1.0, 1.0, 0.0,-1.0],[-1.0,-1.0, 0.0, 1.0],[-1.0,-1.0, 0.0,-1.0],
98    [ 1.0, 1.0, 1.0, 0.0],[ 1.0, 1.0,-1.0, 0.0],[ 1.0,-1.0, 1.0, 0.0],[ 1.0,-1.0,-1.0, 0.0],
99    [-1.0, 1.0, 1.0, 0.0],[-1.0, 1.0,-1.0, 0.0],[-1.0,-1.0, 1.0, 0.0],[-1.0,-1.0,-1.0, 0.0],
100];
101
102// ============================================================
103//  BIOME SYSTEM
104// ============================================================
105
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub enum BiomeId {
108    TropicalRainforest    = 0,
109    TropicalSavanna       = 1,
110    HotDesert             = 2,
111    ColdDesert            = 3,
112    XericShrubland        = 4,
113    MediterraneanShrub    = 5,
114    TemperateGrassland    = 6,
115    TemperateRainforest   = 7,
116    TemperateDeciduous    = 8,
117    BorealForest          = 9,
118    TaigaSpruce           = 10,
119    Tundra                = 11,
120    ArcticDesert          = 12,
121    AlpineMeadow          = 13,
122    AlpineTundra          = 14,
123    PolarIceCap           = 15,
124    Mangrove              = 16,
125    Wetland               = 17,
126    FloodPlain            = 18,
127    VolcanicLandscape     = 19,
128    SaltFlat              = 20,
129    GlacialValley         = 21,
130    CoastalDunes          = 22,
131    DeepOceanFloor        = 23,
132    CoralReef             = 24,
133}
134
135#[derive(Clone, Debug)]
136pub struct BiomeDescriptor {
137    pub id: BiomeId,
138    pub name: &'static str,
139    pub temp_min: f32,
140    pub temp_max: f32,
141    pub humidity_min: f32,
142    pub humidity_max: f32,
143    pub alt_min: f32,
144    pub alt_max: f32,
145    pub ground_color: Vec3,
146    pub tree_density: f32,
147    pub grass_density: f32,
148    pub rock_density: f32,
149    pub snow_coverage: f32,
150    pub rainfall_mm: f32,
151    pub wind_speed_ms: f32,
152    pub fog_density: f32,
153}
154
155impl BiomeDescriptor {
156    pub fn classify_point(temp: f32, humidity: f32, altitude: f32) -> BiomeId {
157        // Altitude override first
158        if altitude > 0.88 {
159            return BiomeId::PolarIceCap;
160        }
161        if altitude > 0.75 {
162            return BiomeId::AlpineTundra;
163        }
164        if altitude > 0.62 {
165            return BiomeId::AlpineMeadow;
166        }
167
168        // Temperature + humidity classification
169        if temp > 24.0 {
170            if humidity > 0.80 {
171                return BiomeId::TropicalRainforest;
172            } else if humidity > 0.50 {
173                return BiomeId::TropicalSavanna;
174            } else if humidity > 0.25 {
175                return BiomeId::XericShrubland;
176            } else {
177                return BiomeId::HotDesert;
178            }
179        } else if temp > 10.0 {
180            if humidity > 0.70 {
181                return BiomeId::TemperateRainforest;
182            } else if humidity > 0.50 {
183                return BiomeId::TemperateDeciduous;
184            } else if humidity > 0.28 {
185                return BiomeId::MediterraneanShrub;
186            } else {
187                return BiomeId::XericShrubland;
188            }
189        } else if temp > 0.0 {
190            if humidity > 0.65 {
191                return BiomeId::BorealForest;
192            } else if humidity > 0.40 {
193                return BiomeId::TemperateGrassland;
194            } else {
195                return BiomeId::ColdDesert;
196            }
197        } else if temp > -10.0 {
198            if humidity > 0.50 {
199                return BiomeId::TaigaSpruce;
200            } else {
201                return BiomeId::Tundra;
202            }
203        } else {
204            if humidity > 0.30 {
205                return BiomeId::Tundra;
206            } else {
207                return BiomeId::ArcticDesert;
208            }
209        }
210    }
211
212    /// Compute a blend weight for this biome at a given (temp, humidity, altitude)
213    pub fn blend_weight(&self, temp: f32, humidity: f32, altitude: f32) -> f32 {
214        let temp_w     = gaussian_falloff(temp,     (self.temp_min     + self.temp_max)     * 0.5, (self.temp_max     - self.temp_min)     * 0.5 + 0.5);
215        let hum_w      = gaussian_falloff(humidity, (self.humidity_min + self.humidity_max) * 0.5, (self.humidity_max - self.humidity_min) * 0.5 + 0.05);
216        let alt_w      = gaussian_falloff(altitude, (self.alt_min      + self.alt_max)      * 0.5, (self.alt_max      - self.alt_min)      * 0.5 + 0.05);
217        (temp_w * hum_w * alt_w).max(0.0)
218    }
219}
220
221fn gaussian_falloff(x: f32, center: f32, sigma: f32) -> f32 {
222    let diff = x - center;
223    (-(diff * diff) / (2.0 * sigma * sigma)).exp()
224}
225
226/// Build the full 25-biome table at runtime
227pub fn build_biome_table() -> Vec<BiomeDescriptor> {
228    vec![
229        BiomeDescriptor {
230            id: BiomeId::TropicalRainforest, name: "Tropical Rainforest",
231            temp_min: 24.0, temp_max: 36.0, humidity_min: 0.80, humidity_max: 1.00,
232            alt_min: 0.00, alt_max: 0.30, ground_color: Vec3::new(0.04, 0.35, 0.06),
233            tree_density: 0.95, grass_density: 0.60, rock_density: 0.03,
234            snow_coverage: 0.00, rainfall_mm: 3000.0, wind_speed_ms: 2.0, fog_density: 0.15,
235        },
236        BiomeDescriptor {
237            id: BiomeId::TropicalSavanna, name: "Tropical Savanna",
238            temp_min: 20.0, temp_max: 35.0, humidity_min: 0.25, humidity_max: 0.55,
239            alt_min: 0.00, alt_max: 0.30, ground_color: Vec3::new(0.62, 0.55, 0.14),
240            tree_density: 0.18, grass_density: 0.85, rock_density: 0.10,
241            snow_coverage: 0.00, rainfall_mm: 900.0, wind_speed_ms: 4.0, fog_density: 0.02,
242        },
243        BiomeDescriptor {
244            id: BiomeId::HotDesert, name: "Hot Desert",
245            temp_min: 20.0, temp_max: 52.0, humidity_min: 0.00, humidity_max: 0.18,
246            alt_min: 0.00, alt_max: 0.35, ground_color: Vec3::new(0.87, 0.79, 0.41),
247            tree_density: 0.01, grass_density: 0.04, rock_density: 0.40,
248            snow_coverage: 0.00, rainfall_mm: 80.0, wind_speed_ms: 7.0, fog_density: 0.00,
249        },
250        BiomeDescriptor {
251            id: BiomeId::ColdDesert, name: "Cold Desert",
252            temp_min: -10.0, temp_max: 15.0, humidity_min: 0.00, humidity_max: 0.20,
253            alt_min: 0.00, alt_max: 0.45, ground_color: Vec3::new(0.70, 0.65, 0.50),
254            tree_density: 0.02, grass_density: 0.10, rock_density: 0.50,
255            snow_coverage: 0.10, rainfall_mm: 150.0, wind_speed_ms: 8.0, fog_density: 0.01,
256        },
257        BiomeDescriptor {
258            id: BiomeId::XericShrubland, name: "Xeric Shrubland",
259            temp_min: 10.0, temp_max: 30.0, humidity_min: 0.10, humidity_max: 0.30,
260            alt_min: 0.00, alt_max: 0.40, ground_color: Vec3::new(0.70, 0.65, 0.30),
261            tree_density: 0.05, grass_density: 0.40, rock_density: 0.30,
262            snow_coverage: 0.00, rainfall_mm: 300.0, wind_speed_ms: 5.0, fog_density: 0.01,
263        },
264        BiomeDescriptor {
265            id: BiomeId::MediterraneanShrub, name: "Mediterranean Shrubland",
266            temp_min: 5.0, temp_max: 28.0, humidity_min: 0.25, humidity_max: 0.50,
267            alt_min: 0.00, alt_max: 0.40, ground_color: Vec3::new(0.55, 0.62, 0.20),
268            tree_density: 0.25, grass_density: 0.55, rock_density: 0.20,
269            snow_coverage: 0.00, rainfall_mm: 600.0, wind_speed_ms: 4.0, fog_density: 0.03,
270        },
271        BiomeDescriptor {
272            id: BiomeId::TemperateGrassland, name: "Temperate Grassland",
273            temp_min: -5.0, temp_max: 20.0, humidity_min: 0.20, humidity_max: 0.50,
274            alt_min: 0.00, alt_max: 0.45, ground_color: Vec3::new(0.50, 0.70, 0.15),
275            tree_density: 0.05, grass_density: 0.90, rock_density: 0.05,
276            snow_coverage: 0.05, rainfall_mm: 500.0, wind_speed_ms: 5.5, fog_density: 0.05,
277        },
278        BiomeDescriptor {
279            id: BiomeId::TemperateRainforest, name: "Temperate Rainforest",
280            temp_min: 5.0, temp_max: 20.0, humidity_min: 0.70, humidity_max: 1.00,
281            alt_min: 0.00, alt_max: 0.50, ground_color: Vec3::new(0.10, 0.40, 0.10),
282            tree_density: 0.85, grass_density: 0.50, rock_density: 0.08,
283            snow_coverage: 0.00, rainfall_mm: 2500.0, wind_speed_ms: 3.0, fog_density: 0.20,
284        },
285        BiomeDescriptor {
286            id: BiomeId::TemperateDeciduous, name: "Temperate Deciduous Forest",
287            temp_min: 5.0, temp_max: 22.0, humidity_min: 0.50, humidity_max: 0.75,
288            alt_min: 0.00, alt_max: 0.50, ground_color: Vec3::new(0.20, 0.50, 0.10),
289            tree_density: 0.70, grass_density: 0.35, rock_density: 0.10,
290            snow_coverage: 0.05, rainfall_mm: 1100.0, wind_speed_ms: 3.5, fog_density: 0.08,
291        },
292        BiomeDescriptor {
293            id: BiomeId::BorealForest, name: "Boreal Forest",
294            temp_min: -10.0, temp_max: 10.0, humidity_min: 0.45, humidity_max: 0.70,
295            alt_min: 0.00, alt_max: 0.55, ground_color: Vec3::new(0.15, 0.35, 0.12),
296            tree_density: 0.75, grass_density: 0.20, rock_density: 0.12,
297            snow_coverage: 0.25, rainfall_mm: 700.0, wind_speed_ms: 4.0, fog_density: 0.10,
298        },
299        BiomeDescriptor {
300            id: BiomeId::TaigaSpruce, name: "Taiga Spruce",
301            temp_min: -20.0, temp_max: 5.0, humidity_min: 0.40, humidity_max: 0.65,
302            alt_min: 0.00, alt_max: 0.60, ground_color: Vec3::new(0.12, 0.28, 0.12),
303            tree_density: 0.65, grass_density: 0.15, rock_density: 0.15,
304            snow_coverage: 0.45, rainfall_mm: 550.0, wind_speed_ms: 5.0, fog_density: 0.12,
305        },
306        BiomeDescriptor {
307            id: BiomeId::Tundra, name: "Tundra",
308            temp_min: -25.0, temp_max: 0.0, humidity_min: 0.20, humidity_max: 0.55,
309            alt_min: 0.00, alt_max: 0.65, ground_color: Vec3::new(0.45, 0.50, 0.30),
310            tree_density: 0.02, grass_density: 0.50, rock_density: 0.30,
311            snow_coverage: 0.60, rainfall_mm: 280.0, wind_speed_ms: 8.0, fog_density: 0.15,
312        },
313        BiomeDescriptor {
314            id: BiomeId::ArcticDesert, name: "Arctic Desert",
315            temp_min: -40.0, temp_max: -10.0, humidity_min: 0.00, humidity_max: 0.20,
316            alt_min: 0.00, alt_max: 0.70, ground_color: Vec3::new(0.80, 0.85, 0.90),
317            tree_density: 0.00, grass_density: 0.02, rock_density: 0.20,
318            snow_coverage: 0.90, rainfall_mm: 100.0, wind_speed_ms: 12.0, fog_density: 0.10,
319        },
320        BiomeDescriptor {
321            id: BiomeId::AlpineMeadow, name: "Alpine Meadow",
322            temp_min: -5.0, temp_max: 12.0, humidity_min: 0.40, humidity_max: 0.75,
323            alt_min: 0.58, alt_max: 0.75, ground_color: Vec3::new(0.35, 0.60, 0.20),
324            tree_density: 0.10, grass_density: 0.75, rock_density: 0.25,
325            snow_coverage: 0.20, rainfall_mm: 800.0, wind_speed_ms: 6.0, fog_density: 0.08,
326        },
327        BiomeDescriptor {
328            id: BiomeId::AlpineTundra, name: "Alpine Tundra",
329            temp_min: -15.0, temp_max: 5.0, humidity_min: 0.20, humidity_max: 0.60,
330            alt_min: 0.72, alt_max: 0.88, ground_color: Vec3::new(0.40, 0.42, 0.38),
331            tree_density: 0.00, grass_density: 0.30, rock_density: 0.60,
332            snow_coverage: 0.50, rainfall_mm: 500.0, wind_speed_ms: 10.0, fog_density: 0.12,
333        },
334        BiomeDescriptor {
335            id: BiomeId::PolarIceCap, name: "Polar Ice Cap",
336            temp_min: -50.0, temp_max: -5.0, humidity_min: 0.00, humidity_max: 0.30,
337            alt_min: 0.85, alt_max: 1.00, ground_color: Vec3::new(0.92, 0.95, 1.00),
338            tree_density: 0.00, grass_density: 0.00, rock_density: 0.05,
339            snow_coverage: 1.00, rainfall_mm: 50.0, wind_speed_ms: 15.0, fog_density: 0.20,
340        },
341        BiomeDescriptor {
342            id: BiomeId::Mangrove, name: "Mangrove",
343            temp_min: 20.0, temp_max: 35.0, humidity_min: 0.70, humidity_max: 1.00,
344            alt_min: 0.00, alt_max: 0.08, ground_color: Vec3::new(0.20, 0.35, 0.10),
345            tree_density: 0.70, grass_density: 0.30, rock_density: 0.02,
346            snow_coverage: 0.00, rainfall_mm: 2000.0, wind_speed_ms: 2.0, fog_density: 0.25,
347        },
348        BiomeDescriptor {
349            id: BiomeId::Wetland, name: "Wetland",
350            temp_min: 0.0, temp_max: 25.0, humidity_min: 0.75, humidity_max: 1.00,
351            alt_min: 0.00, alt_max: 0.15, ground_color: Vec3::new(0.18, 0.32, 0.10),
352            tree_density: 0.30, grass_density: 0.80, rock_density: 0.02,
353            snow_coverage: 0.00, rainfall_mm: 1400.0, wind_speed_ms: 2.0, fog_density: 0.30,
354        },
355        BiomeDescriptor {
356            id: BiomeId::FloodPlain, name: "Flood Plain",
357            temp_min: 10.0, temp_max: 30.0, humidity_min: 0.55, humidity_max: 0.85,
358            alt_min: 0.00, alt_max: 0.12, ground_color: Vec3::new(0.40, 0.55, 0.15),
359            tree_density: 0.15, grass_density: 0.85, rock_density: 0.03,
360            snow_coverage: 0.00, rainfall_mm: 1200.0, wind_speed_ms: 3.0, fog_density: 0.12,
361        },
362        BiomeDescriptor {
363            id: BiomeId::VolcanicLandscape, name: "Volcanic Landscape",
364            temp_min: 5.0, temp_max: 40.0, humidity_min: 0.10, humidity_max: 0.60,
365            alt_min: 0.10, alt_max: 0.70, ground_color: Vec3::new(0.12, 0.10, 0.10),
366            tree_density: 0.05, grass_density: 0.10, rock_density: 0.85,
367            snow_coverage: 0.00, rainfall_mm: 400.0, wind_speed_ms: 6.0, fog_density: 0.20,
368        },
369        BiomeDescriptor {
370            id: BiomeId::SaltFlat, name: "Salt Flat",
371            temp_min: 15.0, temp_max: 45.0, humidity_min: 0.00, humidity_max: 0.12,
372            alt_min: 0.00, alt_max: 0.10, ground_color: Vec3::new(0.95, 0.95, 0.92),
373            tree_density: 0.00, grass_density: 0.03, rock_density: 0.05,
374            snow_coverage: 0.00, rainfall_mm: 50.0, wind_speed_ms: 8.0, fog_density: 0.00,
375        },
376        BiomeDescriptor {
377            id: BiomeId::GlacialValley, name: "Glacial Valley",
378            temp_min: -20.0, temp_max: 2.0, humidity_min: 0.30, humidity_max: 0.70,
379            alt_min: 0.30, alt_max: 0.80, ground_color: Vec3::new(0.55, 0.65, 0.70),
380            tree_density: 0.05, grass_density: 0.15, rock_density: 0.60,
381            snow_coverage: 0.70, rainfall_mm: 600.0, wind_speed_ms: 7.0, fog_density: 0.15,
382        },
383        BiomeDescriptor {
384            id: BiomeId::CoastalDunes, name: "Coastal Dunes",
385            temp_min: 10.0, temp_max: 35.0, humidity_min: 0.15, humidity_max: 0.45,
386            alt_min: 0.00, alt_max: 0.10, ground_color: Vec3::new(0.90, 0.85, 0.65),
387            tree_density: 0.05, grass_density: 0.30, rock_density: 0.10,
388            snow_coverage: 0.00, rainfall_mm: 350.0, wind_speed_ms: 9.0, fog_density: 0.08,
389        },
390        BiomeDescriptor {
391            id: BiomeId::DeepOceanFloor, name: "Deep Ocean Floor",
392            temp_min: 2.0, temp_max: 8.0, humidity_min: 1.00, humidity_max: 1.00,
393            alt_min: 0.00, alt_max: 0.05, ground_color: Vec3::new(0.05, 0.06, 0.15),
394            tree_density: 0.00, grass_density: 0.05, rock_density: 0.20,
395            snow_coverage: 0.00, rainfall_mm: 0.0, wind_speed_ms: 0.0, fog_density: 0.90,
396        },
397        BiomeDescriptor {
398            id: BiomeId::CoralReef, name: "Coral Reef",
399            temp_min: 22.0, temp_max: 32.0, humidity_min: 0.90, humidity_max: 1.00,
400            alt_min: 0.00, alt_max: 0.06, ground_color: Vec3::new(0.90, 0.60, 0.40),
401            tree_density: 0.00, grass_density: 0.60, rock_density: 0.30,
402            snow_coverage: 0.00, rainfall_mm: 0.0, wind_speed_ms: 0.0, fog_density: 0.30,
403        },
404    ]
405}
406
407#[derive(Clone, Debug)]
408pub struct BiomeBlendSample {
409    pub weights: [f32; 25],
410    pub dominant: BiomeId,
411    pub blended_color: Vec3,
412    pub blended_tree_density: f32,
413    pub blended_grass_density: f32,
414    pub blended_rock_density: f32,
415    pub blended_snow: f32,
416}
417
418pub struct BiomeSystem {
419    pub descriptors: Vec<BiomeDescriptor>,
420}
421
422impl BiomeSystem {
423    pub fn new() -> Self {
424        Self { descriptors: build_biome_table() }
425    }
426
427    /// Full biome blend at a point using gaussian weighting
428    pub fn sample(&self, temp: f32, humidity: f32, altitude: f32) -> BiomeBlendSample {
429        let mut weights = [0.0f32; 25];
430        let mut weight_sum = 0.0f32;
431
432        for (i, desc) in self.descriptors.iter().enumerate() {
433            let w = desc.blend_weight(temp, humidity, altitude);
434            weights[i] = w;
435            weight_sum += w;
436        }
437
438        // Normalize
439        if weight_sum < 1e-10 {
440            // fallback: use classified biome
441            let id = BiomeDescriptor::classify_point(temp, humidity, altitude) as usize;
442            weights[id] = 1.0;
443            weight_sum = 1.0;
444        }
445        for w in weights.iter_mut() {
446            *w /= weight_sum;
447        }
448
449        // Find dominant
450        let dominant_idx = weights.iter().enumerate()
451            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
452            .map(|(i, _)| i)
453            .unwrap_or(0);
454
455        // Blend properties
456        let mut blended_color       = Vec3::ZERO;
457        let mut blended_tree        = 0.0f32;
458        let mut blended_grass       = 0.0f32;
459        let mut blended_rock        = 0.0f32;
460        let mut blended_snow        = 0.0f32;
461
462        for (i, desc) in self.descriptors.iter().enumerate() {
463            let w = weights[i];
464            blended_color  += desc.ground_color  * w;
465            blended_tree   += desc.tree_density  * w;
466            blended_grass  += desc.grass_density * w;
467            blended_rock   += desc.rock_density  * w;
468            blended_snow   += desc.snow_coverage * w;
469        }
470
471        let dominant_id = self.descriptors[dominant_idx].id;
472        BiomeBlendSample {
473            weights,
474            dominant: dominant_id,
475            blended_color,
476            blended_tree_density: blended_tree,
477            blended_grass_density: blended_grass,
478            blended_rock_density: blended_rock,
479            blended_snow,
480        }
481    }
482
483    /// Classify transition zone between two biomes (returns a blend factor 0..1)
484    pub fn transition_factor(&self, biome_a: BiomeId, biome_b: BiomeId,
485                              temp: f32, humidity: f32, altitude: f32) -> f32 {
486        let wa = self.descriptors[biome_a as usize].blend_weight(temp, humidity, altitude);
487        let wb = self.descriptors[biome_b as usize].blend_weight(temp, humidity, altitude);
488        if wa + wb < 1e-10 { return 0.5; }
489        wa / (wa + wb)
490    }
491
492    /// Get wind speed interpolated across biome weights at a sample
493    pub fn wind_speed(&self, sample: &BiomeBlendSample) -> f32 {
494        let mut speed = 0.0f32;
495        for (i, desc) in self.descriptors.iter().enumerate() {
496            speed += desc.wind_speed_ms * sample.weights[i];
497        }
498        speed
499    }
500}
501
502// ============================================================
503//  PROCEDURAL NOISE
504// ============================================================
505
506// --- Perlin Noise ---
507
508#[inline]
509fn fade(t: f32) -> f32 {
510    // Ken Perlin's 6t^5 - 15t^4 + 10t^3
511    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
512}
513
514#[inline]
515fn lerp_f(a: f32, b: f32, t: f32) -> f32 {
516    a + t * (b - a)
517}
518
519#[inline]
520fn grad3(hash: u8, x: f32, y: f32, z: f32) -> f32 {
521    let h = (hash & 15) as usize;
522    let g = &GRAD3[h];
523    g[0] * x + g[1] * y + g[2] * z
524}
525
526pub fn perlin_noise_3d(x: f32, y: f32, z: f32) -> f32 {
527    let xi = x.floor() as i32;
528    let yi = y.floor() as i32;
529    let zi = z.floor() as i32;
530
531    let xf = x - xi as f32;
532    let yf = y - yi as f32;
533    let zf = z - zi as f32;
534
535    let u = fade(xf);
536    let v = fade(yf);
537    let w = fade(zf);
538
539    let xi = (xi & 255) as usize;
540    let yi = (yi & 255) as usize;
541    let zi = (zi & 255) as usize;
542
543    let aaa = PERM[PERM[PERM[xi]     as usize + yi]     as usize + zi]     as u8;
544    let aba = PERM[PERM[PERM[xi]     as usize + yi + 1] as usize + zi]     as u8;
545    let aab = PERM[PERM[PERM[xi]     as usize + yi]     as usize + zi + 1] as u8;
546    let abb = PERM[PERM[PERM[xi]     as usize + yi + 1] as usize + zi + 1] as u8;
547    let baa = PERM[PERM[PERM[xi + 1] as usize + yi]     as usize + zi]     as u8;
548    let bba = PERM[PERM[PERM[xi + 1] as usize + yi + 1] as usize + zi]     as u8;
549    let bab = PERM[PERM[PERM[xi + 1] as usize + yi]     as usize + zi + 1] as u8;
550    let bbb = PERM[PERM[PERM[xi + 1] as usize + yi + 1] as usize + zi + 1] as u8;
551
552    let x1 = lerp_f(grad3(aaa, xf,       yf,       zf),       grad3(baa, xf - 1.0, yf,       zf),       u);
553    let x2 = lerp_f(grad3(aba, xf,       yf - 1.0, zf),       grad3(bba, xf - 1.0, yf - 1.0, zf),       u);
554    let y1 = lerp_f(x1, x2, v);
555
556    let x3 = lerp_f(grad3(aab, xf,       yf,       zf - 1.0), grad3(bab, xf - 1.0, yf,       zf - 1.0), u);
557    let x4 = lerp_f(grad3(abb, xf,       yf - 1.0, zf - 1.0), grad3(bbb, xf - 1.0, yf - 1.0, zf - 1.0), u);
558    let y2 = lerp_f(x3, x4, v);
559
560    lerp_f(y1, y2, w)
561}
562
563pub fn perlin_noise_2d(x: f32, y: f32) -> f32 {
564    perlin_noise_3d(x, y, 0.0)
565}
566
567// --- Simplex Noise 3D ---
568
569#[inline]
570fn simplex_grad3(hash: u8, x: f32, y: f32, z: f32) -> f32 {
571    let h = (hash & 15) as usize;
572    let g = &GRAD3[h];
573    g[0] * x + g[1] * y + g[2] * z
574}
575
576pub fn simplex_noise_3d(xin: f32, yin: f32, zin: f32) -> f32 {
577    // Simplex noise: Ken Perlin's improved algorithm
578    let f3 = 1.0 / 3.0_f32;
579    let g3 = 1.0 / 6.0_f32;
580
581    let s = (xin + yin + zin) * f3;
582    let i = (xin + s).floor() as i32;
583    let j = (yin + s).floor() as i32;
584    let k = (zin + s).floor() as i32;
585
586    let t = (i + j + k) as f32 * g3;
587    let x0 = xin - (i as f32 - t);
588    let y0 = yin - (j as f32 - t);
589    let z0 = zin - (k as f32 - t);
590
591    // Determine which simplex we're in
592    let (i1, j1, k1, i2, j2, k2);
593    if x0 >= y0 {
594        if y0 >= z0      { i1=1;j1=0;k1=0; i2=1;j2=1;k2=0; }
595        else if x0 >= z0 { i1=1;j1=0;k1=0; i2=1;j2=0;k2=1; }
596        else             { i1=0;j1=0;k1=1; i2=1;j2=0;k2=1; }
597    } else {
598        if y0 < z0       { i1=0;j1=0;k1=1; i2=0;j2=1;k2=1; }
599        else if x0 < z0  { i1=0;j1=1;k1=0; i2=0;j2=1;k2=1; }
600        else             { i1=0;j1=1;k1=0; i2=1;j2=1;k2=0; }
601    }
602
603    let x1 = x0 - i1 as f32 + g3;
604    let y1 = y0 - j1 as f32 + g3;
605    let z1 = z0 - k1 as f32 + g3;
606    let x2 = x0 - i2 as f32 + 2.0 * g3;
607    let y2 = y0 - j2 as f32 + 2.0 * g3;
608    let z2 = z0 - k2 as f32 + 2.0 * g3;
609    let x3 = x0 - 1.0 + 3.0 * g3;
610    let y3 = y0 - 1.0 + 3.0 * g3;
611    let z3 = z0 - 1.0 + 3.0 * g3;
612
613    let ii = (i & 255) as usize;
614    let jj = (j & 255) as usize;
615    let kk = (k & 255) as usize;
616
617    let gi0 = PERM[ii  + PERM[jj  + PERM[kk ] as usize] as usize] & 15;
618    let gi1 = PERM[ii + i1 as usize + PERM[jj + j1 as usize + PERM[(kk + k1 as usize) & 255] as usize] as usize] & 15;
619    let gi2 = PERM[ii + i2 as usize + PERM[jj + j2 as usize + PERM[(kk + k2 as usize) & 255] as usize] as usize] & 15;
620    let gi3 = PERM[(ii+1)&255 + PERM[(jj+1)&255 + PERM[(kk+1)&255] as usize] as usize] & 15;
621
622    let t0 = 0.6 - x0*x0 - y0*y0 - z0*z0;
623    let n0 = if t0 < 0.0 { 0.0 } else { t0*t0*t0*t0 * simplex_grad3(gi0, x0, y0, z0) };
624
625    let t1 = 0.6 - x1*x1 - y1*y1 - z1*z1;
626    let n1 = if t1 < 0.0 { 0.0 } else { t1*t1*t1*t1 * simplex_grad3(gi1, x1, y1, z1) };
627
628    let t2 = 0.6 - x2*x2 - y2*y2 - z2*z2;
629    let n2 = if t2 < 0.0 { 0.0 } else { t2*t2*t2*t2 * simplex_grad3(gi2, x2, y2, z2) };
630
631    let t3 = 0.6 - x3*x3 - y3*y3 - z3*z3;
632    let n3 = if t3 < 0.0 { 0.0 } else { t3*t3*t3*t3 * simplex_grad3(gi3, x3, y3, z3) };
633
634    32.0 * (n0 + n1 + n2 + n3)
635}
636
637// --- Worley / Cellular Noise ---
638
639/// Returns (F1, F2) — the two nearest feature-point distances
640pub fn worley_noise_2d(x: f32, y: f32) -> (f32, f32) {
641    let cx = x.floor() as i32;
642    let cy = y.floor() as i32;
643
644    let mut f1 = f32::MAX;
645    let mut f2 = f32::MAX;
646
647    for dx in -2..=2i32 {
648        for dy in -2..=2i32 {
649            let nx = cx + dx;
650            let ny = cy + dy;
651            // pseudo-random offset inside this cell
652            let hash = worley_hash(nx, ny);
653            let fx = nx as f32 + ((hash & 0xFFFF) as f32 / 65535.0);
654            let fy = ny as f32 + (((hash >> 16) & 0xFFFF) as f32 / 65535.0);
655            let dist = ((fx - x) * (fx - x) + (fy - y) * (fy - y)).sqrt();
656            if dist < f1       { f2 = f1; f1 = dist; }
657            else if dist < f2  { f2 = dist; }
658        }
659    }
660    (f1, f2)
661}
662
663pub fn worley_noise_3d(x: f32, y: f32, z: f32) -> (f32, f32) {
664    let cx = x.floor() as i32;
665    let cy = y.floor() as i32;
666    let cz = z.floor() as i32;
667
668    let mut f1 = f32::MAX;
669    let mut f2 = f32::MAX;
670
671    for dx in -1..=1i32 {
672        for dy in -1..=1i32 {
673            for dz in -1..=1i32 {
674                let nx = cx + dx;
675                let ny = cy + dy;
676                let nz = cz + dz;
677                let h = worley_hash_3d(nx, ny, nz);
678                let fx = nx as f32 + ((h & 0x3FF) as f32 / 1023.0);
679                let fy = ny as f32 + (((h >> 10) & 0x3FF) as f32 / 1023.0);
680                let fz = nz as f32 + (((h >> 20) & 0x3FF) as f32 / 1023.0);
681                let dist = ((fx-x)*(fx-x) + (fy-y)*(fy-y) + (fz-z)*(fz-z)).sqrt();
682                if dist < f1      { f2 = f1; f1 = dist; }
683                else if dist < f2 { f2 = dist; }
684            }
685        }
686    }
687    (f1, f2)
688}
689
690#[inline]
691fn worley_hash(x: i32, y: i32) -> u32 {
692    let mut h = (x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337))) as u32;
693    h ^= h >> 16;
694    h = h.wrapping_mul(0x45d9f3b);
695    h ^= h >> 16;
696    h
697}
698
699#[inline]
700fn worley_hash_3d(x: i32, y: i32, z: i32) -> u32 {
701    let mut h = (x.wrapping_mul(1619)
702        .wrapping_add(y.wrapping_mul(31337))
703        .wrapping_add(z.wrapping_mul(1013))) as u32;
704    h ^= h >> 16;
705    h = h.wrapping_mul(0x45d9f3b);
706    h ^= h >> 16;
707    h = h.wrapping_mul(0xd7e7f3b);
708    h ^= h >> 16;
709    h
710}
711
712// --- Fractal Brownian Motion ---
713
714#[derive(Clone, Debug)]
715pub struct FbmParams {
716    pub octaves:    usize,
717    pub frequency:  f32,
718    pub lacunarity: f32,
719    pub gain:       f32,
720    pub amplitude:  f32,
721    pub offset:     f32,
722    pub ridge:      bool,
723}
724
725impl FbmParams {
726    pub fn default_terrain() -> Self {
727        FbmParams { octaves: 8, frequency: 1.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 1.0, ridge: false }
728    }
729    pub fn default_ridge() -> Self {
730        FbmParams { octaves: 6, frequency: 1.0, lacunarity: 2.2, gain: 0.6, amplitude: 1.0, offset: 1.0, ridge: true }
731    }
732    pub fn default_cloud() -> Self {
733        FbmParams { octaves: 5, frequency: 2.0, lacunarity: 2.0, gain: 0.45, amplitude: 0.8, offset: 0.0, ridge: false }
734    }
735}
736
737pub fn fbm_3d(x: f32, y: f32, z: f32, params: &FbmParams) -> f32 {
738    let mut freq  = params.frequency;
739    let mut amp   = params.amplitude;
740    let mut value = 0.0f32;
741    let mut weight = 1.0f32;
742    let mut prev   = 1.0f32;
743
744    for i in 0..params.octaves {
745        let n = perlin_noise_3d(x * freq, y * freq, z * freq);
746
747        if params.ridge {
748            let ridged = (params.offset - n.abs()).abs();
749            let signal = ridged * ridged * weight;
750            weight     = (signal * 2.0).clamp(0.0, 1.0);
751            value += signal * amp;
752        } else {
753            value += n * amp;
754        }
755
756        freq *= params.lacunarity;
757        amp  *= params.gain;
758    }
759    value
760}
761
762pub fn fbm_2d(x: f32, y: f32, params: &FbmParams) -> f32 {
763    fbm_3d(x, y, 0.0, params)
764}
765
766/// Turbulence (absolute value FBM)
767pub fn turbulence_2d(x: f32, y: f32, octaves: usize, freq: f32, gain: f32, lacunarity: f32) -> f32 {
768    let mut f   = freq;
769    let mut amp = 1.0f32;
770    let mut v   = 0.0f32;
771    let mut max = 0.0f32;
772    for _ in 0..octaves {
773        v   += perlin_noise_2d(x * f, y * f).abs() * amp;
774        max += amp;
775        f   *= lacunarity;
776        amp *= gain;
777    }
778    if max > 0.0 { v / max } else { 0.0 }
779}
780
781/// Domain-warped FBM (Inigo Quilez style)
782pub fn domain_warp_fbm_2d(x: f32, y: f32, warp_strength: f32, params: &FbmParams) -> f32 {
783    let q_x = fbm_2d(x,             y,             params);
784    let q_y = fbm_2d(x + 5.2, y + 1.3, params);
785    let r_x = fbm_2d(x + warp_strength * q_x + 1.7, y + warp_strength * q_y + 9.2, params);
786    let r_y = fbm_2d(x + warp_strength * q_x + 8.3, y + warp_strength * q_y + 2.8, params);
787    fbm_2d(x + warp_strength * r_x, y + warp_strength * r_y, params)
788}
789
790// ============================================================
791//  HEIGHTMAP + HYDRAULIC EROSION
792// ============================================================
793
794#[derive(Clone, Debug)]
795pub struct Heightmap {
796    pub width:  usize,
797    pub height: usize,
798    pub data:   Vec<f32>,    // row-major, values 0..1
799    pub min_h:  f32,
800    pub max_h:  f32,
801}
802
803impl Heightmap {
804    pub fn new(width: usize, height: usize) -> Self {
805        Self {
806            width,
807            height,
808            data: vec![0.0; width * height],
809            min_h: 0.0,
810            max_h: 1.0,
811        }
812    }
813
814    #[inline]
815    pub fn index(&self, x: usize, y: usize) -> usize {
816        y * self.width + x
817    }
818
819    #[inline]
820    pub fn get(&self, x: usize, y: usize) -> f32 {
821        self.data[self.index(x, y)]
822    }
823
824    #[inline]
825    pub fn set(&mut self, x: usize, y: usize, v: f32) {
826        let idx = self.index(x, y);
827        self.data[idx] = v;
828    }
829
830    #[inline]
831    pub fn get_clamped(&self, x: i32, y: i32) -> f32 {
832        let cx = x.clamp(0, self.width  as i32 - 1) as usize;
833        let cy = y.clamp(0, self.height as i32 - 1) as usize;
834        self.get(cx, cy)
835    }
836
837    pub fn sample_bilinear(&self, u: f32, v: f32) -> f32 {
838        let px = u * (self.width  - 1) as f32;
839        let py = v * (self.height - 1) as f32;
840        let x0 = px.floor() as i32;
841        let y0 = py.floor() as i32;
842        let x1 = x0 + 1;
843        let y1 = y0 + 1;
844        let tx = px - x0 as f32;
845        let ty = py - y0 as f32;
846        let a = self.get_clamped(x0, y0);
847        let b = self.get_clamped(x1, y0);
848        let c = self.get_clamped(x0, y1);
849        let d = self.get_clamped(x1, y1);
850        lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
851    }
852
853    /// Compute surface normal at a pixel using central differences
854    pub fn normal_at(&self, x: usize, y: usize, cell_size: f32) -> Vec3 {
855        let xi = x as i32;
856        let yi = y as i32;
857        let hL = self.get_clamped(xi - 1, yi);
858        let hR = self.get_clamped(xi + 1, yi);
859        let hD = self.get_clamped(xi, yi - 1);
860        let hU = self.get_clamped(xi, yi + 1);
861        let dx = (hR - hL) / (2.0 * cell_size);
862        let dz = (hU - hD) / (2.0 * cell_size);
863        Vec3::new(-dx, 1.0, -dz).normalize()
864    }
865
866    /// Compute slope (radians) at a pixel
867    pub fn slope_at(&self, x: usize, y: usize, cell_size: f32) -> f32 {
868        let n = self.normal_at(x, y, cell_size);
869        n.y.acos()
870    }
871
872    /// Compute gradient at a pixel — returns (dh/dx, dh/dy) in normalised coords
873    pub fn gradient_at(&self, x: usize, y: usize) -> Vec2 {
874        let xi = x as i32;
875        let yi = y as i32;
876        let dx = (self.get_clamped(xi + 1, yi) - self.get_clamped(xi - 1, yi)) * 0.5;
877        let dy = (self.get_clamped(xi, yi + 1) - self.get_clamped(xi, yi - 1)) * 0.5;
878        Vec2::new(dx, dy)
879    }
880
881    pub fn recompute_minmax(&mut self) {
882        self.min_h = self.data.iter().cloned().fold(f32::MAX, f32::min);
883        self.max_h = self.data.iter().cloned().fold(f32::MIN, f32::max);
884    }
885
886    pub fn normalize_to_01(&mut self) {
887        self.recompute_minmax();
888        let range = self.max_h - self.min_h;
889        if range < 1e-10 { return; }
890        for v in self.data.iter_mut() {
891            *v = (*v - self.min_h) / range;
892        }
893        self.min_h = 0.0;
894        self.max_h = 1.0;
895    }
896
897    /// Generate heightmap from FBM noise
898    pub fn generate_fbm(&mut self, params: &FbmParams, seed_offset: Vec2) {
899        for y in 0..self.height {
900            for x in 0..self.width {
901                let nx = x as f32 / self.width  as f32 + seed_offset.x;
902                let ny = y as f32 / self.height as f32 + seed_offset.y;
903                let h = fbm_2d(nx, ny, params) * 0.5 + 0.5;
904                self.set(x, y, h.clamp(0.0, 1.0));
905            }
906        }
907        self.recompute_minmax();
908    }
909
910    /// Generate with domain warped FBM for more natural terrain
911    pub fn generate_domain_warp(&mut self, params: &FbmParams, warp: f32, seed_offset: Vec2) {
912        for y in 0..self.height {
913            for x in 0..self.width {
914                let nx = x as f32 / self.width  as f32 + seed_offset.x;
915                let ny = y as f32 / self.height as f32 + seed_offset.y;
916                let h = domain_warp_fbm_2d(nx, ny, warp, params) * 0.5 + 0.5;
917                self.set(x, y, h.clamp(0.0, 1.0));
918            }
919        }
920        self.recompute_minmax();
921    }
922}
923
924// --- Hydraulic Erosion (particle-based Benes et al. / Sebastian Lague) ---
925
926#[derive(Clone, Debug)]
927pub struct ErosionParams {
928    pub num_particles:  usize,
929    pub inertia:        f32,   // 0..1 — how much particle keeps direction
930    pub capacity:       f32,   // max sediment a droplet can carry (proportional to speed)
931    pub deposition:     f32,   // fraction deposited when over-capacity
932    pub erosion_speed:  f32,   // how quickly terrain is eroded
933    pub evaporation:    f32,   // water lost per step
934    pub min_slope:      f32,   // prevents flat-area erosion artefacts
935    pub gravity:        f32,
936    pub max_steps:      usize,
937    pub erosion_radius: f32,   // radius for depositing sediment onto neighbours
938    pub seed:           u64,
939}
940
941impl Default for ErosionParams {
942    fn default() -> Self {
943        ErosionParams {
944            num_particles:  50_000,
945            inertia:        EROSION_INERTIA,
946            capacity:       EROSION_CAPACITY,
947            deposition:     EROSION_DEPOSITION,
948            erosion_speed:  EROSION_EROSION_SPEED,
949            evaporation:    EROSION_EVAPORATION,
950            min_slope:      EROSION_MIN_SLOPE,
951            gravity:        EROSION_GRAVITY,
952            max_steps:      EROSION_MAX_STEPS,
953            erosion_radius: 3.0,
954            seed:           0xDEAD_BEEF_1234,
955        }
956    }
957}
958
959struct LcgRng { state: u64 }
960impl LcgRng {
961    fn new(seed: u64) -> Self { Self { state: seed ^ 0x123456789ABCDEF } }
962    fn next_u64(&mut self) -> u64 {
963        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
964        self.state
965    }
966    fn next_f32(&mut self) -> f32 { (self.next_u64() >> 32) as f32 / u32::MAX as f32 }
967    fn next_f32_range(&mut self, min: f32, max: f32) -> f32 { min + self.next_f32() * (max - min) }
968}
969
970/// Bilinear height from a continuous position on the heightmap
971fn hmap_height_bilinear(data: &[f32], width: usize, height: usize, x: f32, y: f32) -> f32 {
972    let x0 = x.floor() as i32;
973    let y0 = y.floor() as i32;
974    let x1 = x0 + 1;
975    let y1 = y0 + 1;
976    let tx = x - x0 as f32;
977    let ty = y - y0 as f32;
978
979    let clamp_x = |v: i32| -> usize { v.clamp(0, width  as i32 - 1) as usize };
980    let clamp_y = |v: i32| -> usize { v.clamp(0, height as i32 - 1) as usize };
981
982    let a = data[clamp_y(y0) * width + clamp_x(x0)];
983    let b = data[clamp_y(y0) * width + clamp_x(x1)];
984    let c = data[clamp_y(y1) * width + clamp_x(x0)];
985    let d = data[clamp_y(y1) * width + clamp_x(x1)];
986
987    lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
988}
989
990/// Gradient of the heightmap using bilinear interpolation
991fn hmap_gradient(data: &[f32], width: usize, height: usize, x: f32, y: f32) -> Vec2 {
992    let gx = hmap_height_bilinear(data, width, height, x + 0.5, y)
993           - hmap_height_bilinear(data, width, height, x - 0.5, y);
994    let gy = hmap_height_bilinear(data, width, height, x, y + 0.5)
995           - hmap_height_bilinear(data, width, height, x, y - 0.5);
996    Vec2::new(gx, gy)
997}
998
999/// Erode a heightmap using particle-based hydraulic erosion
1000pub fn hydraulic_erosion(hmap: &mut Heightmap, params: &ErosionParams) {
1001    let w = hmap.width;
1002    let h = hmap.height;
1003    let mut rng = LcgRng::new(params.seed);
1004
1005    // Pre-compute erosion brush weights (circular kernel)
1006    let radius = params.erosion_radius;
1007    let brush_radius = radius.ceil() as i32;
1008    let mut brush_offsets: Vec<(i32, i32, f32)> = Vec::new();
1009    let mut brush_weight_sum = 0.0f32;
1010    for dy in -brush_radius..=brush_radius {
1011        for dx in -brush_radius..=brush_radius {
1012            let dist = ((dx*dx + dy*dy) as f32).sqrt();
1013            if dist <= radius {
1014                let w_val = 1.0 - dist / radius;
1015                brush_offsets.push((dx, dy, w_val));
1016                brush_weight_sum += w_val;
1017            }
1018        }
1019    }
1020    // normalize brush weights
1021    for b in brush_offsets.iter_mut() { b.2 /= brush_weight_sum; }
1022
1023    let data = &mut hmap.data;
1024
1025    for _particle in 0..params.num_particles {
1026        // spawn droplet at random position
1027        let mut pos_x = rng.next_f32_range(0.0, (w - 1) as f32);
1028        let mut pos_y = rng.next_f32_range(0.0, (h - 1) as f32);
1029        let mut vel_x = 0.0f32;
1030        let mut vel_y = 0.0f32;
1031        let mut speed  = 0.0f32;
1032        let mut water  = 1.0f32;
1033        let mut sediment = 0.0f32;
1034
1035        for _step in 0..params.max_steps {
1036            let node_x = pos_x.floor() as i32;
1037            let node_y = pos_y.floor() as i32;
1038
1039            if node_x < 0 || node_x >= w as i32 - 1 || node_y < 0 || node_y >= h as i32 - 1 {
1040                break;
1041            }
1042
1043            let grad = hmap_gradient(data, w, h, pos_x, pos_y);
1044            // Update direction (blend with gradient)
1045            vel_x = vel_x * params.inertia - grad.x * (1.0 - params.inertia);
1046            vel_y = vel_y * params.inertia - grad.y * (1.0 - params.inertia);
1047
1048            let vel_len = (vel_x * vel_x + vel_y * vel_y).sqrt();
1049            if vel_len < 1e-6 {
1050                break; // droplet stuck
1051            }
1052            vel_x /= vel_len;
1053            vel_y /= vel_len;
1054
1055            let new_x = pos_x + vel_x;
1056            let new_y = pos_y + vel_y;
1057
1058            // Height change
1059            let old_h = hmap_height_bilinear(data, w, h, pos_x, pos_y);
1060            let new_h = hmap_height_bilinear(data, w, h, new_x, new_y);
1061            let delta_h = new_h - old_h;
1062
1063            // Carrying capacity
1064            let slope = (-delta_h).max(params.min_slope);
1065            let carry_capacity = slope * vel_len * water * params.capacity;
1066
1067            if sediment > carry_capacity || delta_h > 0.0 {
1068                // Deposit sediment
1069                let amount = if delta_h > 0.0 {
1070                    sediment.min(delta_h)
1071                } else {
1072                    (sediment - carry_capacity) * params.deposition
1073                };
1074                sediment -= amount;
1075
1076                // Deposit around current position with brush
1077                for &(bdx, bdy, bw) in &brush_offsets {
1078                    let bx = node_x + bdx;
1079                    let by = node_y + bdy;
1080                    if bx >= 0 && bx < w as i32 && by >= 0 && by < h as i32 {
1081                        let idx = by as usize * w + bx as usize;
1082                        data[idx] += amount * bw;
1083                    }
1084                }
1085            } else {
1086                // Erode terrain
1087                let erode_amount = ((carry_capacity - sediment) * params.erosion_speed)
1088                    .min(-delta_h);
1089                let erode_amount = erode_amount.max(0.0);
1090                sediment += erode_amount;
1091
1092                for &(bdx, bdy, bw) in &brush_offsets {
1093                    let bx = node_x + bdx;
1094                    let by = node_y + bdy;
1095                    if bx >= 0 && bx < w as i32 && by >= 0 && by < h as i32 {
1096                        let idx = by as usize * w + bx as usize;
1097                        data[idx] -= erode_amount * bw;
1098                        if data[idx] < 0.0 { data[idx] = 0.0; }
1099                    }
1100                }
1101            }
1102
1103            speed = ((speed * speed + delta_h * params.gravity).max(0.0)).sqrt();
1104            water *= 1.0 - params.evaporation;
1105            pos_x = new_x;
1106            pos_y = new_y;
1107
1108            if water < 0.01 { break; }
1109        }
1110    }
1111
1112    hmap.recompute_minmax();
1113}
1114
1115/// Thermal erosion — material avalanches if slope exceeds talus angle
1116pub fn thermal_erosion(hmap: &mut Heightmap, iterations: usize, talus_angle: f32) {
1117    let w = hmap.width;
1118    let h = hmap.height;
1119    let talus = talus_angle.tan(); // in normalised height units per cell
1120
1121    for _iter in 0..iterations {
1122        let data_copy = hmap.data.clone();
1123        for y in 1..h-1 {
1124            for x in 1..w-1 {
1125                let center = data_copy[y * w + x];
1126                let neighbours = [
1127                    (x+1, y), (x-1, y), (x, y+1), (x, y-1),
1128                    (x+1, y+1), (x-1, y+1), (x+1, y-1), (x-1, y-1),
1129                ];
1130                let mut total_diff = 0.0f32;
1131                let mut max_diff   = 0.0f32;
1132                let mut count = 0usize;
1133                for &(nx, ny) in &neighbours {
1134                    let diff = center - data_copy[ny * w + nx];
1135                    if diff > talus {
1136                        total_diff += diff;
1137                        if diff > max_diff { max_diff = diff; }
1138                        count += 1;
1139                    }
1140                }
1141                if count == 0 || total_diff < 1e-8 { continue; }
1142                let move_frac = 0.5 * (max_diff - talus) / total_diff;
1143                for &(nx, ny) in &neighbours {
1144                    let diff = center - data_copy[ny * w + nx];
1145                    if diff > talus {
1146                        let transfer = move_frac * diff;
1147                        hmap.data[y * w + x]   -= transfer;
1148                        hmap.data[ny * w + nx] += transfer;
1149                    }
1150                }
1151            }
1152        }
1153    }
1154    hmap.recompute_minmax();
1155}
1156
1157// ============================================================
1158//  WATER BODIES — RIVER SIMULATION & LAKE FILLING
1159// ============================================================
1160
1161#[derive(Clone, Debug)]
1162pub struct RiverPath {
1163    pub points:      Vec<Vec2>,   // (x, y) in heightmap coords
1164    pub widths:      Vec<f32>,
1165    pub depths:      Vec<f32>,
1166    pub flow_rates:  Vec<f32>,
1167    pub source:      Vec2,
1168    pub mouth:       Vec2,
1169    pub total_length: f32,
1170}
1171
1172impl RiverPath {
1173    pub fn new() -> Self {
1174        RiverPath {
1175            points:      Vec::new(),
1176            widths:      Vec::new(),
1177            depths:      Vec::new(),
1178            flow_rates:  Vec::new(),
1179            source:      Vec2::ZERO,
1180            mouth:       Vec2::ZERO,
1181            total_length: 0.0,
1182        }
1183    }
1184
1185    pub fn compute_total_length(&mut self) {
1186        let mut len = 0.0f32;
1187        for i in 1..self.points.len() {
1188            len += (self.points[i] - self.points[i-1]).length();
1189        }
1190        self.total_length = len;
1191    }
1192}
1193
1194/// Simulate a river starting from a source by following the steepest descent gradient
1195pub fn simulate_river(hmap: &Heightmap, start: Vec2, min_height: f32) -> RiverPath {
1196    let mut path = RiverPath::new();
1197    path.source = start;
1198
1199    let mut pos = start;
1200    let mut flow = 1.0f32;
1201    let mut prev_dir = Vec2::ZERO;
1202
1203    path.points.push(pos);
1204    path.widths.push(0.5);
1205    path.depths.push(0.1);
1206    path.flow_rates.push(flow);
1207
1208    let w = hmap.width  as f32;
1209    let h = hmap.height as f32;
1210
1211    let mut visited: HashSet<(i32, i32)> = HashSet::new();
1212
1213    for step in 0..16384usize {
1214        let ux = (pos.x / w).clamp(0.0, 1.0);
1215        let uy = (pos.y / h).clamp(0.0, 1.0);
1216        let current_h = hmap.sample_bilinear(ux, uy);
1217
1218        if current_h <= min_height { break; }
1219
1220        // Find steepest descent direction, sampled at 8 neighbours + 8 slightly further out
1221        let step_size = 0.5f32;
1222        let mut best_dir  = Vec2::ZERO;
1223        let mut best_drop = 0.0f32;
1224
1225        let angles: [f32; 16] = [
1226            0.0, PI/8.0, PI/4.0, 3.0*PI/8.0, PI/2.0, 5.0*PI/8.0, 3.0*PI/4.0, 7.0*PI/8.0,
1227            PI, 9.0*PI/8.0, 5.0*PI/4.0, 11.0*PI/8.0, 3.0*PI/2.0, 13.0*PI/8.0, 7.0*PI/4.0, 15.0*PI/8.0,
1228        ];
1229
1230        for &angle in &angles {
1231            let dir = Vec2::new(angle.cos(), angle.sin());
1232            // Weight toward previous direction (inertia)
1233            let weighted_dir = if prev_dir.length() > 0.01 {
1234                (dir * 0.7 + prev_dir * 0.3).normalize()
1235            } else {
1236                dir
1237            };
1238            let npos = pos + weighted_dir * step_size;
1239            let nu = (npos.x / w).clamp(0.0, 1.0);
1240            let nv = (npos.y / h).clamp(0.0, 1.0);
1241            let nh = hmap.sample_bilinear(nu, nv);
1242            let drop = current_h - nh;
1243            if drop > best_drop {
1244                best_drop = drop;
1245                best_dir  = weighted_dir;
1246            }
1247        }
1248
1249        if best_drop < 0.0001 && step > 10 {
1250            // Flat — try to find any lower neighbor
1251            break;
1252        }
1253
1254        if best_dir.length() < 0.01 { break; }
1255        best_dir = best_dir.normalize();
1256
1257        pos = pos + best_dir * step_size;
1258        prev_dir = best_dir;
1259
1260        // Accumulate flow
1261        flow += 0.005 * best_drop;
1262        let width = (flow * 0.3).clamp(0.2, 20.0);
1263        let depth = (flow * 0.05).clamp(0.05, 5.0);
1264
1265        path.points.push(pos);
1266        path.widths.push(width);
1267        path.depths.push(depth);
1268        path.flow_rates.push(flow);
1269
1270        let cell = (pos.x as i32, pos.y as i32);
1271        if visited.contains(&cell) { break; } // loop detection
1272        visited.insert(cell);
1273
1274        // Check world boundary
1275        if pos.x < 0.5 || pos.y < 0.5 || pos.x > w - 0.5 || pos.y > h - 0.5 {
1276            break;
1277        }
1278    }
1279
1280    path.mouth = pos;
1281    path.compute_total_length();
1282    path
1283}
1284
1285/// Lake filling — flood-fill from a seed point up to a given water level
1286#[derive(Clone, Debug)]
1287pub struct LakeBody {
1288    pub cells:      Vec<(usize, usize)>,
1289    pub water_level: f32,
1290    pub surface_area: f32,
1291    pub volume:      f32,
1292    pub centroid:    Vec2,
1293}
1294
1295pub fn fill_lake(hmap: &Heightmap, seed_x: usize, seed_y: usize, max_water_level: f32) -> LakeBody {
1296    let w = hmap.width;
1297    let h = hmap.height;
1298    let mut visited = vec![false; w * h];
1299    let mut cells   = Vec::new();
1300    let mut queue   = VecDeque::new();
1301
1302    let seed_h = hmap.get(seed_x, seed_y);
1303    let water_level = seed_h.max(max_water_level);
1304
1305    queue.push_back((seed_x, seed_y));
1306    visited[seed_y * w + seed_x] = true;
1307
1308    while let Some((cx, cy)) = queue.pop_front() {
1309        let ch = hmap.get(cx, cy);
1310        if ch <= water_level {
1311            cells.push((cx, cy));
1312            let neighbours = [
1313                (cx.wrapping_sub(1), cy), (cx+1, cy),
1314                (cx, cy.wrapping_sub(1)), (cx, cy+1),
1315            ];
1316            for &(nx, ny) in &neighbours {
1317                if nx < w && ny < h && !visited[ny * w + nx] {
1318                    visited[ny * w + nx] = true;
1319                    queue.push_back((nx, ny));
1320                }
1321            }
1322        }
1323    }
1324
1325    let surface_area = cells.len() as f32;
1326    let mut cx_sum = 0.0f32;
1327    let mut cy_sum = 0.0f32;
1328    let mut volume  = 0.0f32;
1329    for &(x, y) in &cells {
1330        cx_sum += x as f32;
1331        cy_sum += y as f32;
1332        volume += water_level - hmap.get(x, y);
1333    }
1334    let count = cells.len() as f32;
1335    let centroid = if count > 0.0 {
1336        Vec2::new(cx_sum / count, cy_sum / count)
1337    } else {
1338        Vec2::new(seed_x as f32, seed_y as f32)
1339    };
1340
1341    LakeBody { cells, water_level, surface_area, volume, centroid }
1342}
1343
1344/// Ocean shore generation — scan for coastline cells (land/water boundary)
1345#[derive(Clone, Debug)]
1346pub struct OceanShore {
1347    pub shore_cells: Vec<(usize, usize)>,
1348    pub sea_level:   f32,
1349    pub beach_width: f32,
1350}
1351
1352pub fn generate_ocean_shore(hmap: &Heightmap, sea_level: f32, beach_width: f32) -> OceanShore {
1353    let w = hmap.width;
1354    let h = hmap.height;
1355    let mut shore_cells = Vec::new();
1356
1357    for y in 1..h-1 {
1358        for x in 1..w-1 {
1359            let ch = hmap.get(x, y);
1360            if ch <= sea_level { continue; } // underwater
1361            // check if any neighbour is underwater
1362            let neighbours = [(x+1,y),(x-1,y),(x,y+1),(x,y-1)];
1363            let has_water_nb = neighbours.iter().any(|&(nx, ny)| {
1364                nx < w && ny < h && hmap.get(nx, ny) <= sea_level
1365            });
1366            if has_water_nb {
1367                shore_cells.push((x, y));
1368            }
1369        }
1370    }
1371
1372    OceanShore { shore_cells, sea_level, beach_width }
1373}
1374
1375/// Check if a point is within beach_width of the shore
1376pub fn is_beach(shore: &OceanShore, hmap: &Heightmap, x: usize, y: usize) -> bool {
1377    let h_val = hmap.get(x, y);
1378    h_val > shore.sea_level && h_val < shore.sea_level + shore.beach_width
1379}
1380
1381// ============================================================
1382//  FOLIAGE PLACEMENT — POISSON DISK SAMPLING (BRIDSON ALGORITHM)
1383// ============================================================
1384
1385#[derive(Clone, Debug)]
1386pub struct FoliageInstance {
1387    pub position:   Vec3,
1388    pub rotation:   Quat,
1389    pub scale:      Vec3,
1390    pub asset_id:   u32,
1391    pub biome_id:   u8,
1392    pub lod_factor: f32,
1393}
1394
1395#[derive(Clone, Debug)]
1396pub struct FoliagePlacementParams {
1397    pub min_radius:    f32,   // minimum distance between instances
1398    pub max_instances: usize,
1399    pub max_slope_rad: f32,   // max surface slope (radians)
1400    pub min_altitude:  f32,   // normalised altitude 0..1
1401    pub max_altitude:  f32,
1402    pub density_scale: f32,
1403    pub use_density_map: bool,
1404    pub random_rotation: bool,
1405    pub scale_variance: f32,
1406    pub base_scale:    Vec3,
1407    pub asset_id:      u32,
1408    pub biome_id:      u8,
1409    pub align_to_normal: bool,
1410}
1411
1412impl Default for FoliagePlacementParams {
1413    fn default() -> Self {
1414        FoliagePlacementParams {
1415            min_radius: 2.0,
1416            max_instances: 100_000,
1417            max_slope_rad: 0.7,
1418            min_altitude: 0.05,
1419            max_altitude: 0.75,
1420            density_scale: 1.0,
1421            use_density_map: false,
1422            random_rotation: true,
1423            scale_variance: 0.25,
1424            base_scale: Vec3::ONE,
1425            asset_id: 0,
1426            biome_id: 0,
1427            align_to_normal: false,
1428        }
1429    }
1430}
1431
1432/// Bridson's fast Poisson disk sampling in 2D, returns list of (x, y) sample positions
1433pub fn poisson_disk_2d(
1434    width: f32,
1435    height: f32,
1436    min_dist: f32,
1437    max_attempts: usize,
1438    seed: u64,
1439) -> Vec<Vec2> {
1440    let cell_size = min_dist / (2.0_f32).sqrt();
1441    let grid_w    = (width  / cell_size).ceil() as usize + 1;
1442    let grid_h    = (height / cell_size).ceil() as usize + 1;
1443
1444    let mut grid: Vec<Option<Vec2>> = vec![None; grid_w * grid_h];
1445    let mut active_list: Vec<Vec2>  = Vec::new();
1446    let mut samples:     Vec<Vec2>  = Vec::new();
1447    let mut rng = LcgRng::new(seed ^ 0xF00D);
1448
1449    let grid_idx = |p: Vec2| -> usize {
1450        let gx = (p.x / cell_size) as usize;
1451        let gy = (p.y / cell_size) as usize;
1452        gy * grid_w + gx
1453    };
1454
1455    // Initial sample
1456    let first = Vec2::new(
1457        rng.next_f32() * width,
1458        rng.next_f32() * height,
1459    );
1460    active_list.push(first);
1461    samples.push(first);
1462    grid[grid_idx(first)] = Some(first);
1463
1464    while !active_list.is_empty() {
1465        let rand_idx = (rng.next_f32() * active_list.len() as f32) as usize;
1466        let rand_idx = rand_idx.min(active_list.len() - 1);
1467        let base = active_list[rand_idx];
1468
1469        let mut found = false;
1470        for _ in 0..max_attempts {
1471            // Random point in annulus [r, 2r] around base
1472            let angle = rng.next_f32() * TWO_PI;
1473            let rad   = min_dist + rng.next_f32() * min_dist;
1474            let candidate = Vec2::new(
1475                base.x + angle.cos() * rad,
1476                base.y + angle.sin() * rad,
1477            );
1478
1479            if candidate.x < 0.0 || candidate.x >= width
1480            || candidate.y < 0.0 || candidate.y >= height {
1481                continue;
1482            }
1483
1484            // Check neighbours in grid
1485            let gx0 = ((candidate.x - min_dist) / cell_size).floor() as i32;
1486            let gy0 = ((candidate.y - min_dist) / cell_size).floor() as i32;
1487            let gx1 = ((candidate.x + min_dist) / cell_size).ceil()  as i32;
1488            let gy1 = ((candidate.y + min_dist) / cell_size).ceil()  as i32;
1489
1490            let gx0u = gx0.max(0) as usize;
1491            let gy0u = gy0.max(0) as usize;
1492            let gx1u = (gx1 as usize).min(grid_w - 1);
1493            let gy1u = (gy1 as usize).min(grid_h - 1);
1494
1495            let mut ok = true;
1496            'outer: for gy in gy0u..=gy1u {
1497                for gx in gx0u..=gx1u {
1498                    if let Some(p) = grid[gy * grid_w + gx] {
1499                        if (p - candidate).length() < min_dist {
1500                            ok = false;
1501                            break 'outer;
1502                        }
1503                    }
1504                }
1505            }
1506
1507            if ok {
1508                active_list.push(candidate);
1509                samples.push(candidate);
1510                grid[grid_idx(candidate)] = Some(candidate);
1511                found = true;
1512                break;
1513            }
1514        }
1515
1516        if !found {
1517            active_list.swap_remove(rand_idx);
1518        }
1519    }
1520
1521    samples
1522}
1523
1524pub fn place_foliage(
1525    hmap:        &Heightmap,
1526    density_map: Option<&Vec<f32>>,
1527    params:      &FoliagePlacementParams,
1528    seed:        u64,
1529    cell_size:   f32,
1530) -> Vec<FoliageInstance> {
1531    let w = hmap.width  as f32;
1532    let h = hmap.height as f32;
1533
1534    let candidates = poisson_disk_2d(w, h, params.min_radius, 30, seed);
1535    let mut rng    = LcgRng::new(seed ^ 0xFACE);
1536    let mut result = Vec::new();
1537
1538    for pos2d in &candidates {
1539        if result.len() >= params.max_instances { break; }
1540
1541        let ux = (pos2d.x / w).clamp(0.0, 1.0);
1542        let uy = (pos2d.y / h).clamp(0.0, 1.0);
1543        let altitude = hmap.sample_bilinear(ux, uy);
1544
1545        if altitude < params.min_altitude || altitude > params.max_altitude { continue; }
1546
1547        let xi = pos2d.x as usize;
1548        let yi = pos2d.y as usize;
1549        let slope = if xi < hmap.width && yi < hmap.height {
1550            hmap.slope_at(xi.min(hmap.width-1), yi.min(hmap.height-1), cell_size)
1551        } else { 0.0 };
1552
1553        if slope > params.max_slope_rad { continue; }
1554
1555        // Check density map
1556        if params.use_density_map {
1557            if let Some(dmap) = density_map {
1558                let di = (uy * (hmap.height - 1) as f32) as usize * hmap.width
1559                       + (ux * (hmap.width  - 1) as f32) as usize;
1560                let di = di.min(dmap.len() - 1);
1561                let density = dmap[di] * params.density_scale;
1562                if rng.next_f32() > density { continue; }
1563            }
1564        }
1565
1566        // Compute rotation
1567        let rotation = if params.align_to_normal {
1568            let xi_c = xi.min(hmap.width  - 1);
1569            let yi_c = yi.min(hmap.height - 1);
1570            let normal = hmap.normal_at(xi_c, yi_c, cell_size);
1571            let up = Vec3::Y;
1572            let axis  = up.cross(normal);
1573            let angle  = up.dot(normal).acos();
1574            if axis.length() > 1e-6 {
1575                Quat::from_axis_angle(axis.normalize(), angle)
1576            } else {
1577                Quat::IDENTITY
1578            }
1579        } else if params.random_rotation {
1580            let angle = rng.next_f32() * TWO_PI;
1581            Quat::from_rotation_y(angle)
1582        } else {
1583            Quat::IDENTITY
1584        };
1585
1586        // Compute scale
1587        let sv = 1.0 + (rng.next_f32() * 2.0 - 1.0) * params.scale_variance;
1588        let scale = params.base_scale * sv;
1589
1590        let world_y = altitude * hmap.max_h;
1591        let position = Vec3::new(pos2d.x * cell_size, world_y, pos2d.y * cell_size);
1592
1593        result.push(FoliageInstance {
1594            position,
1595            rotation,
1596            scale,
1597            asset_id: params.asset_id,
1598            biome_id: params.biome_id,
1599            lod_factor: 1.0,
1600        });
1601    }
1602
1603    result
1604}
1605
1606// ============================================================
1607//  ROAD NETWORK — A* PATHFINDING + CATMULL-ROM SMOOTHING
1608// ============================================================
1609
1610#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1611pub struct GridNode {
1612    pub x: i32,
1613    pub y: i32,
1614}
1615
1616impl GridNode {
1617    pub fn new(x: i32, y: i32) -> Self { GridNode { x, y } }
1618    pub fn to_vec2(&self, cell_size: f32) -> Vec2 {
1619        Vec2::new(self.x as f32 * cell_size, self.y as f32 * cell_size)
1620    }
1621}
1622
1623#[derive(Clone, Debug)]
1624pub struct AStarNode {
1625    pub pos:    GridNode,
1626    pub g_cost: f32,
1627    pub h_cost: f32,
1628    pub parent: Option<GridNode>,
1629}
1630
1631impl AStarNode {
1632    pub fn f_cost(&self) -> f32 { self.g_cost + self.h_cost }
1633}
1634
1635fn astar_heuristic(a: &GridNode, b: &GridNode) -> f32 {
1636    // Octile heuristic — good for 8-directional movement
1637    let dx = (a.x - b.x).abs() as f32;
1638    let dy = (a.y - b.y).abs() as f32;
1639    let (min_d, max_d) = if dx < dy { (dx, dy) } else { (dy, dx) };
1640    max_d + (1.41421356 - 1.0) * min_d
1641}
1642
1643pub struct RoadCostParams {
1644    pub slope_weight:   f32,
1645    pub height_weight:  f32,
1646    pub water_penalty:  f32,
1647    pub sea_level:      f32,
1648}
1649
1650impl Default for RoadCostParams {
1651    fn default() -> Self {
1652        RoadCostParams { slope_weight: 5.0, height_weight: 2.0, water_penalty: 100.0, sea_level: 0.1 }
1653    }
1654}
1655
1656fn road_move_cost(hmap: &Heightmap, from: &GridNode, to: &GridNode, cost_params: &RoadCostParams) -> f32 {
1657    let w = hmap.width  as i32;
1658    let h = hmap.height as i32;
1659    if to.x < 0 || to.y < 0 || to.x >= w || to.y >= h { return f32::MAX; }
1660
1661    let diagonal = from.x != to.x && from.y != to.y;
1662    let base_cost = if diagonal { 1.41421356 } else { 1.0 };
1663
1664    let h_from = hmap.get_clamped(from.x, from.y);
1665    let h_to   = hmap.get_clamped(to.x,   to.y);
1666
1667    // Water avoidance
1668    if h_to <= cost_params.sea_level { return base_cost + cost_params.water_penalty; }
1669
1670    let slope = (h_to - h_from).abs();
1671    let cost  = base_cost
1672              + slope * cost_params.slope_weight
1673              + (h_to - 0.3).abs() * cost_params.height_weight;
1674    cost
1675}
1676
1677/// A* pathfinding on a heightmap grid
1678pub fn astar_path(
1679    hmap:        &Heightmap,
1680    start:       GridNode,
1681    goal:        GridNode,
1682    cost_params: &RoadCostParams,
1683) -> Option<Vec<GridNode>> {
1684    use std::collections::BinaryHeap;
1685    use std::cmp::Ordering;
1686
1687    #[derive(Clone)]
1688    struct Entry { cost: f32, node: GridNode }
1689    impl PartialEq for Entry { fn eq(&self, o: &Self) -> bool { self.cost == o.cost } }
1690    impl Eq for Entry {}
1691    impl PartialOrd for Entry {
1692        fn partial_cmp(&self, o: &Self) -> Option<Ordering> { Some(self.cmp(o)) }
1693    }
1694    impl Ord for Entry {
1695        fn cmp(&self, o: &Self) -> Ordering {
1696            o.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal)
1697        }
1698    }
1699
1700    let mut open_heap:  BinaryHeap<Entry> = BinaryHeap::new();
1701    let mut g_score:    HashMap<(i32,i32), f32> = HashMap::new();
1702    let mut came_from:  HashMap<(i32,i32), GridNode> = HashMap::new();
1703    let mut closed_set: HashSet<(i32,i32)> = HashSet::new();
1704
1705    let start_key = (start.x, start.y);
1706    g_score.insert(start_key, 0.0);
1707    open_heap.push(Entry { cost: astar_heuristic(&start, &goal), node: start.clone() });
1708
1709    let directions: [(i32,i32); 8] = [
1710        (1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)
1711    ];
1712
1713    let mut iterations = 0usize;
1714    const MAX_ITER: usize = 200_000;
1715
1716    while let Some(Entry { node: current, .. }) = open_heap.pop() {
1717        iterations += 1;
1718        if iterations > MAX_ITER { return None; }
1719
1720        let cur_key = (current.x, current.y);
1721
1722        if current.x == goal.x && current.y == goal.y {
1723            // Reconstruct path
1724            let mut path = vec![current.clone()];
1725            let mut cur  = cur_key;
1726            while let Some(parent) = came_from.get(&cur) {
1727                path.push(parent.clone());
1728                cur = (parent.x, parent.y);
1729            }
1730            path.reverse();
1731            return Some(path);
1732        }
1733
1734        if closed_set.contains(&cur_key) { continue; }
1735        closed_set.insert(cur_key);
1736
1737        let cur_g = *g_score.get(&cur_key).unwrap_or(&f32::MAX);
1738
1739        for &(dx, dy) in &directions {
1740            let nb = GridNode::new(current.x + dx, current.y + dy);
1741            let nb_key = (nb.x, nb.y);
1742            if closed_set.contains(&nb_key) { continue; }
1743
1744            let move_c = road_move_cost(hmap, &current, &nb, cost_params);
1745            if move_c >= f32::MAX * 0.5 { continue; }
1746
1747            let tentative_g = cur_g + move_c;
1748            let old_g = *g_score.get(&nb_key).unwrap_or(&f32::MAX);
1749            if tentative_g < old_g {
1750                g_score.insert(nb_key, tentative_g);
1751                came_from.insert(nb_key, current.clone());
1752                let f = tentative_g + astar_heuristic(&nb, &goal);
1753                open_heap.push(Entry { cost: f, node: nb });
1754            }
1755        }
1756    }
1757    None
1758}
1759
1760/// Catmull-Rom spline interpolation between control points
1761pub fn catmull_rom(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
1762    let t2 = t * t;
1763    let t3 = t2 * t;
1764    // Catmull-Rom matrix (alpha = 0.5)
1765    let q = 0.5 * (
1766        (p1 * 2.0)
1767        + (-p0 + p2) * t
1768        + (p0 * 2.0 - p1 * 5.0 + p2 * 4.0 - p3) * t2
1769        + (-p0 + p1 * 3.0 - p2 * 3.0 + p3) * t3
1770    );
1771    q
1772}
1773
1774/// Smooth a road path using Catmull-Rom
1775pub fn smooth_road_path(
1776    raw_nodes: &[GridNode],
1777    cell_size: f32,
1778    samples_per_segment: usize,
1779) -> Vec<Vec2> {
1780    if raw_nodes.len() < 2 { return Vec::new(); }
1781    let pts: Vec<Vec2> = raw_nodes.iter().map(|n| n.to_vec2(cell_size)).collect();
1782    let n = pts.len();
1783    let mut result = Vec::with_capacity(n * samples_per_segment);
1784
1785    for i in 0..n - 1 {
1786        let p0 = if i == 0     { pts[0] + (pts[0] - pts[1]) }       else { pts[i-1] };
1787        let p1 = pts[i];
1788        let p2 = pts[i+1];
1789        let p3 = if i+2 >= n   { pts[n-1] + (pts[n-1] - pts[n-2]) } else { pts[i+2] };
1790
1791        for s in 0..samples_per_segment {
1792            let t = s as f32 / samples_per_segment as f32;
1793            result.push(catmull_rom(p0, p1, p2, p3, t));
1794        }
1795    }
1796    result.push(*pts.last().unwrap());
1797    result
1798}
1799
1800#[derive(Clone, Debug)]
1801pub struct RoadSegment {
1802    pub id:           u32,
1803    pub control_pts:  Vec<Vec2>,
1804    pub smoothed_pts: Vec<Vec2>,
1805    pub width:        f32,
1806    pub road_type:    RoadType,
1807    pub start_node:   u32,
1808    pub end_node:     u32,
1809    pub length:       f32,
1810}
1811
1812#[derive(Clone, Debug, PartialEq, Eq)]
1813pub enum RoadType {
1814    Dirt,
1815    Gravel,
1816    Paved,
1817    Highway,
1818    Trail,
1819}
1820
1821impl RoadSegment {
1822    pub fn compute_length(&mut self) {
1823        let mut len = 0.0f32;
1824        for i in 1..self.smoothed_pts.len() {
1825            len += (self.smoothed_pts[i] - self.smoothed_pts[i-1]).length();
1826        }
1827        self.length = len;
1828    }
1829}
1830
1831#[derive(Clone, Debug)]
1832pub struct RoadNetwork {
1833    pub segments: Vec<RoadSegment>,
1834    pub nodes:    HashMap<u32, Vec2>,
1835    pub next_node_id: u32,
1836    pub next_seg_id:  u32,
1837}
1838
1839impl RoadNetwork {
1840    pub fn new() -> Self {
1841        RoadNetwork { segments: Vec::new(), nodes: HashMap::new(), next_node_id: 0, next_seg_id: 0 }
1842    }
1843
1844    pub fn add_node(&mut self, pos: Vec2) -> u32 {
1845        let id = self.next_node_id;
1846        self.nodes.insert(id, pos);
1847        self.next_node_id += 1;
1848        id
1849    }
1850
1851    pub fn build_road(
1852        &mut self,
1853        hmap:        &Heightmap,
1854        start_world: Vec2,
1855        end_world:   Vec2,
1856        cell_size:   f32,
1857        road_type:   RoadType,
1858        cost_params: &RoadCostParams,
1859    ) -> Option<u32> {
1860        let start_node = GridNode::new(
1861            (start_world.x / cell_size) as i32,
1862            (start_world.y / cell_size) as i32,
1863        );
1864        let end_node = GridNode::new(
1865            (end_world.x / cell_size) as i32,
1866            (end_world.y / cell_size) as i32,
1867        );
1868
1869        let path = astar_path(hmap, start_node, end_node, cost_params)?;
1870        let smoothed = smooth_road_path(&path, cell_size, 8);
1871        let control_pts: Vec<Vec2> = path.iter().map(|n| n.to_vec2(cell_size)).collect();
1872
1873        let width = match road_type {
1874            RoadType::Dirt    => 3.0,
1875            RoadType::Gravel  => 4.5,
1876            RoadType::Paved   => 6.0,
1877            RoadType::Highway => 12.0,
1878            RoadType::Trail   => 1.5,
1879        };
1880
1881        let sid = self.next_seg_id;
1882        self.next_seg_id += 1;
1883
1884        let start_nid = self.add_node(start_world);
1885        let end_nid   = self.add_node(end_world);
1886
1887        let mut seg = RoadSegment {
1888            id: sid,
1889            control_pts,
1890            smoothed_pts: smoothed,
1891            width,
1892            road_type,
1893            start_node: start_nid,
1894            end_node:   end_nid,
1895            length: 0.0,
1896        };
1897        seg.compute_length();
1898        self.segments.push(seg);
1899        Some(sid)
1900    }
1901}
1902
1903// ============================================================
1904//  ATMOSPHERE — RAYLEIGH & MIE SCATTERING
1905// ============================================================
1906
1907#[derive(Clone, Debug)]
1908pub struct AtmosphereParams {
1909    pub rayleigh_scale_height: f64,
1910    pub mie_scale_height:      f64,
1911    pub rayleigh_coeff:        [f64; 3], // per wavelength (R,G,B)
1912    pub mie_coeff:             f64,
1913    pub mie_asymmetry:         f64,      // g factor
1914    pub sun_intensity:         f64,
1915    pub num_view_samples:      usize,
1916    pub num_light_samples:     usize,
1917    pub planet_radius:         f64,      // km
1918    pub atmo_radius:           f64,      // km
1919}
1920
1921impl Default for AtmosphereParams {
1922    fn default() -> Self {
1923        AtmosphereParams {
1924            rayleigh_scale_height: RAYLEIGH_SCALE_HEIGHT,
1925            mie_scale_height:      MIE_SCALE_HEIGHT,
1926            rayleigh_coeff:        [RAYLEIGH_R, RAYLEIGH_G, RAYLEIGH_B],
1927            mie_coeff:             MIE_COEFF,
1928            mie_asymmetry:         MIE_G,
1929            sun_intensity:         20.0,
1930            num_view_samples:      16,
1931            num_light_samples:     8,
1932            planet_radius:         EARTH_RADIUS,
1933            atmo_radius:           ATMO_RADIUS,
1934        }
1935    }
1936}
1937
1938fn ray_sphere_intersection(
1939    ray_origin: [f64; 3],
1940    ray_dir:    [f64; 3],
1941    sphere_radius: f64,
1942) -> Option<(f64, f64)> {
1943    let a = dot3(ray_dir, ray_dir);
1944    let b = 2.0 * dot3(ray_origin, ray_dir);
1945    let c = dot3(ray_origin, ray_origin) - sphere_radius * sphere_radius;
1946    let disc = b * b - 4.0 * a * c;
1947    if disc < 0.0 { return None; }
1948    let sqrt_disc = disc.sqrt();
1949    let t0 = (-b - sqrt_disc) / (2.0 * a);
1950    let t1 = (-b + sqrt_disc) / (2.0 * a);
1951    Some((t0, t1))
1952}
1953
1954#[inline]
1955fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
1956    a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
1957}
1958
1959#[inline]
1960fn normalize3(v: [f64; 3]) -> [f64; 3] {
1961    let len = (v[0]*v[0] + v[1]*v[1] + v[2]*v[2]).sqrt();
1962    if len < 1e-15 { return [0.0, 1.0, 0.0]; }
1963    [v[0]/len, v[1]/len, v[2]/len]
1964}
1965
1966fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]+b[0], a[1]+b[1], a[2]+b[2]] }
1967fn scale3(a: [f64; 3], s: f64) -> [f64; 3] { [a[0]*s, a[1]*s, a[2]*s] }
1968fn mul3_elem(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { [a[0]*b[0], a[1]*b[1], a[2]*b[2]] }
1969fn exp3(a: [f64; 3]) -> [f64; 3] { [a[0].exp(), a[1].exp(), a[2].exp()] }
1970fn neg3(a: [f64; 3]) -> [f64; 3] { [-a[0], -a[1], -a[2]] }
1971
1972/// Rayleigh phase function
1973fn phase_rayleigh(cos_theta: f64) -> f64 {
1974    (3.0 / (16.0 * std::f64::consts::PI)) * (1.0 + cos_theta * cos_theta)
1975}
1976
1977/// Mie phase function (Henyey-Greenstein)
1978fn phase_mie(cos_theta: f64, g: f64) -> f64 {
1979    let g2 = g * g;
1980    (1.0 - g2) / (4.0 * std::f64::consts::PI * (1.0 + g2 - 2.0 * g * cos_theta).powf(1.5))
1981}
1982
1983/// Compute sky color for a given view direction and sun direction (all in km space)
1984/// Returns Vec3 (R, G, B) in linear HDR
1985pub fn compute_sky_color(
1986    view_dir: Vec3,
1987    sun_dir:  Vec3,
1988    params:   &AtmosphereParams,
1989) -> Vec3 {
1990    let planet_r = params.planet_radius;
1991    let atmo_r   = params.atmo_radius;
1992
1993    // Camera is on the surface at altitude 0
1994    let camera_pos = [0.0f64, planet_r + 0.1, 0.0f64]; // 100 m above surface
1995    let vd = [view_dir.x as f64, view_dir.y as f64, view_dir.z as f64];
1996    let vd = normalize3(vd);
1997    let sd = [sun_dir.x as f64, sun_dir.y as f64, sun_dir.z as f64];
1998    let sd = normalize3(sd);
1999
2000    // Ray-atmosphere intersection
2001    let (_, t_max_opt) = match ray_sphere_intersection(camera_pos, vd, atmo_r) {
2002        Some(v) => v,
2003        None => return Vec3::ZERO,
2004    };
2005    let t_max_opt = t_max_opt.max(0.0);
2006
2007    // If ray hits planet, clip
2008    let t_max = if let Some((t0, _)) = ray_sphere_intersection(camera_pos, vd, planet_r + 0.001) {
2009        if t0 > 0.0 { t0 } else { t_max_opt }
2010    } else {
2011        t_max_opt
2012    };
2013
2014    let cos_theta = dot3(vd, sd);
2015    let phase_r   = phase_rayleigh(cos_theta);
2016    let phase_m   = phase_mie(cos_theta, params.mie_asymmetry);
2017
2018    let mut total_rayleigh = [0.0f64; 3];
2019    let mut total_mie      = [0.0f64; 3];
2020
2021    let ns = params.num_view_samples;
2022    let seg_len = t_max / ns as f64;
2023
2024    let mut optical_depth_r = 0.0f64;
2025    let mut optical_depth_m = 0.0f64;
2026
2027    for i in 0..ns {
2028        let t_mid = (i as f64 + 0.5) * seg_len;
2029        let sample_pos = add3(camera_pos, scale3(vd, t_mid));
2030        let sample_r = (dot3(sample_pos, sample_pos)).sqrt();
2031        let height = (sample_r - planet_r).max(0.0);
2032
2033        let hr = (-(height / params.rayleigh_scale_height)).exp();
2034        let hm = (-(height / params.mie_scale_height)).exp();
2035
2036        optical_depth_r += hr * seg_len;
2037        optical_depth_m += hm * seg_len;
2038
2039        // Light ray integration
2040        let (_, t_light) = match ray_sphere_intersection(sample_pos, sd, atmo_r) {
2041            Some(v) => v,
2042            None => continue,
2043        };
2044        let t_light = t_light.max(0.0);
2045        let nl = params.num_light_samples;
2046        let light_seg = t_light / nl as f64;
2047        let mut od_lr = 0.0f64;
2048        let mut od_lm = 0.0f64;
2049        let mut above_planet = true;
2050        for j in 0..nl {
2051            let lt = (j as f64 + 0.5) * light_seg;
2052            let lpos = add3(sample_pos, scale3(sd, lt));
2053            let lr = (dot3(lpos, lpos)).sqrt();
2054            if lr < planet_r { above_planet = false; break; }
2055            let lh = (lr - planet_r).max(0.0);
2056            od_lr += (-(lh / params.rayleigh_scale_height)).exp() * light_seg;
2057            od_lm += (-(lh / params.mie_scale_height)).exp() * light_seg;
2058        }
2059        if !above_planet { continue; }
2060
2061        let tau_r = [
2062            params.rayleigh_coeff[0] * (optical_depth_r + od_lr),
2063            params.rayleigh_coeff[1] * (optical_depth_r + od_lr),
2064            params.rayleigh_coeff[2] * (optical_depth_r + od_lr),
2065        ];
2066        let tau_m_val = 1.1 * params.mie_coeff * (optical_depth_m + od_lm);
2067        let tau_m = [tau_m_val; 3];
2068
2069        let attenuation_r = exp3(neg3(tau_r));
2070        let attenuation_m = exp3(neg3(tau_m));
2071
2072        let contrib_r = scale3(attenuation_r, hr * seg_len);
2073        let contrib_m = scale3(attenuation_m, hm * seg_len);
2074
2075        for k in 0..3 {
2076            total_rayleigh[k] += contrib_r[k] * params.rayleigh_coeff[k];
2077            total_mie[k]      += contrib_m[k] * params.mie_coeff;
2078        }
2079    }
2080
2081    let sun_intensity = params.sun_intensity;
2082    let color = [
2083        sun_intensity * (phase_r * total_rayleigh[0] + phase_m * total_mie[0]),
2084        sun_intensity * (phase_r * total_rayleigh[1] + phase_m * total_mie[1]),
2085        sun_intensity * (phase_r * total_rayleigh[2] + phase_m * total_mie[2]),
2086    ];
2087
2088    Vec3::new(color[0] as f32, color[1] as f32, color[2] as f32)
2089}
2090
2091/// Render a sun disk contribution on top of sky color
2092pub fn sun_disk_color(view_dir: Vec3, sun_dir: Vec3, disk_size: f32, sun_color: Vec3) -> Vec3 {
2093    let cos_angle = view_dir.dot(sun_dir).clamp(-1.0, 1.0);
2094    let angle = cos_angle.acos();
2095    if angle < disk_size {
2096        // Smooth edge using a limb-darkening approximation
2097        let t     = (angle / disk_size).clamp(0.0, 1.0);
2098        let limb  = 1.0 - 0.6 * t.sqrt(); // limb darkening coefficient ~0.6
2099        sun_color * limb
2100    } else {
2101        Vec3::ZERO
2102    }
2103}
2104
2105/// Apply tone mapping (ACES filmic approximation)
2106pub fn aces_tonemap(color: Vec3) -> Vec3 {
2107    let a = 2.51f32;
2108    let b = 0.03f32;
2109    let c = 2.43f32;
2110    let d = 0.59f32;
2111    let e = 0.14f32;
2112    let result = (color * (color * a + Vec3::splat(b)))
2113        / (color * (color * c + Vec3::splat(d)) + Vec3::splat(e));
2114    result.clamp(Vec3::ZERO, Vec3::ONE)
2115}
2116
2117/// Simple Reinhard tone mapping
2118pub fn reinhard_tonemap(color: Vec3) -> Vec3 {
2119    color / (Vec3::ONE + color)
2120}
2121
2122// ============================================================
2123//  DAY / NIGHT — SOLAR MATH
2124// ============================================================
2125
2126/// Compute solar declination for a given day of year (1-365)
2127/// Returns angle in degrees
2128pub fn solar_declination(day_of_year: f64) -> f64 {
2129    // Spencer (1971) formula — accurate to ±0.3°
2130    let b = 360.0 / 365.0 * (day_of_year - 81.0);
2131    let b_rad = b * std::f64::consts::PI / 180.0;
2132    SOLAR_OBLIQUITY * b_rad.sin()
2133}
2134
2135/// Equation of time (minutes) for a given day of year
2136pub fn equation_of_time(day_of_year: f64) -> f64 {
2137    let b = 360.0 / 365.0 * (day_of_year - 81.0);
2138    let b_rad = b * std::f64::consts::PI / 180.0;
2139    9.87 * (2.0 * b_rad).sin() - 7.53 * b_rad.cos() - 1.5 * b_rad.sin()
2140}
2141
2142/// Compute hour angle (degrees) from longitude, solar time, and equation of time
2143pub fn hour_angle(longitude_deg: f64, solar_time_hours: f64) -> f64 {
2144    15.0 * (solar_time_hours - 12.0)
2145}
2146
2147/// Compute solar altitude and azimuth angles (degrees) from observer's position and time
2148/// Returns (altitude_deg, azimuth_deg)
2149pub fn solar_position(
2150    latitude_deg:  f64,
2151    longitude_deg: f64,
2152    day_of_year:   f64,
2153    utc_hour:      f64,
2154) -> (f64, f64) {
2155    let decl = solar_declination(day_of_year) * std::f64::consts::PI / 180.0;
2156    let lat  = latitude_deg                   * std::f64::consts::PI / 180.0;
2157    let eot  = equation_of_time(day_of_year);
2158    let solar_time = utc_hour + longitude_deg / 15.0 + eot / 60.0;
2159    let ha   = hour_angle(longitude_deg, solar_time) * std::f64::consts::PI / 180.0;
2160
2161    let sin_alt = decl.sin() * lat.sin() + decl.cos() * lat.cos() * ha.cos();
2162    let altitude = sin_alt.asin() * 180.0 / std::f64::consts::PI;
2163
2164    let cos_az = (decl.sin() * lat.cos() - decl.cos() * lat.sin() * ha.cos())
2165               / (1.0 - sin_alt * sin_alt).sqrt().max(1e-10);
2166    let azimuth_rad = cos_az.acos();
2167    let azimuth = if ha > 0.0 { 360.0 - azimuth_rad * 180.0 / std::f64::consts::PI }
2168                  else        { azimuth_rad * 180.0 / std::f64::consts::PI };
2169
2170    (altitude, azimuth)
2171}
2172
2173/// Compute sunrise and sunset UTC hours for a given lat/lon and day of year
2174/// Returns (sunrise_utc, sunset_utc) or None if sun never rises/sets
2175pub fn sunrise_sunset(
2176    latitude_deg:  f64,
2177    longitude_deg: f64,
2178    day_of_year:   f64,
2179) -> Option<(f64, f64)> {
2180    let decl = solar_declination(day_of_year) * std::f64::consts::PI / 180.0;
2181    let lat  = latitude_deg                   * std::f64::consts::PI / 180.0;
2182    let eot  = equation_of_time(day_of_year);
2183
2184    // Hour angle at sunrise/sunset (solar altitude = -0.833° accounting for refraction)
2185    let h_arg = (-0.01454 - decl.sin() * lat.sin()) / (decl.cos() * lat.cos());
2186    if h_arg.abs() > 1.0 { return None; } // polar day/night
2187    let ha0_deg = h_arg.acos() * 180.0 / std::f64::consts::PI;
2188
2189    let solar_noon_utc = 12.0 - longitude_deg / 15.0 - eot / 60.0;
2190    let half_day = ha0_deg / 15.0;
2191    Some((solar_noon_utc - half_day, solar_noon_utc + half_day))
2192}
2193
2194/// Convert solar altitude/azimuth to a world-space direction vector
2195pub fn solar_direction_vec(altitude_deg: f64, azimuth_deg: f64) -> Vec3 {
2196    let alt = (altitude_deg as f32) * DEG2RAD;
2197    let az  = (azimuth_deg  as f32) * DEG2RAD;
2198    let cos_alt = alt.cos();
2199    Vec3::new(cos_alt * az.sin(), alt.sin(), cos_alt * az.cos())
2200}
2201
2202#[derive(Clone, Debug)]
2203pub struct SolarState {
2204    pub altitude_deg:  f64,
2205    pub azimuth_deg:   f64,
2206    pub direction:     Vec3,
2207    pub is_day:        bool,
2208    pub sun_color:     Vec3,
2209    pub sun_intensity: f32,
2210    pub sky_color:     Vec3,
2211    pub ambient_color: Vec3,
2212}
2213
2214impl SolarState {
2215    pub fn compute(
2216        latitude:  f64,
2217        longitude: f64,
2218        doy:       f64,
2219        utc_hour:  f64,
2220        atmo:      &AtmosphereParams,
2221    ) -> Self {
2222        let (alt, az) = solar_position(latitude, longitude, doy, utc_hour);
2223        let dir       = solar_direction_vec(alt, az);
2224        let is_day    = alt > -0.833;
2225
2226        // Sun color: yellower near horizon (atmospheric reddening approximation)
2227        let elevation_factor = (alt as f32 / 90.0 + 0.1).clamp(0.0, 1.0);
2228        let sun_color = Vec3::new(
2229            1.0,
2230            0.8 + 0.2 * elevation_factor,
2231            0.5 + 0.5 * elevation_factor,
2232        );
2233        let sun_intensity = if is_day {
2234            ((alt as f32 * DEG2RAD).sin().max(0.0)).sqrt() * 100.0
2235        } else {
2236            0.0
2237        };
2238
2239        let sky_color = if is_day {
2240            let sky = compute_sky_color(dir * -1.0, dir, atmo); // view dir is up
2241            let view_up = Vec3::Y;
2242            let raw = compute_sky_color(view_up, dir, atmo);
2243            aces_tonemap(raw)
2244        } else {
2245            Vec3::new(0.005, 0.005, 0.02)
2246        };
2247
2248        let ambient_color = sky_color * 0.3 + Vec3::new(0.02, 0.02, 0.04);
2249
2250        SolarState { altitude_deg: alt, azimuth_deg: az, direction: dir, is_day, sun_color, sun_intensity, sky_color, ambient_color }
2251    }
2252}
2253
2254// ============================================================
2255//  WEATHER MARKOV CHAIN
2256// ============================================================
2257
2258#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
2259pub enum WeatherState {
2260    Clear    = 0,
2261    Cloudy   = 1,
2262    Rain     = 2,
2263    Storm    = 3,
2264    Snow     = 4,
2265}
2266
2267impl WeatherState {
2268    pub fn from_index(i: usize) -> Self {
2269        match i {
2270            0 => WeatherState::Clear,
2271            1 => WeatherState::Cloudy,
2272            2 => WeatherState::Rain,
2273            3 => WeatherState::Storm,
2274            4 => WeatherState::Snow,
2275            _ => WeatherState::Clear,
2276        }
2277    }
2278    pub fn name(&self) -> &'static str {
2279        match self {
2280            WeatherState::Clear  => "Clear",
2281            WeatherState::Cloudy => "Cloudy",
2282            WeatherState::Rain   => "Rain",
2283            WeatherState::Storm  => "Storm",
2284            WeatherState::Snow   => "Snow",
2285        }
2286    }
2287}
2288
2289/// Transition matrix: rows = current state, cols = next state, values = probability
2290/// Order: Clear, Cloudy, Rain, Storm, Snow
2291pub const WEATHER_TRANSITION_MATRIX: [[f32; 5]; 5] = [
2292    // Clear -> Clear  Cloudy  Rain  Storm  Snow
2293    [0.60,  0.28,   0.08,  0.02,  0.02],
2294    // Cloudy
2295    [0.25,  0.40,   0.25,  0.07,  0.03],
2296    // Rain
2297    [0.10,  0.30,   0.40,  0.15,  0.05],
2298    // Storm
2299    [0.05,  0.20,   0.35,  0.30,  0.10],
2300    // Snow
2301    [0.08,  0.25,   0.10,  0.05,  0.52],
2302];
2303
2304#[derive(Clone, Debug)]
2305pub struct WeatherSnapshot {
2306    pub state:           WeatherState,
2307    pub temperature_c:   f32,
2308    pub wind_speed_ms:   f32,
2309    pub wind_dir_deg:    f32,
2310    pub precipitation_mm: f32,
2311    pub cloud_cover:     f32,   // 0..1
2312    pub visibility_km:   f32,
2313    pub humidity:        f32,
2314    pub pressure_hpa:    f32,
2315    pub fog_density:     f32,
2316    pub lightning_chance: f32,
2317}
2318
2319impl WeatherSnapshot {
2320    pub fn clear(temp: f32) -> Self {
2321        WeatherSnapshot {
2322            state: WeatherState::Clear,
2323            temperature_c: temp,
2324            wind_speed_ms: 2.0,
2325            wind_dir_deg:  0.0,
2326            precipitation_mm: 0.0,
2327            cloud_cover:   0.05,
2328            visibility_km: 50.0,
2329            humidity:      0.30,
2330            pressure_hpa:  1013.25,
2331            fog_density:   0.0,
2332            lightning_chance: 0.0,
2333        }
2334    }
2335}
2336
2337pub struct WeatherSystem {
2338    pub current:    WeatherSnapshot,
2339    pub history:    VecDeque<WeatherSnapshot>,
2340    pub max_history: usize,
2341    pub rng:        LcgRng,
2342    pub base_temp:  f32,
2343    pub season:     f32,  // 0..1, 0 = winter, 0.5 = summer
2344    pub latitude:   f32,
2345}
2346
2347impl WeatherSystem {
2348    pub fn new(seed: u64, base_temp: f32, latitude: f32) -> Self {
2349        WeatherSystem {
2350            current:     WeatherSnapshot::clear(base_temp),
2351            history:     VecDeque::with_capacity(256),
2352            max_history: 256,
2353            rng:         LcgRng::new(seed),
2354            base_temp,
2355            season:      0.25, // spring
2356            latitude,
2357        }
2358    }
2359
2360    /// Advance weather by one time step using Markov chain transitions
2361    pub fn step(&mut self, hours_elapsed: f32) {
2362        // Determine how many steps to apply (one step per simulated hour)
2363        let steps = (hours_elapsed as usize).max(1);
2364
2365        for _ in 0..steps {
2366            let cur_idx = self.current.state as usize;
2367            let row     = &WEATHER_TRANSITION_MATRIX[cur_idx];
2368
2369            // Weighted random transition
2370            let r = self.rng.next_f32();
2371            let mut cum = 0.0f32;
2372            let mut next_state = self.current.state;
2373            for (i, &p) in row.iter().enumerate() {
2374                cum += p;
2375                if r < cum {
2376                    next_state = WeatherState::from_index(i);
2377                    break;
2378                }
2379            }
2380
2381            // Generate consistent meteorological variables for new state
2382            let temp = self.compute_temperature(next_state);
2383            let snap = self.generate_snapshot(next_state, temp);
2384
2385            self.history.push_back(self.current.clone());
2386            if self.history.len() > self.max_history {
2387                self.history.pop_front();
2388            }
2389            self.current = snap;
2390        }
2391    }
2392
2393    fn compute_temperature(&mut self, state: WeatherState) -> f32 {
2394        // Seasonal adjustment: ±15°C from base temperature
2395        let seasonal_bias = (self.season * TWO_PI).sin() * 15.0;
2396        // Latitude cooling: roughly -0.5°C per degree from equator
2397        let lat_bias = -(self.latitude.abs() * 0.5);
2398        let weather_bias = match state {
2399            WeatherState::Clear  =>  2.0,
2400            WeatherState::Cloudy => -1.0,
2401            WeatherState::Rain   => -3.0,
2402            WeatherState::Storm  => -5.0,
2403            WeatherState::Snow   => -8.0,
2404        };
2405        let noise = (self.rng.next_f32() - 0.5) * 4.0;
2406        self.base_temp + seasonal_bias + lat_bias + weather_bias + noise
2407    }
2408
2409    fn generate_snapshot(&mut self, state: WeatherState, temp: f32) -> WeatherSnapshot {
2410        let r = |rng: &mut LcgRng| rng.next_f32();
2411        match state {
2412            WeatherState::Clear => WeatherSnapshot {
2413                state,
2414                temperature_c:    temp,
2415                wind_speed_ms:    r(&mut self.rng) * 5.0,
2416                wind_dir_deg:     r(&mut self.rng) * 360.0,
2417                precipitation_mm: 0.0,
2418                cloud_cover:      r(&mut self.rng) * 0.15,
2419                visibility_km:    40.0 + r(&mut self.rng) * 30.0,
2420                humidity:         0.20 + r(&mut self.rng) * 0.25,
2421                pressure_hpa:     1015.0 + r(&mut self.rng) * 10.0,
2422                fog_density:      0.0,
2423                lightning_chance: 0.0,
2424            },
2425            WeatherState::Cloudy => WeatherSnapshot {
2426                state,
2427                temperature_c:    temp,
2428                wind_speed_ms:    2.0 + r(&mut self.rng) * 8.0,
2429                wind_dir_deg:     r(&mut self.rng) * 360.0,
2430                precipitation_mm: 0.0,
2431                cloud_cover:      0.50 + r(&mut self.rng) * 0.40,
2432                visibility_km:    15.0 + r(&mut self.rng) * 25.0,
2433                humidity:         0.50 + r(&mut self.rng) * 0.25,
2434                pressure_hpa:     1005.0 + r(&mut self.rng) * 10.0,
2435                fog_density:      r(&mut self.rng) * 0.1,
2436                lightning_chance: 0.0,
2437            },
2438            WeatherState::Rain => WeatherSnapshot {
2439                state,
2440                temperature_c:    temp,
2441                wind_speed_ms:    5.0 + r(&mut self.rng) * 10.0,
2442                wind_dir_deg:     r(&mut self.rng) * 360.0,
2443                precipitation_mm: 1.0 + r(&mut self.rng) * 8.0,
2444                cloud_cover:      0.75 + r(&mut self.rng) * 0.25,
2445                visibility_km:    3.0 + r(&mut self.rng) * 7.0,
2446                humidity:         0.75 + r(&mut self.rng) * 0.20,
2447                pressure_hpa:     995.0 + r(&mut self.rng) * 10.0,
2448                fog_density:      0.1 + r(&mut self.rng) * 0.2,
2449                lightning_chance: 0.05,
2450            },
2451            WeatherState::Storm => WeatherSnapshot {
2452                state,
2453                temperature_c:    temp,
2454                wind_speed_ms:    15.0 + r(&mut self.rng) * 30.0,
2455                wind_dir_deg:     r(&mut self.rng) * 360.0,
2456                precipitation_mm: 8.0 + r(&mut self.rng) * 25.0,
2457                cloud_cover:      0.90 + r(&mut self.rng) * 0.10,
2458                visibility_km:    0.2 + r(&mut self.rng) * 2.0,
2459                humidity:         0.90 + r(&mut self.rng) * 0.10,
2460                pressure_hpa:     975.0 + r(&mut self.rng) * 15.0,
2461                fog_density:      0.3 + r(&mut self.rng) * 0.4,
2462                lightning_chance: 0.40 + r(&mut self.rng) * 0.40,
2463            },
2464            WeatherState::Snow => WeatherSnapshot {
2465                state,
2466                temperature_c:    temp.min(-1.0),
2467                wind_speed_ms:    3.0 + r(&mut self.rng) * 15.0,
2468                wind_dir_deg:     r(&mut self.rng) * 360.0,
2469                precipitation_mm: 0.5 + r(&mut self.rng) * 4.0,
2470                cloud_cover:      0.70 + r(&mut self.rng) * 0.30,
2471                visibility_km:    0.5 + r(&mut self.rng) * 4.0,
2472                humidity:         0.60 + r(&mut self.rng) * 0.30,
2473                pressure_hpa:     1000.0 + r(&mut self.rng) * 15.0,
2474                fog_density:      0.15 + r(&mut self.rng) * 0.25,
2475                lightning_chance: 0.02,
2476            },
2477        }
2478    }
2479
2480    /// Linearly interpolate two weather snapshots (for smooth transitions)
2481    pub fn interpolate_snapshots(a: &WeatherSnapshot, b: &WeatherSnapshot, t: f32) -> WeatherSnapshot {
2482        let lerp = |x: f32, y: f32| x + (y - x) * t;
2483        WeatherSnapshot {
2484            state:            if t < 0.5 { a.state } else { b.state },
2485            temperature_c:    lerp(a.temperature_c,    b.temperature_c),
2486            wind_speed_ms:    lerp(a.wind_speed_ms,    b.wind_speed_ms),
2487            wind_dir_deg:     lerp(a.wind_dir_deg,     b.wind_dir_deg),
2488            precipitation_mm: lerp(a.precipitation_mm, b.precipitation_mm),
2489            cloud_cover:      lerp(a.cloud_cover,      b.cloud_cover),
2490            visibility_km:    lerp(a.visibility_km,    b.visibility_km),
2491            humidity:         lerp(a.humidity,         b.humidity),
2492            pressure_hpa:     lerp(a.pressure_hpa,     b.pressure_hpa),
2493            fog_density:      lerp(a.fog_density,      b.fog_density),
2494            lightning_chance: lerp(a.lightning_chance, b.lightning_chance),
2495        }
2496    }
2497
2498    /// Compute wind vector from speed and direction
2499    pub fn wind_vector(&self) -> Vec2 {
2500        let dir_rad = self.current.wind_dir_deg * DEG2RAD;
2501        Vec2::new(dir_rad.cos(), dir_rad.sin()) * self.current.wind_speed_ms
2502    }
2503
2504    /// Check if it's snowing (temperature below 0 with precipitation)
2505    pub fn is_snowing(&self) -> bool {
2506        self.current.state == WeatherState::Snow
2507        || (self.current.temperature_c < 0.0 && self.current.precipitation_mm > 0.5)
2508    }
2509
2510    /// Advance season (0 = winter, 1 = winter again after full year)
2511    pub fn advance_season(&mut self, delta_fraction: f32) {
2512        self.season = (self.season + delta_fraction) % 1.0;
2513    }
2514}
2515
2516// ============================================================
2517//  UNDO / REDO SYSTEM
2518// ============================================================
2519
2520#[derive(Debug)]
2521pub enum EditAction {
2522    SetHeightRegion {
2523        x: usize, y: usize,
2524        width: usize, height: usize,
2525        old_data: Vec<f32>,
2526        new_data: Vec<f32>,
2527    },
2528    PlaceFoliageInstances {
2529        instances: Vec<FoliageInstance>,
2530        indices:   Vec<usize>,
2531    },
2532    RemoveFoliageInstances {
2533        indices:   Vec<usize>,
2534        instances: Vec<FoliageInstance>,
2535    },
2536    AddRoadSegment {
2537        segment_id: u32,
2538        segment:    RoadSegment,
2539    },
2540    RemoveRoadSegment {
2541        segment_id: u32,
2542        segment:    RoadSegment,
2543    },
2544    SetBiomeOverride {
2545        x: usize, y: usize,
2546        old_biome: Option<BiomeId>,
2547        new_biome: Option<BiomeId>,
2548    },
2549    AddWaterBody {
2550        lake: LakeBody,
2551        index: usize,
2552    },
2553    RemoveWaterBody {
2554        lake: LakeBody,
2555        index: usize,
2556    },
2557    CompoundAction {
2558        actions: Vec<EditAction>,
2559        description: String,
2560    },
2561}
2562
2563pub struct UndoRedoStack {
2564    pub undo_stack: Vec<EditAction>,
2565    pub redo_stack: Vec<EditAction>,
2566    pub max_depth:  usize,
2567}
2568
2569impl UndoRedoStack {
2570    pub fn new(max_depth: usize) -> Self {
2571        UndoRedoStack { undo_stack: Vec::new(), redo_stack: Vec::new(), max_depth }
2572    }
2573
2574    pub fn push(&mut self, action: EditAction) {
2575        self.redo_stack.clear();
2576        if self.undo_stack.len() >= self.max_depth {
2577            self.undo_stack.remove(0);
2578        }
2579        self.undo_stack.push(action);
2580    }
2581
2582    pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() }
2583    pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() }
2584
2585    pub fn pop_undo(&mut self) -> Option<EditAction> {
2586        let a = self.undo_stack.pop()?;
2587        Some(a)
2588    }
2589
2590    pub fn push_redo(&mut self, action: EditAction) {
2591        self.redo_stack.push(action);
2592    }
2593
2594    pub fn pop_redo(&mut self) -> Option<EditAction> {
2595        self.redo_stack.pop()
2596    }
2597}
2598
2599// ============================================================
2600//  SELECTION SYSTEM
2601// ============================================================
2602
2603#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2604pub enum SelectionItem {
2605    TerrainCell(usize, usize),
2606    FoliageInstance(usize),
2607    RoadSegment(u32),
2608    WaterBody(usize),
2609    RiverPath(usize),
2610    BiomeZone(BiomeId),
2611}
2612
2613#[derive(Clone, Debug)]
2614pub struct SelectionState {
2615    pub items:        HashSet<SelectionItem>,
2616    pub pivot:        Option<Vec3>,
2617    pub aabb_min:     Vec3,
2618    pub aabb_max:     Vec3,
2619    pub mode:         SelectionMode,
2620}
2621
2622#[derive(Clone, Debug, PartialEq, Eq)]
2623pub enum SelectionMode {
2624    Single,
2625    Multi,
2626    Box,
2627    Paint,
2628}
2629
2630impl SelectionState {
2631    pub fn new() -> Self {
2632        SelectionState {
2633            items:    HashSet::new(),
2634            pivot:    None,
2635            aabb_min: Vec3::splat(f32::MAX),
2636            aabb_max: Vec3::splat(f32::MIN),
2637            mode:     SelectionMode::Single,
2638        }
2639    }
2640
2641    pub fn select(&mut self, item: SelectionItem) {
2642        if self.mode == SelectionMode::Single { self.items.clear(); }
2643        self.items.insert(item);
2644    }
2645
2646    pub fn deselect(&mut self, item: &SelectionItem) {
2647        self.items.remove(item);
2648    }
2649
2650    pub fn toggle(&mut self, item: SelectionItem) {
2651        if self.items.contains(&item) { self.items.remove(&item); }
2652        else { self.items.insert(item); }
2653    }
2654
2655    pub fn clear(&mut self) {
2656        self.items.clear();
2657        self.pivot = None;
2658    }
2659
2660    pub fn is_empty(&self) -> bool { self.items.is_empty() }
2661    pub fn len(&self) -> usize { self.items.len() }
2662
2663    /// Box selection — add all terrain cells within a 2D bounding rectangle
2664    pub fn box_select_terrain(&mut self, x0: usize, y0: usize, x1: usize, y1: usize) {
2665        let (lx, rx) = if x0 < x1 { (x0, x1) } else { (x1, x0) };
2666        let (ly, ry) = if y0 < y1 { (y0, y1) } else { (y1, y0) };
2667        for y in ly..=ry {
2668            for x in lx..=rx {
2669                self.items.insert(SelectionItem::TerrainCell(x, y));
2670            }
2671        }
2672    }
2673
2674    pub fn count_terrain_cells(&self) -> usize {
2675        self.items.iter().filter(|i| matches!(i, SelectionItem::TerrainCell(..)) ).count()
2676    }
2677}
2678
2679// ============================================================
2680//  SERIALIZATION HELPERS
2681// ============================================================
2682
2683#[derive(Clone, Debug)]
2684pub struct SerializedWorld {
2685    pub version:    u32,
2686    pub width:      usize,
2687    pub height:     usize,
2688    pub cell_size:  f32,
2689    pub heightmap:  Vec<f32>,
2690    pub biome_map:  Vec<u8>,
2691    pub rivers:     Vec<SerializedRiver>,
2692    pub lakes:      Vec<SerializedLake>,
2693    pub roads:      Vec<SerializedRoad>,
2694    pub foliage:    Vec<SerializedFoliage>,
2695    pub sea_level:  f32,
2696    pub world_name: String,
2697    pub metadata:   HashMap<String, String>,
2698}
2699
2700#[derive(Clone, Debug)]
2701pub struct SerializedRiver {
2702    pub points: Vec<[f32; 2]>,
2703    pub widths: Vec<f32>,
2704    pub depths: Vec<f32>,
2705}
2706
2707#[derive(Clone, Debug)]
2708pub struct SerializedLake {
2709    pub water_level: f32,
2710    pub centroid:    [f32; 2],
2711    pub volume:      f32,
2712    pub surface_area: f32,
2713}
2714
2715#[derive(Clone, Debug)]
2716pub struct SerializedRoad {
2717    pub id:        u32,
2718    pub road_type: u8,
2719    pub width:     f32,
2720    pub points:    Vec<[f32; 2]>,
2721    pub length:    f32,
2722}
2723
2724#[derive(Clone, Debug)]
2725pub struct SerializedFoliage {
2726    pub asset_id:  u32,
2727    pub biome_id:  u8,
2728    pub position:  [f32; 3],
2729    pub rotation:  [f32; 4],
2730    pub scale:     [f32; 3],
2731}
2732
2733impl SerializedWorld {
2734    pub fn from_editor(editor: &WorldEditor) -> Self {
2735        let heightmap = editor.heightmap.data.clone();
2736        let w = editor.heightmap.width;
2737        let h = editor.heightmap.height;
2738
2739        let mut biome_map = vec![0u8; w * h];
2740        for y in 0..h {
2741            for x in 0..w {
2742                let temp     = editor.temperature_map[y * w + x];
2743                let humidity = editor.humidity_map[y * w + x];
2744                let altitude = editor.heightmap.get(x, y);
2745                let biome    = BiomeDescriptor::classify_point(temp, humidity, altitude);
2746                biome_map[y * w + x] = biome as u8;
2747            }
2748        }
2749
2750        let rivers: Vec<SerializedRiver> = editor.rivers.iter().map(|r| {
2751            SerializedRiver {
2752                points: r.points.iter().map(|p| [p.x, p.y]).collect(),
2753                widths: r.widths.clone(),
2754                depths: r.depths.clone(),
2755            }
2756        }).collect();
2757
2758        let lakes: Vec<SerializedLake> = editor.lakes.iter().map(|l| {
2759            SerializedLake {
2760                water_level:  l.water_level,
2761                centroid:     [l.centroid.x, l.centroid.y],
2762                volume:       l.volume,
2763                surface_area: l.surface_area,
2764            }
2765        }).collect();
2766
2767        let roads: Vec<SerializedRoad> = editor.road_network.segments.iter().map(|seg| {
2768            SerializedRoad {
2769                id:        seg.id,
2770                road_type: seg.road_type.clone() as u8,
2771                width:     seg.width,
2772                points:    seg.smoothed_pts.iter().map(|p| [p.x, p.y]).collect(),
2773                length:    seg.length,
2774            }
2775        }).collect();
2776
2777        let foliage: Vec<SerializedFoliage> = editor.foliage.iter().map(|fi| {
2778            SerializedFoliage {
2779                asset_id: fi.asset_id,
2780                biome_id: fi.biome_id,
2781                position: [fi.position.x, fi.position.y, fi.position.z],
2782                rotation: [fi.rotation.x, fi.rotation.y, fi.rotation.z, fi.rotation.w],
2783                scale:    [fi.scale.x, fi.scale.y, fi.scale.z],
2784            }
2785        }).collect();
2786
2787        SerializedWorld {
2788            version: 1,
2789            width: w,
2790            height: h,
2791            cell_size: editor.cell_size,
2792            heightmap,
2793            biome_map,
2794            rivers,
2795            lakes,
2796            roads,
2797            foliage,
2798            sea_level: editor.sea_level,
2799            world_name: editor.world_name.clone(),
2800            metadata: editor.metadata.clone(),
2801        }
2802    }
2803
2804    /// Serialize to bytes (simple binary format)
2805    pub fn to_bytes(&self) -> Vec<u8> {
2806        let mut buf = Vec::new();
2807        // Header
2808        buf.extend_from_slice(b"WRLD");
2809        push_u32(&mut buf, self.version);
2810        push_u32(&mut buf, self.width  as u32);
2811        push_u32(&mut buf, self.height as u32);
2812        push_f32(&mut buf, self.cell_size);
2813        push_f32(&mut buf, self.sea_level);
2814
2815        // Heightmap
2816        push_u32(&mut buf, self.heightmap.len() as u32);
2817        for &v in &self.heightmap { push_f32(&mut buf, v); }
2818
2819        // Biome map
2820        push_u32(&mut buf, self.biome_map.len() as u32);
2821        buf.extend_from_slice(&self.biome_map);
2822
2823        // Name
2824        let name_bytes = self.world_name.as_bytes();
2825        push_u32(&mut buf, name_bytes.len() as u32);
2826        buf.extend_from_slice(name_bytes);
2827
2828        // Rivers
2829        push_u32(&mut buf, self.rivers.len() as u32);
2830        for river in &self.rivers {
2831            push_u32(&mut buf, river.points.len() as u32);
2832            for &[px, py] in &river.points { push_f32(&mut buf, px); push_f32(&mut buf, py); }
2833            for &w in &river.widths { push_f32(&mut buf, w); }
2834            for &d in &river.depths { push_f32(&mut buf, d); }
2835        }
2836
2837        // Lakes
2838        push_u32(&mut buf, self.lakes.len() as u32);
2839        for lake in &self.lakes {
2840            push_f32(&mut buf, lake.water_level);
2841            push_f32(&mut buf, lake.centroid[0]);
2842            push_f32(&mut buf, lake.centroid[1]);
2843            push_f32(&mut buf, lake.volume);
2844            push_f32(&mut buf, lake.surface_area);
2845        }
2846
2847        // Roads
2848        push_u32(&mut buf, self.roads.len() as u32);
2849        for road in &self.roads {
2850            push_u32(&mut buf, road.id);
2851            buf.push(road.road_type);
2852            push_f32(&mut buf, road.width);
2853            push_f32(&mut buf, road.length);
2854            push_u32(&mut buf, road.points.len() as u32);
2855            for &[px, py] in &road.points { push_f32(&mut buf, px); push_f32(&mut buf, py); }
2856        }
2857
2858        // Foliage
2859        push_u32(&mut buf, self.foliage.len() as u32);
2860        for fi in &self.foliage {
2861            push_u32(&mut buf, fi.asset_id);
2862            buf.push(fi.biome_id);
2863            for &v in &fi.position { push_f32(&mut buf, v); }
2864            for &v in &fi.rotation { push_f32(&mut buf, v); }
2865            for &v in &fi.scale    { push_f32(&mut buf, v); }
2866        }
2867
2868        buf
2869    }
2870
2871    /// Deserialize from bytes
2872    pub fn from_bytes(data: &[u8]) -> Option<Self> {
2873        let mut cursor = 0usize;
2874
2875        if data.len() < 4 { return None; }
2876        if &data[0..4] != b"WRLD" { return None; }
2877        cursor += 4;
2878
2879        let version  = read_u32(data, &mut cursor)?;
2880        let width    = read_u32(data, &mut cursor)? as usize;
2881        let height   = read_u32(data, &mut cursor)? as usize;
2882        let cell_size = read_f32(data, &mut cursor)?;
2883        let sea_level = read_f32(data, &mut cursor)?;
2884
2885        let hmap_len = read_u32(data, &mut cursor)? as usize;
2886        let mut heightmap = Vec::with_capacity(hmap_len);
2887        for _ in 0..hmap_len {
2888            heightmap.push(read_f32(data, &mut cursor)?);
2889        }
2890
2891        let biome_len = read_u32(data, &mut cursor)? as usize;
2892        if cursor + biome_len > data.len() { return None; }
2893        let biome_map = data[cursor..cursor + biome_len].to_vec();
2894        cursor += biome_len;
2895
2896        let name_len = read_u32(data, &mut cursor)? as usize;
2897        if cursor + name_len > data.len() { return None; }
2898        let world_name = String::from_utf8(data[cursor..cursor + name_len].to_vec()).ok()?;
2899        cursor += name_len;
2900
2901        // Simplified: skip remaining for brevity in deserialization
2902        Some(SerializedWorld {
2903            version,
2904            width,
2905            height,
2906            cell_size,
2907            heightmap,
2908            biome_map,
2909            rivers: Vec::new(),
2910            lakes: Vec::new(),
2911            roads: Vec::new(),
2912            foliage: Vec::new(),
2913            sea_level,
2914            world_name,
2915            metadata: HashMap::new(),
2916        })
2917    }
2918}
2919
2920fn push_u32(buf: &mut Vec<u8>, v: u32) {
2921    buf.extend_from_slice(&v.to_le_bytes());
2922}
2923fn push_f32(buf: &mut Vec<u8>, v: f32) {
2924    buf.extend_from_slice(&v.to_bits().to_le_bytes());
2925}
2926fn read_u32(data: &[u8], cursor: &mut usize) -> Option<u32> {
2927    if *cursor + 4 > data.len() { return None; }
2928    let v = u32::from_le_bytes(data[*cursor..*cursor+4].try_into().ok()?);
2929    *cursor += 4;
2930    Some(v)
2931}
2932fn read_f32(data: &[u8], cursor: &mut usize) -> Option<f32> {
2933    let bits = read_u32(data, cursor)?;
2934    Some(f32::from_bits(bits))
2935}
2936
2937// ============================================================
2938//  EDITOR TOOL MODES
2939// ============================================================
2940
2941#[derive(Clone, Debug, PartialEq, Eq)]
2942pub enum EditorTool {
2943    Select,
2944    TerrainRaise,
2945    TerrainLower,
2946    TerrainSmooth,
2947    TerrainFlatten,
2948    TerrainPaint,
2949    TerrainErode,
2950    FoliagePaint,
2951    FoliageErase,
2952    RoadDraw,
2953    WaterPaint,
2954    BiomePaint,
2955    MeasureTool,
2956    ViewOnly,
2957}
2958
2959#[derive(Clone, Debug)]
2960pub struct BrushSettings {
2961    pub radius:    f32,
2962    pub strength:  f32,
2963    pub falloff:   BrushFalloff,
2964    pub scatter:   f32,
2965}
2966
2967#[derive(Clone, Debug, PartialEq)]
2968pub enum BrushFalloff {
2969    Linear,
2970    Smooth,
2971    Constant,
2972    Spike,
2973}
2974
2975impl BrushSettings {
2976    pub fn weight_at_radius(&self, dist: f32) -> f32 {
2977        let t = (dist / self.radius).clamp(0.0, 1.0);
2978        match self.falloff {
2979            BrushFalloff::Linear   => (1.0 - t) * self.strength,
2980            BrushFalloff::Smooth   => { let s = 1.0 - t; s * s * (3.0 - 2.0 * s) * self.strength }
2981            BrushFalloff::Constant => self.strength,
2982            BrushFalloff::Spike    => (1.0 - t * t) * self.strength,
2983        }
2984    }
2985}
2986
2987// ============================================================
2988//  TERRAIN OPERATIONS (editor-level)
2989// ============================================================
2990
2991/// Apply a brush raise/lower operation to the heightmap
2992pub fn terrain_brush_raise(
2993    hmap:    &mut Heightmap,
2994    cx:      f32,
2995    cy:      f32,
2996    brush:   &BrushSettings,
2997    delta:   f32,
2998) -> EditAction {
2999    let r  = brush.radius.ceil() as i32;
3000    let cx_i = cx as i32;
3001    let cy_i = cy as i32;
3002
3003    let x0 = (cx_i - r).max(0) as usize;
3004    let y0 = (cy_i - r).max(0) as usize;
3005    let x1 = (cx_i + r).min(hmap.width  as i32 - 1) as usize;
3006    let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3007
3008    let width  = x1 - x0 + 1;
3009    let height = y1 - y0 + 1;
3010
3011    let mut old_data = Vec::with_capacity(width * height);
3012    for y in y0..=y1 {
3013        for x in x0..=x1 {
3014            old_data.push(hmap.get(x, y));
3015        }
3016    }
3017
3018    for y in y0..=y1 {
3019        for x in x0..=x1 {
3020            let dx = x as f32 - cx;
3021            let dy = y as f32 - cy;
3022            let dist = (dx*dx + dy*dy).sqrt();
3023            if dist <= brush.radius {
3024                let w   = brush.weight_at_radius(dist);
3025                let old = hmap.get(x, y);
3026                hmap.set(x, y, (old + delta * w).clamp(0.0, 1.0));
3027            }
3028        }
3029    }
3030
3031    let mut new_data = Vec::with_capacity(width * height);
3032    for y in y0..=y1 {
3033        for x in x0..=x1 {
3034            new_data.push(hmap.get(x, y));
3035        }
3036    }
3037
3038    EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3039}
3040
3041/// Smooth terrain in brush region (box filter)
3042pub fn terrain_brush_smooth(
3043    hmap:  &mut Heightmap,
3044    cx:    f32,
3045    cy:    f32,
3046    brush: &BrushSettings,
3047    iterations: usize,
3048) -> EditAction {
3049    let r    = brush.radius.ceil() as i32;
3050    let cx_i = cx as i32;
3051    let cy_i = cy as i32;
3052
3053    let x0 = (cx_i - r).max(0) as usize;
3054    let y0 = (cy_i - r).max(0) as usize;
3055    let x1 = (cx_i + r).min(hmap.width  as i32 - 1) as usize;
3056    let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3057
3058    let width  = x1 - x0 + 1;
3059    let height_r = y1 - y0 + 1;
3060
3061    let mut old_data = Vec::with_capacity(width * height_r);
3062    for y in y0..=y1 {
3063        for x in x0..=x1 {
3064            old_data.push(hmap.get(x, y));
3065        }
3066    }
3067
3068    for _iter in 0..iterations {
3069        let copy = hmap.data.clone();
3070        for y in y0..=y1 {
3071            for x in x0..=x1 {
3072                let dx = x as f32 - cx;
3073                let dy = y as f32 - cy;
3074                let dist = (dx*dx + dy*dy).sqrt();
3075                if dist > brush.radius { continue; }
3076                let w = brush.weight_at_radius(dist);
3077
3078                let xi = x as i32;
3079                let yi = y as i32;
3080                let sum = copy[hmap.index(x, y)]
3081                        + hmap.get_clamped(xi-1, yi)
3082                        + hmap.get_clamped(xi+1, yi)
3083                        + hmap.get_clamped(xi, yi-1)
3084                        + hmap.get_clamped(xi, yi+1);
3085                let avg = sum / 5.0;
3086                let old = copy[hmap.index(x, y)];
3087                hmap.set(x, y, old + (avg - old) * w);
3088            }
3089        }
3090    }
3091
3092    let mut new_data = Vec::with_capacity(width * height_r);
3093    for y in y0..=y1 {
3094        for x in x0..=x1 {
3095            new_data.push(hmap.get(x, y));
3096        }
3097    }
3098
3099    EditAction::SetHeightRegion { x: x0, y: y0, width, height: height_r, old_data, new_data }
3100}
3101
3102/// Flatten terrain toward a target height
3103pub fn terrain_brush_flatten(
3104    hmap:          &mut Heightmap,
3105    cx:            f32,
3106    cy:            f32,
3107    brush:         &BrushSettings,
3108    target_height: f32,
3109) -> EditAction {
3110    let r    = brush.radius.ceil() as i32;
3111    let cx_i = cx as i32;
3112    let cy_i = cy as i32;
3113
3114    let x0 = (cx_i - r).max(0) as usize;
3115    let y0 = (cy_i - r).max(0) as usize;
3116    let x1 = (cx_i + r).min(hmap.width  as i32 - 1) as usize;
3117    let y1 = (cy_i + r).min(hmap.height as i32 - 1) as usize;
3118
3119    let width  = x1 - x0 + 1;
3120    let height = y1 - y0 + 1;
3121
3122    let mut old_data = Vec::with_capacity(width * height);
3123    for y in y0..=y1 { for x in x0..=x1 { old_data.push(hmap.get(x, y)); } }
3124
3125    for y in y0..=y1 {
3126        for x in x0..=x1 {
3127            let dx = x as f32 - cx;
3128            let dy = y as f32 - cy;
3129            let dist = (dx*dx + dy*dy).sqrt();
3130            if dist > brush.radius { continue; }
3131            let w   = brush.weight_at_radius(dist);
3132            let old = hmap.get(x, y);
3133            hmap.set(x, y, old + (target_height - old) * w);
3134        }
3135    }
3136
3137    let mut new_data = Vec::with_capacity(width * height);
3138    for y in y0..=y1 { for x in x0..=x1 { new_data.push(hmap.get(x, y)); } }
3139
3140    EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3141}
3142
3143/// Apply stamp (add a precomputed height kernel to a region)
3144pub fn terrain_stamp(
3145    hmap:    &mut Heightmap,
3146    cx:      f32,
3147    cy:      f32,
3148    stamp:   &[f32],
3149    sw:      usize,
3150    sh:      usize,
3151    scale:   f32,
3152) -> EditAction {
3153    let x0 = ((cx - sw as f32 * 0.5) as i32).max(0) as usize;
3154    let y0 = ((cy - sh as f32 * 0.5) as i32).max(0) as usize;
3155    let x1 = (x0 + sw).min(hmap.width);
3156    let y1 = (y0 + sh).min(hmap.height);
3157
3158    let width  = x1 - x0;
3159    let height = y1 - y0;
3160
3161    let mut old_data = Vec::with_capacity(width * height);
3162    for y in y0..y1 { for x in x0..x1 { old_data.push(hmap.get(x, y)); } }
3163
3164    for y in y0..y1 {
3165        for x in x0..x1 {
3166            let si = (y - y0) * sw + (x - x0);
3167            if si < stamp.len() {
3168                let old = hmap.get(x, y);
3169                hmap.set(x, y, (old + stamp[si] * scale).clamp(0.0, 1.0));
3170            }
3171        }
3172    }
3173
3174    let mut new_data = Vec::with_capacity(width * height);
3175    for y in y0..y1 { for x in x0..x1 { new_data.push(hmap.get(x, y)); } }
3176
3177    EditAction::SetHeightRegion { x: x0, y: y0, width, height, old_data, new_data }
3178}
3179
3180// ============================================================
3181//  TEMPERATURE & HUMIDITY MAP GENERATION
3182// ============================================================
3183
3184pub fn generate_temperature_map(
3185    hmap:        &Heightmap,
3186    base_temp:   f32,
3187    latitude:    f32,
3188    noise_scale: f32,
3189    seed:        u64,
3190) -> Vec<f32> {
3191    let w = hmap.width;
3192    let h = hmap.height;
3193    let mut temp_map = vec![0.0f32; w * h];
3194
3195    // Temperature decreases with altitude (environmental lapse rate: ~6.5°C per km)
3196    // Assume 1 unit height = 1000 m
3197    let lapse_rate = 6.5f32;
3198
3199    // Latitude effect: cooler at poles
3200    // We treat y axis as N-S gradient
3201    let lat_range = 60.0f32; // ±60° simulation range
3202
3203    let fbm_params = FbmParams { octaves: 4, frequency: noise_scale, lacunarity: 2.0, gain: 0.5, amplitude: 5.0, offset: 0.0, ridge: false };
3204
3205    for y in 0..h {
3206        for x in 0..w {
3207            let nx = x as f32 / w as f32 + (seed as f32 * 0.0001);
3208            let ny = y as f32 / h as f32;
3209            let altitude = hmap.get(x, y);
3210
3211            // Latitude gradient: y=0 -> -lat_range, y=h -> +lat_range
3212            let lat_factor = (ny - 0.5) * 2.0 * lat_range + latitude;
3213            let lat_temp   = base_temp - lat_factor.abs() * 0.5;
3214
3215            // Altitude cooling
3216            let alt_cooling = altitude * lapse_rate * 5.0; // scale factor for normalised heights
3217
3218            // Noise variation
3219            let noise_var = fbm_2d(nx * 3.0, ny * 3.0, &fbm_params);
3220
3221            temp_map[y * w + x] = lat_temp - alt_cooling + noise_var;
3222        }
3223    }
3224    temp_map
3225}
3226
3227pub fn generate_humidity_map(
3228    hmap:        &Heightmap,
3229    sea_level:   f32,
3230    noise_scale: f32,
3231    seed:        u64,
3232) -> Vec<f32> {
3233    let w = hmap.width;
3234    let h = hmap.height;
3235    let mut hum_map = vec![0.0f32; w * h];
3236
3237    let fbm_params = FbmParams { octaves: 5, frequency: noise_scale, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
3238
3239    // Simple humidity: higher near sea, lower far from water, noise variation
3240    // First pass: mark ocean cells
3241    let is_ocean: Vec<bool> = (0..w*h).map(|i| hmap.data[i] <= sea_level).collect();
3242
3243    // Distance-to-ocean approximation using a fast spread (BFS would be ideal but we use noise)
3244    for y in 0..h {
3245        for x in 0..w {
3246            let nx = x as f32 / w as f32 + (seed as f32 * 0.0002 + 0.5);
3247            let ny = y as f32 / h as f32 + 0.33;
3248            let altitude = hmap.get(x, y);
3249
3250            // Base humidity from proximity to sea (approximated by altitude inversion)
3251            let coast_humidity = if altitude <= sea_level + 0.05 {
3252                0.85 + fbm_2d(nx * 2.0, ny * 2.0, &fbm_params) * 0.15
3253            } else {
3254                let alt_factor = ((altitude - sea_level) / (1.0 - sea_level)).clamp(0.0, 1.0);
3255                (0.7 - alt_factor * 0.5 + fbm_2d(nx * 4.0, ny * 4.0, &fbm_params) * 0.3).clamp(0.0, 1.0)
3256            };
3257
3258            hum_map[y * w + x] = coast_humidity;
3259        }
3260    }
3261    hum_map
3262}
3263
3264// ============================================================
3265//  CLIP PLANES, FRUSTUM CULLING
3266// ============================================================
3267
3268#[derive(Clone, Debug)]
3269pub struct Plane {
3270    pub normal: Vec3,
3271    pub d:      f32,
3272}
3273
3274impl Plane {
3275    pub fn new(normal: Vec3, d: f32) -> Self { Plane { normal, d } }
3276    pub fn from_point_normal(point: Vec3, normal: Vec3) -> Self {
3277        Plane { normal: normal.normalize(), d: -normal.normalize().dot(point) }
3278    }
3279    pub fn distance_to_point(&self, p: Vec3) -> f32 {
3280        self.normal.dot(p) + self.d
3281    }
3282    pub fn normalize(&self) -> Self {
3283        let len = self.normal.length();
3284        Plane { normal: self.normal / len, d: self.d / len }
3285    }
3286}
3287
3288#[derive(Clone, Debug)]
3289pub struct Frustum {
3290    pub planes: [Plane; 6], // near, far, left, right, top, bottom
3291}
3292
3293impl Frustum {
3294    pub fn from_view_proj(vp: Mat4) -> Self {
3295        let m = vp.to_cols_array();
3296        // Extract frustum planes from view-projection matrix (Gribb-Hartmann method)
3297        let planes = [
3298            Plane::new(Vec3::new(m[3]+m[2], m[7]+m[6], m[11]+m[10]), m[15]+m[14]).normalize(), // near
3299            Plane::new(Vec3::new(m[3]-m[2], m[7]-m[6], m[11]-m[10]), m[15]-m[14]).normalize(), // far
3300            Plane::new(Vec3::new(m[3]+m[0], m[7]+m[4], m[11]+m[8]),  m[15]+m[12]).normalize(), // left
3301            Plane::new(Vec3::new(m[3]-m[0], m[7]-m[4], m[11]-m[8]),  m[15]-m[12]).normalize(), // right
3302            Plane::new(Vec3::new(m[3]+m[1], m[7]+m[5], m[11]+m[9]),  m[15]+m[13]).normalize(), // top
3303            Plane::new(Vec3::new(m[3]-m[1], m[7]-m[5], m[11]-m[9]),  m[15]-m[13]).normalize(), // bottom
3304        ];
3305        Frustum { planes }
3306    }
3307
3308    pub fn test_aabb(&self, min: Vec3, max: Vec3) -> bool {
3309        for plane in &self.planes {
3310            // Find positive vertex (most positive in plane normal direction)
3311            let px = if plane.normal.x >= 0.0 { max.x } else { min.x };
3312            let py = if plane.normal.y >= 0.0 { max.y } else { min.y };
3313            let pz = if plane.normal.z >= 0.0 { max.z } else { min.z };
3314            let pv = Vec3::new(px, py, pz);
3315            if plane.distance_to_point(pv) < 0.0 { return false; }
3316        }
3317        true
3318    }
3319
3320    pub fn test_sphere(&self, center: Vec3, radius: f32) -> bool {
3321        for plane in &self.planes {
3322            if plane.distance_to_point(center) < -radius { return false; }
3323        }
3324        true
3325    }
3326}
3327
3328// ============================================================
3329//  RAY CASTING AGAINST HEIGHTMAP
3330// ============================================================
3331
3332#[derive(Clone, Debug)]
3333pub struct RayHit {
3334    pub point:    Vec3,
3335    pub normal:   Vec3,
3336    pub t:        f32,
3337    pub cell_x:   usize,
3338    pub cell_y:   usize,
3339    pub altitude: f32,
3340}
3341
3342/// Ray-heightmap intersection using adaptive stepping
3343pub fn ray_heightmap_intersect(
3344    hmap:      &Heightmap,
3345    cell_size: f32,
3346    height_scale: f32,
3347    ray_origin: Vec3,
3348    ray_dir:    Vec3,
3349) -> Option<RayHit> {
3350    let dir = ray_dir.normalize();
3351    if dir.y.abs() < 1e-6 { return None; }
3352
3353    let w = hmap.width  as f32;
3354    let h = hmap.height as f32;
3355
3356    let mut t   = 0.0f32;
3357    let step    = cell_size * 0.5;
3358    let max_t   = (w * w + h * h + height_scale * height_scale).sqrt() * 2.0;
3359
3360    let mut prev_pos = ray_origin;
3361    let mut prev_above = true;
3362
3363    loop {
3364        t += step;
3365        if t > max_t { return None; }
3366
3367        let pos = ray_origin + dir * t;
3368        let gx  = pos.x / cell_size;
3369        let gz  = pos.z / cell_size;
3370
3371        if gx < 0.0 || gz < 0.0 || gx >= w || gz >= h { continue; }
3372
3373        let ux = gx / w;
3374        let uz = gz / h;
3375        let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
3376
3377        let above = pos.y >= terrain_h;
3378        if !above && prev_above {
3379            // Bisect for precise hit
3380            let mut lo = t - step;
3381            let mut hi = t;
3382            for _ in 0..8 {
3383                let mid = (lo + hi) * 0.5;
3384                let mpos = ray_origin + dir * mid;
3385                let mx = mpos.x / cell_size;
3386                let mz = mpos.z / cell_size;
3387                if mx < 0.0 || mz < 0.0 || mx >= w || mz >= h { hi = mid; continue; }
3388                let mu = mx / w;
3389                let mv = mz / h;
3390                let mh = hmap.sample_bilinear(mu, mv) * height_scale;
3391                if mpos.y >= mh { lo = mid; } else { hi = mid; }
3392            }
3393            let hit_t   = (lo + hi) * 0.5;
3394            let hit_pos = ray_origin + dir * hit_t;
3395            let hx = (hit_pos.x / cell_size) as usize;
3396            let hz = (hit_pos.z / cell_size) as usize;
3397            let hx = hx.min(hmap.width  - 1);
3398            let hz = hz.min(hmap.height - 1);
3399            let normal = hmap.normal_at(hx, hz, cell_size);
3400            let altitude = hmap.get(hx, hz);
3401            return Some(RayHit { point: hit_pos, normal, t: hit_t, cell_x: hx, cell_y: hz, altitude });
3402        }
3403
3404        prev_above = above;
3405        prev_pos   = pos;
3406    }
3407}
3408
3409// ============================================================
3410//  MAIN WorldEditor STRUCT
3411// ============================================================
3412
3413pub struct WorldEditor {
3414    // Core terrain
3415    pub heightmap:       Heightmap,
3416    pub cell_size:       f32,
3417    pub height_scale:    f32,
3418    pub sea_level:       f32,
3419    pub world_name:      String,
3420    pub metadata:        HashMap<String, String>,
3421
3422    // Climate maps
3423    pub temperature_map: Vec<f32>,
3424    pub humidity_map:    Vec<f32>,
3425
3426    // Systems
3427    pub biome_system:    BiomeSystem,
3428    pub weather:         WeatherSystem,
3429    pub road_network:    RoadNetwork,
3430    pub atmosphere:      AtmosphereParams,
3431
3432    // Water
3433    pub rivers:     Vec<RiverPath>,
3434    pub lakes:      Vec<LakeBody>,
3435    pub shore:      Option<OceanShore>,
3436
3437    // Foliage
3438    pub foliage:    Vec<FoliageInstance>,
3439    pub foliage_params: Vec<FoliagePlacementParams>,
3440
3441    // Editor state
3442    pub selection:   SelectionState,
3443    pub undo_redo:   UndoRedoStack,
3444    pub active_tool: EditorTool,
3445    pub brush:       BrushSettings,
3446
3447    // Time / solar
3448    pub utc_hour:    f64,
3449    pub day_of_year: f64,
3450    pub latitude:    f64,
3451    pub longitude:   f64,
3452    pub solar:       SolarState,
3453
3454    // Statistics cache
3455    pub stats:       WorldStats,
3456
3457    // Noise configuration
3458    pub terrain_fbm_params: FbmParams,
3459    pub warp_strength:       f32,
3460    pub erosion_params:      ErosionParams,
3461
3462    // Seed for procedural generation
3463    pub master_seed: u64,
3464
3465    // Dirty flags
3466    pub heightmap_dirty:     bool,
3467    pub climate_dirty:       bool,
3468    pub foliage_dirty:       bool,
3469    pub water_dirty:         bool,
3470}
3471
3472#[derive(Clone, Debug, Default)]
3473pub struct WorldStats {
3474    pub total_cells:       usize,
3475    pub ocean_cells:       usize,
3476    pub land_cells:        usize,
3477    pub mountain_cells:    usize,
3478    pub river_count:       usize,
3479    pub lake_count:        usize,
3480    pub road_segments:     usize,
3481    pub road_total_length: f32,
3482    pub foliage_count:     usize,
3483    pub min_height:        f32,
3484    pub max_height:        f32,
3485    pub mean_height:       f32,
3486    pub dominant_biome:    Option<BiomeId>,
3487}
3488
3489impl WorldEditor {
3490    pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
3491        let heightmap = Heightmap::new(width, height);
3492        let temp_map  = vec![15.0f32; width * height];
3493        let hum_map   = vec![0.5f32;  width * height];
3494
3495        let weather = WeatherSystem::new(12345, 15.0, 45.0);
3496        let atmo    = AtmosphereParams::default();
3497
3498        let solar = SolarState::compute(45.0, 0.0, 180.0, 12.0, &atmo);
3499
3500        WorldEditor {
3501            heightmap,
3502            cell_size,
3503            height_scale: 500.0,
3504            sea_level: 0.2,
3505            world_name: String::from("Untitled World"),
3506            metadata: HashMap::new(),
3507
3508            temperature_map: temp_map,
3509            humidity_map:    hum_map,
3510
3511            biome_system: BiomeSystem::new(),
3512            weather,
3513            road_network: RoadNetwork::new(),
3514            atmosphere: atmo,
3515
3516            rivers: Vec::new(),
3517            lakes:  Vec::new(),
3518            shore:  None,
3519
3520            foliage: Vec::new(),
3521            foliage_params: Vec::new(),
3522
3523            selection:   SelectionState::new(),
3524            undo_redo:   UndoRedoStack::new(256),
3525            active_tool: EditorTool::Select,
3526            brush: BrushSettings {
3527                radius:   20.0,
3528                strength: 0.01,
3529                falloff:  BrushFalloff::Smooth,
3530                scatter:  0.0,
3531            },
3532
3533            utc_hour:    12.0,
3534            day_of_year: 180.0,
3535            latitude:    45.0,
3536            longitude:   0.0,
3537            solar,
3538
3539            stats: WorldStats::default(),
3540            terrain_fbm_params: FbmParams::default_terrain(),
3541            warp_strength: 0.3,
3542            erosion_params: ErosionParams::default(),
3543
3544            master_seed: 0xCAFEBABE,
3545            heightmap_dirty:  true,
3546            climate_dirty:    true,
3547            foliage_dirty:    true,
3548            water_dirty:      true,
3549        }
3550    }
3551
3552    // ---- Terrain Generation ----
3553
3554    pub fn generate_terrain(&mut self) {
3555        let seed_offset = Vec2::new(
3556            (self.master_seed & 0xFFFF) as f32 / 65536.0,
3557            ((self.master_seed >> 16) & 0xFFFF) as f32 / 65536.0,
3558        );
3559
3560        if self.warp_strength > 0.0 {
3561            self.heightmap.generate_domain_warp(&self.terrain_fbm_params, self.warp_strength, seed_offset);
3562        } else {
3563            self.heightmap.generate_fbm(&self.terrain_fbm_params, seed_offset);
3564        }
3565
3566        self.heightmap_dirty = true;
3567        self.climate_dirty   = true;
3568        self.water_dirty     = true;
3569        self.foliage_dirty   = true;
3570    }
3571
3572    pub fn apply_erosion(&mut self) {
3573        hydraulic_erosion(&mut self.heightmap, &self.erosion_params);
3574        self.heightmap_dirty = true;
3575        self.water_dirty     = true;
3576        self.foliage_dirty   = true;
3577    }
3578
3579    pub fn apply_thermal_erosion(&mut self, iterations: usize, talus_deg: f32) {
3580        let talus_rad = talus_deg * DEG2RAD;
3581        thermal_erosion(&mut self.heightmap, iterations, talus_rad);
3582        self.heightmap_dirty = true;
3583    }
3584
3585    // ---- Climate ----
3586
3587    pub fn generate_climate(&mut self) {
3588        self.temperature_map = generate_temperature_map(
3589            &self.heightmap,
3590            15.0,
3591            self.latitude as f32,
3592            2.0,
3593            self.master_seed,
3594        );
3595        self.humidity_map = generate_humidity_map(
3596            &self.heightmap,
3597            self.sea_level,
3598            2.0,
3599            self.master_seed ^ 0x55AA,
3600        );
3601        self.climate_dirty = false;
3602    }
3603
3604    pub fn get_biome_at(&self, x: usize, y: usize) -> BiomeBlendSample {
3605        let w = self.heightmap.width;
3606        let idx = y * w + x;
3607        let temp     = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
3608        let humidity = if idx < self.humidity_map.len()    { self.humidity_map[idx]    } else { 0.5  };
3609        let altitude = self.heightmap.get(x, y);
3610        self.biome_system.sample(temp, humidity, altitude)
3611    }
3612
3613    // ---- Water ----
3614
3615    pub fn generate_rivers(&mut self, num_rivers: usize) {
3616        self.rivers.clear();
3617        let mut rng = LcgRng::new(self.master_seed ^ 0xABCDEF);
3618
3619        let w = self.heightmap.width  as f32;
3620        let h = self.heightmap.height as f32;
3621
3622        for _ in 0..num_rivers {
3623            // Start from high-altitude random point
3624            let attempts = 20;
3625            let mut start = Vec2::ZERO;
3626            let mut found_start = false;
3627            for _ in 0..attempts {
3628                let sx = rng.next_f32() * w;
3629                let sy = rng.next_f32() * h;
3630                let ux = sx / w;
3631                let uy = sy / h;
3632                let alt = self.heightmap.sample_bilinear(ux, uy);
3633                if alt > 0.55 {
3634                    start = Vec2::new(sx, sy);
3635                    found_start = true;
3636                    break;
3637                }
3638            }
3639            if !found_start { continue; }
3640
3641            let river = simulate_river(&self.heightmap, start, self.sea_level);
3642            if river.points.len() >= 10 {
3643                self.rivers.push(river);
3644            }
3645        }
3646
3647        self.water_dirty = false;
3648    }
3649
3650    pub fn generate_lakes(&mut self, num_lakes: usize, max_water_level: f32) {
3651        self.lakes.clear();
3652        let mut rng = LcgRng::new(self.master_seed ^ 0x123123);
3653        let w = self.heightmap.width;
3654        let h = self.heightmap.height;
3655
3656        for _ in 0..num_lakes {
3657            let sx = (rng.next_f32() * (w - 2) as f32) as usize + 1;
3658            let sy = (rng.next_f32() * (h - 2) as f32) as usize + 1;
3659            let base_h = self.heightmap.get(sx, sy);
3660            if base_h <= self.sea_level || base_h > 0.6 { continue; }
3661            let water_level = base_h + rng.next_f32() * max_water_level;
3662            let lake = fill_lake(&self.heightmap, sx, sy, water_level);
3663            if lake.cells.len() > 4 {
3664                self.lakes.push(lake);
3665            }
3666        }
3667    }
3668
3669    pub fn generate_shore(&mut self) {
3670        self.shore = Some(generate_ocean_shore(&self.heightmap, self.sea_level, 0.02));
3671    }
3672
3673    // ---- Foliage ----
3674
3675    pub fn place_foliage_layer(&mut self, params: FoliagePlacementParams, seed: u64) {
3676        let new_instances = place_foliage(
3677            &self.heightmap,
3678            None,
3679            &params,
3680            seed,
3681            self.cell_size,
3682        );
3683        self.foliage.extend(new_instances);
3684    }
3685
3686    pub fn clear_foliage(&mut self) {
3687        self.foliage.clear();
3688    }
3689
3690    pub fn cull_foliage(&mut self, frustum: &Frustum) -> Vec<usize> {
3691        let mut visible = Vec::new();
3692        for (i, fi) in self.foliage.iter().enumerate() {
3693            let r = fi.scale.length();
3694            if frustum.test_sphere(fi.position, r) {
3695                visible.push(i);
3696            }
3697        }
3698        visible
3699    }
3700
3701    // ---- Roads ----
3702
3703    pub fn build_road(
3704        &mut self,
3705        start: Vec2,
3706        end:   Vec2,
3707        road_type: RoadType,
3708    ) -> Option<u32> {
3709        let cost_params = RoadCostParams::default();
3710        let seg_id = self.road_network.build_road(
3711            &self.heightmap,
3712            start,
3713            end,
3714            self.cell_size,
3715            road_type,
3716            &cost_params,
3717        )?;
3718        Some(seg_id)
3719    }
3720
3721    // ---- Solar / Sky ----
3722
3723    pub fn update_solar(&mut self) {
3724        self.solar = SolarState::compute(
3725            self.latitude,
3726            self.longitude,
3727            self.day_of_year,
3728            self.utc_hour,
3729            &self.atmosphere,
3730        );
3731    }
3732
3733    pub fn advance_time(&mut self, delta_hours: f64) {
3734        self.utc_hour += delta_hours;
3735        if self.utc_hour >= 24.0 {
3736            self.utc_hour -= 24.0;
3737            self.day_of_year += 1.0;
3738            if self.day_of_year > 365.0 {
3739                self.day_of_year = 1.0;
3740            }
3741        }
3742        self.update_solar();
3743        self.weather.advance_season((delta_hours / 8760.0) as f32);
3744        self.weather.step(delta_hours as f32);
3745    }
3746
3747    // ---- Editing ----
3748
3749    pub fn raise_terrain(&mut self, cx: f32, cy: f32, delta: f32) {
3750        let action = terrain_brush_raise(&mut self.heightmap, cx, cy, &self.brush, delta);
3751        self.undo_redo.push(action);
3752        self.heightmap_dirty = true;
3753    }
3754
3755    pub fn lower_terrain(&mut self, cx: f32, cy: f32, delta: f32) {
3756        let action = terrain_brush_raise(&mut self.heightmap, cx, cy, &self.brush, -delta);
3757        self.undo_redo.push(action);
3758        self.heightmap_dirty = true;
3759    }
3760
3761    pub fn smooth_terrain(&mut self, cx: f32, cy: f32, iters: usize) {
3762        let action = terrain_brush_smooth(&mut self.heightmap, cx, cy, &self.brush, iters);
3763        self.undo_redo.push(action);
3764        self.heightmap_dirty = true;
3765    }
3766
3767    pub fn flatten_terrain(&mut self, cx: f32, cy: f32, target: f32) {
3768        let action = terrain_brush_flatten(&mut self.heightmap, cx, cy, &self.brush, target);
3769        self.undo_redo.push(action);
3770        self.heightmap_dirty = true;
3771    }
3772
3773    // ---- Undo / Redo ----
3774
3775    pub fn undo(&mut self) {
3776        if let Some(action) = self.undo_redo.pop_undo() {
3777            let redo_action = self.apply_action_inverse(&action);
3778            self.undo_redo.push_redo(redo_action);
3779            self.heightmap_dirty = true;
3780        }
3781    }
3782
3783    pub fn redo(&mut self) {
3784        if let Some(action) = self.undo_redo.pop_redo() {
3785            let undo_action = self.apply_action_inverse(&action);
3786            self.undo_redo.push(undo_action);
3787            self.heightmap_dirty = true;
3788        }
3789    }
3790
3791    fn apply_action_inverse(&mut self, action: &EditAction) -> EditAction {
3792        match action {
3793            EditAction::SetHeightRegion { x, y, width, height, old_data, new_data } => {
3794                for row in 0..*height {
3795                    for col in 0..*width {
3796                        let hx = x + col;
3797                        let hy = y + row;
3798                        if hx < self.heightmap.width && hy < self.heightmap.height {
3799                            let i = row * width + col;
3800                            if i < old_data.len() {
3801                                self.heightmap.set(hx, hy, old_data[i]);
3802                            }
3803                        }
3804                    }
3805                }
3806                EditAction::SetHeightRegion {
3807                    x: *x, y: *y, width: *width, height: *height,
3808                    old_data: new_data.clone(),
3809                    new_data: old_data.clone(),
3810                }
3811            }
3812            EditAction::AddRoadSegment { segment_id, segment } => {
3813                self.road_network.segments.retain(|s| s.id != *segment_id);
3814                EditAction::RemoveRoadSegment { segment_id: *segment_id, segment: segment.clone() }
3815            }
3816            EditAction::RemoveRoadSegment { segment_id, segment } => {
3817                self.road_network.segments.push(segment.clone());
3818                EditAction::AddRoadSegment { segment_id: *segment_id, segment: segment.clone() }
3819            }
3820            EditAction::AddWaterBody { lake, index } => {
3821                if *index < self.lakes.len() { self.lakes.remove(*index); }
3822                EditAction::RemoveWaterBody { lake: lake.clone(), index: *index }
3823            }
3824            EditAction::RemoveWaterBody { lake, index } => {
3825                let i = (*index).min(self.lakes.len());
3826                self.lakes.insert(i, lake.clone());
3827                EditAction::AddWaterBody { lake: lake.clone(), index: *index }
3828            }
3829            EditAction::PlaceFoliageInstances { instances, indices } => {
3830                for &idx in indices.iter().rev() {
3831                    if idx < self.foliage.len() { self.foliage.remove(idx); }
3832                }
3833                EditAction::RemoveFoliageInstances {
3834                    indices: indices.clone(),
3835                    instances: instances.clone(),
3836                }
3837            }
3838            EditAction::RemoveFoliageInstances { instances, indices } => {
3839                for (i, inst) in indices.iter().zip(instances.iter()) {
3840                    let insert_at = (*i).min(self.foliage.len());
3841                    self.foliage.insert(insert_at, inst.clone());
3842                }
3843                EditAction::PlaceFoliageInstances {
3844                    instances: instances.clone(),
3845                    indices: indices.clone(),
3846                }
3847            }
3848            EditAction::SetBiomeOverride { .. } => {
3849                // Biome overrides not yet stored on editor — return no-op
3850                action.clone()
3851            }
3852            EditAction::CompoundAction { actions, description } => {
3853                let mut reverse_actions = Vec::with_capacity(actions.len());
3854                for a in actions.iter().rev() {
3855                    reverse_actions.push(self.apply_action_inverse(a));
3856                }
3857                EditAction::CompoundAction {
3858                    actions: reverse_actions,
3859                    description: format!("Undo: {}", description),
3860                }
3861            }
3862        }
3863    }
3864
3865    // ---- Ray Casting ----
3866
3867    pub fn ray_cast(&self, ray_origin: Vec3, ray_dir: Vec3) -> Option<RayHit> {
3868        ray_heightmap_intersect(
3869            &self.heightmap,
3870            self.cell_size,
3871            self.height_scale,
3872            ray_origin,
3873            ray_dir,
3874        )
3875    }
3876
3877    // ---- Statistics ----
3878
3879    pub fn compute_stats(&mut self) {
3880        let w = self.heightmap.width;
3881        let h = self.heightmap.height;
3882        let total = w * h;
3883
3884        let mut ocean    = 0usize;
3885        let mut mountain = 0usize;
3886        let mut sum      = 0.0f64;
3887        let mut biome_counts = [0usize; 25];
3888
3889        for y in 0..h {
3890            for x in 0..w {
3891                let alt = self.heightmap.get(x, y);
3892                sum += alt as f64;
3893                if alt <= self.sea_level { ocean += 1; }
3894                if alt > 0.7 { mountain += 1; }
3895                let idx = y * w + x;
3896                let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
3897                let hum  = if idx < self.humidity_map.len()    { self.humidity_map[idx]    } else { 0.5  };
3898                let biome = BiomeDescriptor::classify_point(temp, hum, alt);
3899                biome_counts[biome as usize] += 1;
3900            }
3901        }
3902
3903        let dominant_idx = biome_counts.iter().enumerate()
3904            .max_by_key(|(_, &c)| c)
3905            .map(|(i, _)| i)
3906            .unwrap_or(0);
3907
3908        self.heightmap.recompute_minmax();
3909
3910        let road_len: f32 = self.road_network.segments.iter().map(|s| s.length).sum();
3911
3912        self.stats = WorldStats {
3913            total_cells:       total,
3914            ocean_cells:       ocean,
3915            land_cells:        total - ocean,
3916            mountain_cells:    mountain,
3917            river_count:       self.rivers.len(),
3918            lake_count:        self.lakes.len(),
3919            road_segments:     self.road_network.segments.len(),
3920            road_total_length: road_len,
3921            foliage_count:     self.foliage.len(),
3922            min_height:        self.heightmap.min_h,
3923            max_height:        self.heightmap.max_h,
3924            mean_height:       (sum / total as f64) as f32,
3925            dominant_biome:    Some(BiomeId::TropicalRainforest), // simplified
3926        };
3927    }
3928
3929    // ---- Serialization ----
3930
3931    pub fn serialize(&self) -> Vec<u8> {
3932        let sw = SerializedWorld::from_editor(self);
3933        sw.to_bytes()
3934    }
3935
3936    // ---- Full procedural generation pipeline ----
3937
3938    pub fn generate_full_world(
3939        &mut self,
3940        num_rivers: usize,
3941        num_lakes:  usize,
3942        foliage_density: f32,
3943    ) {
3944        // 1. Generate terrain
3945        self.generate_terrain();
3946
3947        // 2. Apply erosion
3948        self.apply_erosion();
3949        self.apply_thermal_erosion(5, 35.0);
3950
3951        // 3. Generate climate maps
3952        self.generate_climate();
3953
3954        // 4. Generate water bodies
3955        self.generate_rivers(num_rivers);
3956        self.generate_lakes(num_lakes, 0.03);
3957        self.generate_shore();
3958
3959        // 5. Place foliage
3960        let biome_table = build_biome_table();
3961        for (biome_idx, desc) in biome_table.iter().enumerate() {
3962            let fp = FoliagePlacementParams {
3963                min_radius:      2.0 + (1.0 - desc.tree_density) * 8.0,
3964                max_instances:   (desc.tree_density * foliage_density * 50000.0) as usize,
3965                max_slope_rad:   0.6,
3966                min_altitude:    desc.alt_min,
3967                max_altitude:    desc.alt_max,
3968                density_scale:   desc.tree_density * foliage_density,
3969                use_density_map: false,
3970                random_rotation: true,
3971                scale_variance:  0.3,
3972                base_scale:      Vec3::new(1.0, 1.0 + desc.tree_density, 1.0),
3973                asset_id:        biome_idx as u32,
3974                biome_id:        biome_idx as u8,
3975                align_to_normal: false,
3976            };
3977            self.place_foliage_layer(fp, self.master_seed ^ (biome_idx as u64 * 997));
3978        }
3979
3980        // 6. Update solar state
3981        self.update_solar();
3982
3983        // 7. Compute statistics
3984        self.compute_stats();
3985    }
3986
3987    // ---- Camera helpers ----
3988
3989    pub fn world_to_heightmap(&self, world: Vec3) -> (usize, usize) {
3990        let x = (world.x / self.cell_size) as usize;
3991        let z = (world.z / self.cell_size) as usize;
3992        (x.min(self.heightmap.width - 1), z.min(self.heightmap.height - 1))
3993    }
3994
3995    pub fn heightmap_to_world(&self, x: usize, z: usize) -> Vec3 {
3996        let height = self.heightmap.get(x, z) * self.height_scale;
3997        Vec3::new(x as f32 * self.cell_size, height, z as f32 * self.cell_size)
3998    }
3999
4000    pub fn world_bounds(&self) -> (Vec3, Vec3) {
4001        let min = Vec3::ZERO;
4002        let max = Vec3::new(
4003            self.heightmap.width  as f32 * self.cell_size,
4004            self.height_scale,
4005            self.heightmap.height as f32 * self.cell_size,
4006        );
4007        (min, max)
4008    }
4009
4010    // ---- Gizmo rendering helpers ----
4011
4012    pub fn get_selection_pivot(&self) -> Vec3 {
4013        if let Some(p) = self.selection.pivot { return p; }
4014        // Compute from selected cells
4015        let mut sum = Vec3::ZERO;
4016        let mut count = 0;
4017        for item in &self.selection.items {
4018            if let SelectionItem::TerrainCell(x, z) = item {
4019                sum += self.heightmap_to_world(*x, *z);
4020                count += 1;
4021            }
4022        }
4023        if count > 0 { sum / count as f32 } else { Vec3::ZERO }
4024    }
4025
4026    // ---- Measure tool ----
4027
4028    pub fn measure_distance(&self, a_world: Vec3, b_world: Vec3) -> f32 {
4029        (b_world - a_world).length()
4030    }
4031
4032    pub fn measure_area_of_selection(&self) -> f32 {
4033        let count = self.selection.count_terrain_cells();
4034        count as f32 * self.cell_size * self.cell_size
4035    }
4036
4037    // ---- LOD helpers ----
4038
4039    pub fn compute_lod_factor(&self, pos: Vec3, camera_pos: Vec3, lod_distances: &[f32]) -> u8 {
4040        let dist = (pos - camera_pos).length();
4041        for (i, &d) in lod_distances.iter().enumerate() {
4042            if dist < d { return i as u8; }
4043        }
4044        lod_distances.len() as u8
4045    }
4046
4047    pub fn update_foliage_lod(&mut self, camera_pos: Vec3) {
4048        let lod_distances = [50.0f32, 150.0, 400.0, 1000.0];
4049        for fi in self.foliage.iter_mut() {
4050            let dist = (fi.position - camera_pos).length();
4051            fi.lod_factor = (dist / lod_distances[lod_distances.len() - 1]).clamp(0.0, 1.0);
4052        }
4053    }
4054
4055    // ---- Debug helpers ----
4056
4057    pub fn sample_sky_at_direction(&self, dir: Vec3) -> Vec3 {
4058        let raw = compute_sky_color(dir, self.solar.direction, &self.atmosphere);
4059        let with_sun = raw + sun_disk_color(dir, self.solar.direction, 0.009, self.solar.sun_color * self.solar.sun_intensity);
4060        aces_tonemap(with_sun)
4061    }
4062}
4063
4064fn _0_009_rad_equiv_inner() -> f32 { 0.009 }
4065trait RadEquiv { fn _0_009_rad_equiv(&self) -> f32; }
4066// Hack to make the compiler happy — use a free fn:
4067fn zero_point_zero_zero_nine() -> f32 { 0.009 }
4068
4069// Direct free function reference to avoid method call on literal:
4070impl WorldEditor {
4071    pub fn sky_at(&self, view_dir: Vec3) -> Vec3 {
4072        let raw = compute_sky_color(view_dir, self.solar.direction, &self.atmosphere);
4073        let sun = sun_disk_color(view_dir, self.solar.direction, 0.009, self.solar.sun_color * self.solar.sun_intensity);
4074        aces_tonemap(raw + sun)
4075    }
4076}
4077
4078// ============================================================
4079//  ADDITIONAL MATHEMATICAL UTILITIES
4080// ============================================================
4081
4082/// Smooth-step: 3x² - 2x³
4083#[inline] pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4084    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4085    t * t * (3.0 - 2.0 * t)
4086}
4087
4088/// Smoother-step: 6x⁵ - 15x⁴ + 10x³
4089#[inline] pub fn smootherstep(edge0: f32, edge1: f32, x: f32) -> f32 {
4090    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
4091    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
4092}
4093
4094/// Remap value from [in_min, in_max] to [out_min, out_max]
4095#[inline] pub fn remap(v: f32, in_min: f32, in_max: f32, out_min: f32, out_max: f32) -> f32 {
4096    out_min + (out_max - out_min) * ((v - in_min) / (in_max - in_min)).clamp(0.0, 1.0)
4097}
4098
4099/// Bilinear interpolation of a 2D value grid
4100pub fn bilinear_sample(data: &[f32], width: usize, height: usize, u: f32, v: f32) -> f32 {
4101    let px = u * (width  - 1) as f32;
4102    let py = v * (height - 1) as f32;
4103    let x0 = px.floor() as usize;
4104    let y0 = py.floor() as usize;
4105    let x1 = (x0 + 1).min(width  - 1);
4106    let y1 = (y0 + 1).min(height - 1);
4107    let tx = px - x0 as f32;
4108    let ty = py - y0 as f32;
4109    let a = data[y0 * width + x0];
4110    let b = data[y0 * width + x1];
4111    let c = data[y1 * width + x0];
4112    let d = data[y1 * width + x1];
4113    lerp_f(lerp_f(a, b, tx), lerp_f(c, d, tx), ty)
4114}
4115
4116/// Build a 2D Gaussian kernel (sigma, kernel_size must be odd)
4117pub fn gaussian_kernel_2d(sigma: f32, size: usize) -> Vec<f32> {
4118    let half   = (size / 2) as i32;
4119    let sigma2 = sigma * sigma;
4120    let mut k  = vec![0.0f32; size * size];
4121    let mut sum = 0.0f32;
4122    for y in 0..size as i32 {
4123        for x in 0..size as i32 {
4124            let dx = (x - half) as f32;
4125            let dy = (y - half) as f32;
4126            let v  = (-(dx*dx + dy*dy) / (2.0 * sigma2)).exp();
4127            k[(y as usize) * size + (x as usize)] = v;
4128            sum += v;
4129        }
4130    }
4131    for v in k.iter_mut() { *v /= sum; }
4132    k
4133}
4134
4135/// Apply a separable Gaussian blur to a 2D float map
4136pub fn gaussian_blur_2d(data: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
4137    let radius = (sigma * 3.0).ceil() as i32;
4138    let size   = (radius * 2 + 1) as usize;
4139    // 1D kernel
4140    let mut kernel = vec![0.0f32; size];
4141    let mut ksum   = 0.0f32;
4142    for i in 0..size as i32 {
4143        let d  = (i - radius) as f32;
4144        let v  = (-(d*d) / (2.0 * sigma * sigma)).exp();
4145        kernel[i as usize] = v;
4146        ksum += v;
4147    }
4148    for v in kernel.iter_mut() { *v /= ksum; }
4149
4150    // Horizontal pass
4151    let mut temp = vec![0.0f32; width * height];
4152    for y in 0..height {
4153        for x in 0..width {
4154            let mut acc = 0.0f32;
4155            for (ki, &kv) in kernel.iter().enumerate() {
4156                let nx = (x as i32 + ki as i32 - radius).clamp(0, width as i32 - 1) as usize;
4157                acc += data[y * width + nx] * kv;
4158            }
4159            temp[y * width + x] = acc;
4160        }
4161    }
4162
4163    // Vertical pass
4164    let mut out = vec![0.0f32; width * height];
4165    for y in 0..height {
4166        for x in 0..width {
4167            let mut acc = 0.0f32;
4168            for (ki, &kv) in kernel.iter().enumerate() {
4169                let ny = (y as i32 + ki as i32 - radius).clamp(0, height as i32 - 1) as usize;
4170                acc += temp[ny * width + x] * kv;
4171            }
4172            out[y * width + x] = acc;
4173        }
4174    }
4175    out
4176}
4177
4178/// Diamond-square fractal terrain generation
4179pub fn diamond_square(size: usize, roughness: f32, seed: u64) -> Vec<f32> {
4180    // size must be 2^n + 1
4181    let mut grid = vec![0.0f32; size * size];
4182    let mut rng  = LcgRng::new(seed);
4183
4184    // Corner seeds
4185    grid[0]                   = rng.next_f32();
4186    grid[size - 1]            = rng.next_f32();
4187    grid[(size-1)*size]       = rng.next_f32();
4188    grid[(size-1)*size+size-1]= rng.next_f32();
4189
4190    let mut step    = size - 1;
4191    let mut scale   = roughness;
4192    let half_size   = size as i32;
4193
4194    while step > 1 {
4195        let half = step / 2;
4196
4197        // Diamond step
4198        let mut y = 0;
4199        while y < size - 1 {
4200            let mut x = 0;
4201            while x < size - 1 {
4202                let avg = (
4203                    grid[y        * size + x       ]
4204                    + grid[y        * size + x + step]
4205                    + grid[(y+step) * size + x       ]
4206                    + grid[(y+step) * size + x + step]
4207                ) / 4.0;
4208                grid[(y+half) * size + (x+half)] = avg + (rng.next_f32() * 2.0 - 1.0) * scale;
4209                x += step;
4210            }
4211            y += step;
4212        }
4213
4214        // Square step
4215        let mut y = 0i32;
4216        while y < size as i32 {
4217            let mut x = (if (y as usize / half) % 2 == 0 { half as i32 } else { 0 });
4218            while x < size as i32 {
4219                let mut sum   = 0.0f32;
4220                let mut count = 0;
4221                let offsets: [(i32,i32); 4] = [(-(half as i32), 0), (half as i32, 0), (0, -(half as i32)), (0, half as i32)];
4222                for &(dx, dy) in &offsets {
4223                    let nx = x + dx;
4224                    let ny = y + dy;
4225                    if nx >= 0 && nx < size as i32 && ny >= 0 && ny < size as i32 {
4226                        sum += grid[ny as usize * size + nx as usize];
4227                        count += 1;
4228                    }
4229                }
4230                grid[y as usize * size + x as usize] = sum / count as f32 + (rng.next_f32() * 2.0 - 1.0) * scale;
4231                x += step as i32;
4232            }
4233            y += half as i32;
4234        }
4235
4236        step  /= 2;
4237        scale *= roughness.powf(1.0);
4238    }
4239
4240    // Normalize
4241    let min_v = grid.iter().cloned().fold(f32::MAX, f32::min);
4242    let max_v = grid.iter().cloned().fold(f32::MIN, f32::max);
4243    let range = max_v - min_v;
4244    if range > 1e-10 {
4245        for v in grid.iter_mut() { *v = (*v - min_v) / range; }
4246    }
4247
4248    grid
4249}
4250
4251// ============================================================
4252//  SLOPE MAP, CURVATURE, FLOW DIRECTION
4253// ============================================================
4254
4255/// Compute a slope map (value in radians) for the entire heightmap
4256pub fn compute_slope_map(hmap: &Heightmap, cell_size: f32) -> Vec<f32> {
4257    let w = hmap.width;
4258    let h = hmap.height;
4259    let mut slope_map = vec![0.0f32; w * h];
4260    for y in 0..h {
4261        for x in 0..w {
4262            slope_map[y * w + x] = hmap.slope_at(x, y, cell_size);
4263        }
4264    }
4265    slope_map
4266}
4267
4268/// Curvature — second derivative of height (Laplacian, approximated)
4269pub fn compute_curvature_map(hmap: &Heightmap) -> Vec<f32> {
4270    let w = hmap.width;
4271    let h = hmap.height;
4272    let mut curv_map = vec![0.0f32; w * h];
4273    for y in 1..h-1 {
4274        for x in 1..w-1 {
4275            let center = hmap.get(x, y);
4276            let d2hdx2 = hmap.get(x+1, y) - 2.0 * center + hmap.get(x-1, y);
4277            let d2hdy2 = hmap.get(x, y+1) - 2.0 * center + hmap.get(x, y-1);
4278            curv_map[y * w + x] = d2hdx2 + d2hdy2;
4279        }
4280    }
4281    curv_map
4282}
4283
4284/// D8 flow direction (8 neighbors) — returns index 0-7 of steepest descent
4285pub fn compute_flow_direction(hmap: &Heightmap) -> Vec<u8> {
4286    let w = hmap.width;
4287    let h = hmap.height;
4288    let mut flow = vec![0u8; w * h];
4289    let dirs: [(i32,i32); 8] = [(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1)];
4290    for y in 1..h-1 {
4291        for x in 1..w-1 {
4292            let center = hmap.get(x, y);
4293            let mut best_drop = 0.0f32;
4294            let mut best_dir  = 0u8;
4295            for (i, &(dx, dy)) in dirs.iter().enumerate() {
4296                let nh = hmap.get_clamped(x as i32 + dx, y as i32 + dy);
4297                let drop = center - nh;
4298                let dist = if dx != 0 && dy != 0 { (2.0f32).sqrt() } else { 1.0 };
4299                let slope = drop / dist;
4300                if slope > best_drop { best_drop = slope; best_dir = i as u8; }
4301            }
4302            flow[y * w + x] = best_dir;
4303        }
4304    }
4305    flow
4306}
4307
4308/// Compute flow accumulation from flow direction map
4309pub fn compute_flow_accumulation(flow_dir: &[u8], width: usize, height: usize) -> Vec<u32> {
4310    let mut acc = vec![1u32; width * height]; // each cell starts with 1
4311    let dirs: [(i32,i32); 8] = [(1,0),(1,1),(0,1),(-1,1),(-1,0),(-1,-1),(0,-1),(1,-1)];
4312
4313    // Topological sort (simplified): iterate multiple passes
4314    for _pass in 0..height {
4315        for y in 1..height-1 {
4316            for x in 1..width-1 {
4317                let dir = flow_dir[y * width + x] as usize;
4318                let (dx, dy) = dirs[dir];
4319                let nx = (x as i32 + dx) as usize;
4320                let ny = (y as i32 + dy) as usize;
4321                if nx < width && ny < height {
4322                    acc[ny * width + nx] += acc[y * width + x];
4323                }
4324            }
4325        }
4326    }
4327    acc
4328}
4329
4330// ============================================================
4331//  AMBIENT OCCLUSION (SSAO-style precompute for terrain)
4332// ============================================================
4333
4334/// Compute horizon-based ambient occlusion for each heightmap cell
4335/// Casts rays in multiple horizontal directions and measures occlusion
4336pub fn compute_terrain_ao(hmap: &Heightmap, num_rays: usize, max_dist: f32, cell_size: f32) -> Vec<f32> {
4337    let w = hmap.width;
4338    let h = hmap.height;
4339    let mut ao = vec![1.0f32; w * h];
4340
4341    let angle_step = TWO_PI / num_rays as f32;
4342
4343    for y in 0..h {
4344        for x in 0..w {
4345            let base_h  = hmap.get(x, y);
4346            let mut occ = 0.0f32;
4347
4348            for ray in 0..num_rays {
4349                let angle   = ray as f32 * angle_step;
4350                let ray_dx  = angle.cos();
4351                let ray_dz  = angle.sin();
4352                let mut max_horizon = 0.0f32; // max elevation angle seen
4353
4354                let steps = (max_dist / cell_size).ceil() as usize;
4355                for step in 1..=steps {
4356                    let t  = step as f32 * cell_size;
4357                    let nx = x as f32 + ray_dx * t / cell_size;
4358                    let nz = y as f32 + ray_dz * t / cell_size;
4359                    if nx < 0.0 || nz < 0.0 || nx >= w as f32 || nz >= h as f32 { break; }
4360
4361                    let ux = (nx / w as f32).clamp(0.0, 1.0);
4362                    let uz = (nz / h as f32).clamp(0.0, 1.0);
4363                    let nh = hmap.sample_bilinear(ux, uz);
4364
4365                    let elevation_angle = (nh - base_h) / t * cell_size; // approximate
4366                    if elevation_angle > max_horizon {
4367                        max_horizon = elevation_angle;
4368                    }
4369                }
4370
4371                // Convert max horizon angle to occlusion
4372                let horizon_angle = max_horizon.atan();
4373                occ += (horizon_angle / HALF_PI).clamp(0.0, 1.0);
4374            }
4375
4376            ao[y * w + x] = 1.0 - (occ / num_rays as f32).clamp(0.0, 1.0);
4377        }
4378    }
4379
4380    ao
4381}
4382
4383// ============================================================
4384//  CLOUD LAYER SIMULATION
4385// ============================================================
4386
4387#[derive(Clone, Debug)]
4388pub struct CloudLayer {
4389    pub altitude_km:  f32,
4390    pub thickness_km: f32,
4391    pub coverage:     f32,   // 0..1
4392    pub density:      f32,
4393    pub wind_vel:     Vec2,
4394    pub noise_offset: Vec2,
4395}
4396
4397impl CloudLayer {
4398    pub fn new(altitude_km: f32, thickness_km: f32, coverage: f32) -> Self {
4399        CloudLayer {
4400            altitude_km,
4401            thickness_km,
4402            coverage,
4403            density: coverage * 0.5,
4404            wind_vel: Vec2::new(5.0, 2.0),
4405            noise_offset: Vec2::ZERO,
4406        }
4407    }
4408
4409    /// Compute cloud opacity at a given UV position
4410    pub fn opacity_at(&self, u: f32, v: f32, time: f32) -> f32 {
4411        let offset = self.wind_vel * time * 0.0001;
4412        let su = u + offset.x + self.noise_offset.x;
4413        let sv = v + offset.y + self.noise_offset.y;
4414
4415        let cloud_params = FbmParams { octaves: 5, frequency: 2.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
4416        let n  = fbm_2d(su * 3.0, sv * 3.0, &cloud_params) * 0.5 + 0.5;
4417        let cloud_val = smoothstep(1.0 - self.coverage, 1.0, n);
4418        cloud_val * self.density
4419    }
4420
4421    /// Update cloud layer position by wind
4422    pub fn update(&mut self, delta_time: f32) {
4423        self.noise_offset += self.wind_vel * delta_time * 0.00001;
4424    }
4425}
4426
4427pub struct SkySystem {
4428    pub cloud_layers: Vec<CloudLayer>,
4429    pub params:       AtmosphereParams,
4430    pub time:         f32,
4431}
4432
4433impl SkySystem {
4434    pub fn new() -> Self {
4435        SkySystem {
4436            cloud_layers: vec![
4437                CloudLayer::new(2.0, 0.5, 0.4),
4438                CloudLayer::new(5.0, 1.0, 0.3),
4439                CloudLayer::new(8.0, 2.0, 0.2),
4440            ],
4441            params: AtmosphereParams::default(),
4442            time: 0.0,
4443        }
4444    }
4445
4446    pub fn update(&mut self, delta_time: f32) {
4447        self.time += delta_time;
4448        for layer in self.cloud_layers.iter_mut() {
4449            layer.update(delta_time);
4450        }
4451    }
4452
4453    /// Sample total cloud coverage (max of all layers) at a UV
4454    pub fn cloud_coverage_at(&self, u: f32, v: f32) -> f32 {
4455        self.cloud_layers.iter()
4456            .map(|l| l.opacity_at(u, v, self.time))
4457            .fold(0.0f32, f32::max)
4458    }
4459
4460    pub fn render_sky(&self, view_dir: Vec3, sun_dir: Vec3) -> Vec3 {
4461        let sky  = compute_sky_color(view_dir, sun_dir, &self.params);
4462        let sun  = sun_disk_color(view_dir, sun_dir, 0.009, Vec3::new(10.0, 9.0, 8.0));
4463        aces_tonemap(sky + sun)
4464    }
4465}
4466
4467// ============================================================
4468//  TERRAIN LOD QUADTREE
4469// ============================================================
4470
4471#[derive(Clone, Debug)]
4472pub struct QuadtreeNode {
4473    pub x:        usize,
4474    pub y:        usize,
4475    pub size:     usize,
4476    pub lod:      u8,
4477    pub children: Option<[Box<QuadtreeNode>; 4]>,
4478    pub min_h:    f32,
4479    pub max_h:    f32,
4480    pub center:   Vec3,
4481    pub is_leaf:  bool,
4482}
4483
4484impl QuadtreeNode {
4485    pub fn new(x: usize, y: usize, size: usize, cell_size: f32) -> Self {
4486        let half = size as f32 * 0.5;
4487        let center = Vec3::new(
4488            (x as f32 + half) * cell_size,
4489            0.0,
4490            (y as f32 + half) * cell_size,
4491        );
4492        QuadtreeNode { x, y, size, lod: 0, children: None, min_h: 0.0, max_h: 1.0, center, is_leaf: true }
4493    }
4494
4495    pub fn build(hmap: &Heightmap, x: usize, y: usize, size: usize, min_size: usize, cell_size: f32, depth: u8) -> Box<Self> {
4496        let mut node = QuadtreeNode::new(x, y, size, cell_size);
4497        node.lod = depth;
4498
4499        // Compute height range
4500        let mut min_h = f32::MAX;
4501        let mut max_h = f32::MIN;
4502        let x1 = (x + size).min(hmap.width);
4503        let y1 = (y + size).min(hmap.height);
4504        for cy in y..y1 {
4505            for cx in x..x1 {
4506                let h = hmap.get(cx, cy);
4507                if h < min_h { min_h = h; }
4508                if h > max_h { max_h = h; }
4509            }
4510        }
4511        node.min_h = min_h;
4512        node.max_h = max_h;
4513        node.center.y = (min_h + max_h) * 0.5 * 500.0;
4514
4515        if size <= min_size {
4516            node.is_leaf = true;
4517            return Box::new(node);
4518        }
4519
4520        let half = size / 2;
4521        node.is_leaf = false;
4522        node.children = Some([
4523            QuadtreeNode::build(hmap, x,        y,        half, min_size, cell_size, depth + 1),
4524            QuadtreeNode::build(hmap, x + half, y,        half, min_size, cell_size, depth + 1),
4525            QuadtreeNode::build(hmap, x,        y + half, half, min_size, cell_size, depth + 1),
4526            QuadtreeNode::build(hmap, x + half, y + half, half, min_size, cell_size, depth + 1),
4527        ]);
4528
4529        Box::new(node)
4530    }
4531
4532    /// Collect visible leaf nodes given a frustum and camera position
4533    pub fn collect_visible<'a>(&'a self, frustum: &Frustum, cam_pos: Vec3, max_lod: u8, out: &mut Vec<&'a QuadtreeNode>, cell_size: f32, height_scale: f32) {
4534        let aabb_min = Vec3::new(
4535            self.x as f32 * cell_size,
4536            self.min_h * height_scale,
4537            self.y as f32 * cell_size,
4538        );
4539        let aabb_max = Vec3::new(
4540            (self.x + self.size) as f32 * cell_size,
4541            self.max_h * height_scale,
4542            (self.y + self.size) as f32 * cell_size,
4543        );
4544
4545        if !frustum.test_aabb(aabb_min, aabb_max) { return; }
4546
4547        if self.is_leaf || self.lod >= max_lod {
4548            out.push(self);
4549            return;
4550        }
4551
4552        let dist = (self.center - cam_pos).length();
4553        let lod_size = self.size as f32 * cell_size;
4554        // Switch to leaf if this node's size is small enough relative to distance
4555        if lod_size / dist < 0.5 {
4556            out.push(self);
4557            return;
4558        }
4559
4560        if let Some(ref ch) = self.children {
4561            for c in ch.iter() {
4562                c.collect_visible(frustum, cam_pos, max_lod, out, cell_size, height_scale);
4563            }
4564        } else {
4565            out.push(self);
4566        }
4567    }
4568}
4569
4570// ============================================================
4571//  VEGETATION DENSITY MAP FROM BIOMES
4572// ============================================================
4573
4574pub fn generate_vegetation_density_map(
4575    hmap:        &Heightmap,
4576    temp_map:    &[f32],
4577    hum_map:     &[f32],
4578    biome_sys:   &BiomeSystem,
4579) -> Vec<f32> {
4580    let w = hmap.width;
4581    let h = hmap.height;
4582    let mut density = vec![0.0f32; w * h];
4583    for y in 0..h {
4584        for x in 0..w {
4585            let idx      = y * w + x;
4586            let altitude = hmap.get(x, y);
4587            let temp     = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
4588            let humidity = if idx < hum_map.len()  { hum_map[idx]  } else { 0.5  };
4589            let sample   = biome_sys.sample(temp, humidity, altitude);
4590            density[idx] = sample.blended_tree_density * sample.blended_grass_density;
4591        }
4592    }
4593    density
4594}
4595
4596// ============================================================
4597//  SUNLIGHT SHADOW MAP (DIRECTIONAL SHADOW APPROXIMATE)
4598// ============================================================
4599
4600/// Compute shadow intensity for each heightmap cell from a directional light
4601/// Returns 0 = fully shadowed, 1 = fully lit
4602pub fn compute_terrain_shadow_map(
4603    hmap:       &Heightmap,
4604    sun_dir:    Vec3,
4605    cell_size:  f32,
4606    height_scale: f32,
4607) -> Vec<f32> {
4608    let w = hmap.width;
4609    let h = hmap.height;
4610    let mut shadow = vec![1.0f32; w * h];
4611
4612    // Only compute shadows when sun is above horizon
4613    if sun_dir.y < 0.01 {
4614        return vec![0.0f32; w * h];
4615    }
4616
4617    // For each cell, march toward sun and check if any terrain is in the way
4618    let sun_horiz = Vec2::new(sun_dir.x, sun_dir.z);
4619    if sun_horiz.length() < 1e-6 { return shadow; } // sun straight up — no shadows
4620
4621    let sun_2d     = sun_horiz.normalize();
4622    let slope_inv  = sun_dir.y / sun_horiz.length();
4623    let step_dist  = cell_size;
4624    let max_steps  = ((w + h) / 2) as usize;
4625
4626    for y in 0..h {
4627        for x in 0..w {
4628            let base_h = hmap.get(x, y) * height_scale;
4629            let mut cur_x = x as f32;
4630            let mut cur_z = y as f32;
4631            let mut shadowed = false;
4632
4633            for step in 1..max_steps {
4634                cur_x += sun_2d.x * step_dist / cell_size;
4635                cur_z += sun_2d.y * step_dist / cell_size; // sun_2d.y maps to Z axis
4636                if cur_x < 0.0 || cur_z < 0.0 || cur_x >= w as f32 || cur_z >= h as f32 { break; }
4637
4638                let ux = (cur_x / w as f32).clamp(0.0, 1.0);
4639                let uz = (cur_z / h as f32).clamp(0.0, 1.0);
4640                let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
4641                let expected_h = base_h + step as f32 * step_dist * slope_inv;
4642
4643                if terrain_h > expected_h {
4644                    shadowed = true;
4645                    break;
4646                }
4647            }
4648
4649            shadow[y * w + x] = if shadowed { 0.0 } else { 1.0 };
4650        }
4651    }
4652
4653    shadow
4654}
4655
4656// ============================================================
4657//  COLOR RAMP FOR VISUALIZATION
4658// ============================================================
4659
4660#[derive(Clone, Debug)]
4661pub struct ColorRamp {
4662    pub stops: Vec<(f32, Vec3)>, // (position 0..1, color)
4663}
4664
4665impl ColorRamp {
4666    pub fn terrain_default() -> Self {
4667        ColorRamp {
4668            stops: vec![
4669                (0.00, Vec3::new(0.05, 0.15, 0.60)), // deep water
4670                (0.18, Vec3::new(0.10, 0.40, 0.80)), // shallow water
4671                (0.22, Vec3::new(0.90, 0.85, 0.65)), // sand/beach
4672                (0.30, Vec3::new(0.30, 0.55, 0.15)), // lowland grass
4673                (0.50, Vec3::new(0.20, 0.45, 0.10)), // forest
4674                (0.65, Vec3::new(0.45, 0.40, 0.30)), // highland
4675                (0.80, Vec3::new(0.55, 0.50, 0.45)), // rock
4676                (0.92, Vec3::new(0.80, 0.85, 0.90)), // snow line
4677                (1.00, Vec3::new(0.95, 0.97, 1.00)), // peak snow
4678            ],
4679        }
4680    }
4681
4682    pub fn sample(&self, t: f32) -> Vec3 {
4683        let t = t.clamp(0.0, 1.0);
4684        if self.stops.is_empty() { return Vec3::ZERO; }
4685        if self.stops.len() == 1 { return self.stops[0].1; }
4686
4687        for i in 0..self.stops.len() - 1 {
4688            let (ta, ca) = self.stops[i];
4689            let (tb, cb) = self.stops[i + 1];
4690            if t >= ta && t <= tb {
4691                let local_t = (t - ta) / (tb - ta);
4692                let st      = smoothstep(0.0, 1.0, local_t);
4693                return ca + (cb - ca) * st;
4694            }
4695        }
4696
4697        self.stops.last().unwrap().1
4698    }
4699}
4700
4701// ============================================================
4702//  EDITOR VIEWPORT CAMERA
4703// ============================================================
4704
4705#[derive(Clone, Debug)]
4706pub struct EditorCamera {
4707    pub position:    Vec3,
4708    pub target:      Vec3,
4709    pub up:          Vec3,
4710    pub fov_deg:     f32,
4711    pub aspect:      f32,
4712    pub near:        f32,
4713    pub far:         f32,
4714    pub orbit_yaw:   f32,
4715    pub orbit_pitch: f32,
4716    pub orbit_dist:  f32,
4717}
4718
4719impl EditorCamera {
4720    pub fn new(aspect: f32) -> Self {
4721        EditorCamera {
4722            position:    Vec3::new(512.0, 200.0, 512.0),
4723            target:      Vec3::new(512.0, 0.0, 512.0),
4724            up:          Vec3::Y,
4725            fov_deg:     60.0,
4726            aspect,
4727            near:        1.0,
4728            far:         50000.0,
4729            orbit_yaw:   -30.0,
4730            orbit_pitch: 45.0,
4731            orbit_dist:  600.0,
4732        }
4733    }
4734
4735    pub fn view_matrix(&self) -> Mat4 {
4736        Mat4::look_at_rh(self.position, self.target, self.up)
4737    }
4738
4739    pub fn proj_matrix(&self) -> Mat4 {
4740        Mat4::perspective_rh(self.fov_deg * DEG2RAD, self.aspect, self.near, self.far)
4741    }
4742
4743    pub fn view_proj(&self) -> Mat4 {
4744        self.proj_matrix() * self.view_matrix()
4745    }
4746
4747    pub fn frustum(&self) -> Frustum {
4748        Frustum::from_view_proj(self.view_proj())
4749    }
4750
4751    /// Update camera position from orbit parameters
4752    pub fn update_orbit(&mut self) {
4753        let yaw_rad   = self.orbit_yaw   * DEG2RAD;
4754        let pitch_rad = self.orbit_pitch * DEG2RAD;
4755
4756        let x = self.orbit_dist * pitch_rad.cos() * yaw_rad.sin();
4757        let y = self.orbit_dist * pitch_rad.sin();
4758        let z = self.orbit_dist * pitch_rad.cos() * yaw_rad.cos();
4759
4760        self.position = self.target + Vec3::new(x, y, z);
4761    }
4762
4763    pub fn orbit(&mut self, delta_yaw: f32, delta_pitch: f32) {
4764        self.orbit_yaw   += delta_yaw;
4765        self.orbit_pitch  = (self.orbit_pitch + delta_pitch).clamp(5.0, 85.0);
4766        self.update_orbit();
4767    }
4768
4769    pub fn zoom(&mut self, delta: f32) {
4770        self.orbit_dist = (self.orbit_dist + delta).clamp(10.0, 10000.0);
4771        self.update_orbit();
4772    }
4773
4774    pub fn pan(&mut self, delta: Vec3) {
4775        self.target   += delta;
4776        self.position += delta;
4777    }
4778
4779    /// Compute a ray from the camera through a screen-space point (ndc -1..1)
4780    pub fn screen_to_ray(&self, ndc_x: f32, ndc_y: f32) -> (Vec3, Vec3) {
4781        let inv_vp = self.view_proj().inverse();
4782        let near_ndc = Vec4::new(ndc_x, ndc_y, -1.0, 1.0);
4783        let far_ndc  = Vec4::new(ndc_x, ndc_y,  1.0, 1.0);
4784
4785        let near_world = inv_vp * near_ndc;
4786        let far_world  = inv_vp * far_ndc;
4787
4788        let nw = Vec3::new(near_world.x / near_world.w, near_world.y / near_world.w, near_world.z / near_world.w);
4789        let fw = Vec3::new(far_world.x  / far_world.w,  far_world.y  / far_world.w,  far_world.z  / far_world.w);
4790
4791        let dir = (fw - nw).normalize();
4792        (nw, dir)
4793    }
4794}
4795
4796// ============================================================
4797//  TERRAIN PAINTER (multi-layer blending)
4798// ============================================================
4799
4800#[derive(Clone, Debug)]
4801pub struct TerrainLayer {
4802    pub id:        usize,
4803    pub name:      String,
4804    pub weight_map: Vec<f32>,   // same dimensions as heightmap
4805    pub tiling:    f32,
4806    pub normal_strength: f32,
4807}
4808
4809impl TerrainLayer {
4810    pub fn new(id: usize, name: &str, width: usize, height: usize) -> Self {
4811        TerrainLayer {
4812            id,
4813            name: name.to_string(),
4814            weight_map: vec![0.0; width * height],
4815            tiling: 10.0,
4816            normal_strength: 1.0,
4817        }
4818    }
4819
4820    pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, width: usize, height: usize) {
4821        let r  = brush.radius.ceil() as i32;
4822        let cx_i = cx as i32;
4823        let cy_i = cy as i32;
4824        let x0 = (cx_i - r).max(0) as usize;
4825        let y0 = (cy_i - r).max(0) as usize;
4826        let x1 = (cx_i + r).min(width  as i32 - 1) as usize;
4827        let y1 = (cy_i + r).min(height as i32 - 1) as usize;
4828
4829        for y in y0..=y1 {
4830            for x in x0..=x1 {
4831                let dx   = x as f32 - cx;
4832                let dy   = y as f32 - cy;
4833                let dist = (dx*dx + dy*dy).sqrt();
4834                if dist > brush.radius { continue; }
4835                let w = brush.weight_at_radius(dist);
4836                let idx = y * width + x;
4837                self.weight_map[idx] = (self.weight_map[idx] + w).clamp(0.0, 1.0);
4838            }
4839        }
4840    }
4841}
4842
4843/// Normalize paint weights across layers so they sum to 1
4844pub fn normalize_paint_weights(layers: &mut [TerrainLayer], width: usize, height: usize) {
4845    for i in 0..width * height {
4846        let total: f32 = layers.iter().map(|l| l.weight_map[i]).sum();
4847        if total > 1e-6 {
4848            for l in layers.iter_mut() {
4849                l.weight_map[i] /= total;
4850            }
4851        }
4852    }
4853}
4854
4855// ============================================================
4856//  WEATHER EFFECTS (PARTICLE RAIN/SNOW SIMULATION)
4857// ============================================================
4858
4859#[derive(Clone, Debug)]
4860pub struct Particle {
4861    pub position: Vec3,
4862    pub velocity: Vec3,
4863    pub life:     f32,
4864    pub max_life: f32,
4865    pub size:     f32,
4866    pub color:    Vec4,
4867}
4868
4869impl Particle {
4870    pub fn lifetime_t(&self) -> f32 { 1.0 - self.life / self.max_life }
4871}
4872
4873pub struct ParticleSystem {
4874    pub particles: Vec<Particle>,
4875    pub max_count: usize,
4876    rng:           LcgRng,
4877}
4878
4879impl ParticleSystem {
4880    pub fn new(max_count: usize, seed: u64) -> Self {
4881        ParticleSystem { particles: Vec::with_capacity(max_count), max_count, rng: LcgRng::new(seed) }
4882    }
4883
4884    pub fn emit_rain(&mut self, camera_pos: Vec3, wind: Vec2, density: f32) {
4885        let count = (density * self.max_count as f32) as usize;
4886        let existing = self.particles.len();
4887        let to_emit  = (count.saturating_sub(existing)).min(500);
4888
4889        for _ in 0..to_emit {
4890            let rx = camera_pos.x + (self.rng.next_f32() - 0.5) * 200.0;
4891            let rz = camera_pos.z + (self.rng.next_f32() - 0.5) * 200.0;
4892            let ry = camera_pos.y + 80.0 + self.rng.next_f32() * 40.0;
4893
4894            self.particles.push(Particle {
4895                position: Vec3::new(rx, ry, rz),
4896                velocity: Vec3::new(wind.x * 0.3, -10.0 - self.rng.next_f32() * 5.0, wind.y * 0.3),
4897                life:     0.5 + self.rng.next_f32() * 2.0,
4898                max_life: 2.5,
4899                size:     0.02 + self.rng.next_f32() * 0.01,
4900                color:    Vec4::new(0.6, 0.7, 0.9, 0.6),
4901            });
4902        }
4903    }
4904
4905    pub fn emit_snow(&mut self, camera_pos: Vec3, wind: Vec2, density: f32) {
4906        let count    = (density * self.max_count as f32) as usize;
4907        let existing = self.particles.len();
4908        let to_emit  = (count.saturating_sub(existing)).min(300);
4909
4910        for _ in 0..to_emit {
4911            let rx = camera_pos.x + (self.rng.next_f32() - 0.5) * 300.0;
4912            let rz = camera_pos.z + (self.rng.next_f32() - 0.5) * 300.0;
4913            let ry = camera_pos.y + 60.0 + self.rng.next_f32() * 30.0;
4914
4915            self.particles.push(Particle {
4916                position: Vec3::new(rx, ry, rz),
4917                velocity: Vec3::new(
4918                    wind.x * 0.5 + (self.rng.next_f32() - 0.5) * 0.5,
4919                    -1.5 - self.rng.next_f32(),
4920                    wind.y * 0.5 + (self.rng.next_f32() - 0.5) * 0.5,
4921                ),
4922                life:     3.0 + self.rng.next_f32() * 4.0,
4923                max_life: 7.0,
4924                size:     0.05 + self.rng.next_f32() * 0.08,
4925                color:    Vec4::new(0.95, 0.97, 1.0, 0.8),
4926            });
4927        }
4928    }
4929
4930    pub fn update(&mut self, dt: f32, gravity: f32) {
4931        self.particles.retain_mut(|p| {
4932            p.velocity.y -= gravity * dt;
4933            p.position    += p.velocity * dt;
4934            p.life        -= dt;
4935            p.life > 0.0
4936        });
4937    }
4938
4939    pub fn count(&self) -> usize { self.particles.len() }
4940}
4941
4942// ============================================================
4943//  GRID SNAPPING & COORDINATE HELPERS
4944// ============================================================
4945
4946pub fn snap_to_grid(pos: Vec3, grid_size: f32) -> Vec3 {
4947    Vec3::new(
4948        (pos.x / grid_size).round() * grid_size,
4949        (pos.y / grid_size).round() * grid_size,
4950        (pos.z / grid_size).round() * grid_size,
4951    )
4952}
4953
4954pub fn snap_to_terrain(pos: Vec3, hmap: &Heightmap, cell_size: f32, height_scale: f32) -> Vec3 {
4955    let gx = pos.x / cell_size;
4956    let gz = pos.z / cell_size;
4957    let ux = (gx / hmap.width  as f32).clamp(0.0, 1.0);
4958    let uz = (gz / hmap.height as f32).clamp(0.0, 1.0);
4959    let h  = hmap.sample_bilinear(ux, uz) * height_scale;
4960    Vec3::new(pos.x, h, pos.z)
4961}
4962
4963pub fn world_to_uv(pos: Vec3, world_width: f32, world_depth: f32) -> Vec2 {
4964    Vec2::new(
4965        (pos.x / world_width).clamp(0.0, 1.0),
4966        (pos.z / world_depth).clamp(0.0, 1.0),
4967    )
4968}
4969
4970// ============================================================
4971//  SCENE GRAPH PLACEHOLDER (editor objects)
4972// ============================================================
4973
4974#[derive(Clone, Debug)]
4975pub enum EditorObjectKind {
4976    SpawnPoint,
4977    Trigger { radius: f32 },
4978    LightProbe { radius: f32 },
4979    NavigationMarker,
4980    CustomMarker { label: String },
4981}
4982
4983#[derive(Clone, Debug)]
4984pub struct EditorObject {
4985    pub id:        u32,
4986    pub name:      String,
4987    pub transform: Mat4,
4988    pub kind:      EditorObjectKind,
4989    pub visible:   bool,
4990    pub locked:    bool,
4991    pub selected:  bool,
4992}
4993
4994impl EditorObject {
4995    pub fn new(id: u32, name: &str, pos: Vec3, kind: EditorObjectKind) -> Self {
4996        EditorObject {
4997            id,
4998            name:      name.to_string(),
4999            transform: Mat4::from_translation(pos),
5000            kind,
5001            visible:   true,
5002            locked:    false,
5003            selected:  false,
5004        }
5005    }
5006
5007    pub fn position(&self) -> Vec3 {
5008        Vec3::new(self.transform.w_axis.x, self.transform.w_axis.y, self.transform.w_axis.z)
5009    }
5010}
5011
5012// ============================================================
5013//  MINIMAP / OVERVIEW MAP
5014// ============================================================
5015
5016pub struct Minimap {
5017    pub width:   usize,
5018    pub height:  usize,
5019    pub pixels:  Vec<Vec4>,  // RGBA
5020    pub ramp:    ColorRamp,
5021    pub dirty:   bool,
5022}
5023
5024impl Minimap {
5025    pub fn new(width: usize, height: usize) -> Self {
5026        Minimap {
5027            width,
5028            height,
5029            pixels: vec![Vec4::ZERO; width * height],
5030            ramp:   ColorRamp::terrain_default(),
5031            dirty:  true,
5032        }
5033    }
5034
5035    /// Render the minimap from the heightmap + biome data
5036    pub fn update(&mut self, hmap: &Heightmap, ao_map: Option<&[f32]>, sea_level: f32) {
5037        let tw = hmap.width;
5038        let th = hmap.height;
5039
5040        for y in 0..self.height {
5041            for x in 0..self.width {
5042                let u  = x as f32 / self.width  as f32;
5043                let v  = y as f32 / self.height as f32;
5044                let h  = hmap.sample_bilinear(u, v);
5045                let mut color = self.ramp.sample(h);
5046
5047                // Apply AO if available
5048                if let Some(ao) = ao_map {
5049                    let ax  = (u * (tw - 1) as f32) as usize;
5050                    let ay  = (v * (th - 1) as f32) as usize;
5051                    let ao_val = ao[ay * tw + ax];
5052                    color *= ao_val * 0.7 + 0.3;
5053                }
5054
5055                self.pixels[y * self.width + x] = Vec4::new(color.x, color.y, color.z, 1.0);
5056            }
5057        }
5058        self.dirty = false;
5059    }
5060
5061    /// Draw a marker at a world position
5062    pub fn draw_marker(&mut self, world_x: f32, world_z: f32, world_w: f32, world_h: f32, color: Vec4) {
5063        let u  = (world_x / world_w).clamp(0.0, 1.0);
5064        let v  = (world_z / world_h).clamp(0.0, 1.0);
5065        let px = (u * (self.width  - 1) as f32) as usize;
5066        let py = (v * (self.height - 1) as f32) as usize;
5067
5068        let radius = 3usize;
5069        let x0 = px.saturating_sub(radius);
5070        let y0 = py.saturating_sub(radius);
5071        let x1 = (px + radius).min(self.width  - 1);
5072        let y1 = (py + radius).min(self.height - 1);
5073        for cy in y0..=y1 {
5074            for cx in x0..=x1 {
5075                let dx = cx as i32 - px as i32;
5076                let dy = cy as i32 - py as i32;
5077                if dx*dx + dy*dy <= (radius*radius) as i32 {
5078                    self.pixels[cy * self.width + cx] = color;
5079                }
5080            }
5081        }
5082    }
5083}
5084
5085// ============================================================
5086//  EDITOR STATE SNAPSHOT (for save/restore)
5087// ============================================================
5088
5089#[derive(Clone, Debug)]
5090pub struct EditorStateSnapshot {
5091    pub heightmap_data:  Vec<f32>,
5092    pub foliage_count:   usize,
5093    pub lake_count:      usize,
5094    pub river_count:     usize,
5095    pub road_count:      usize,
5096    pub utc_hour:        f64,
5097    pub day_of_year:     f64,
5098    pub weather_state:   WeatherState,
5099    pub sea_level:       f32,
5100    pub world_name:      String,
5101}
5102
5103impl WorldEditor {
5104    pub fn snapshot(&self) -> EditorStateSnapshot {
5105        EditorStateSnapshot {
5106            heightmap_data:  self.heightmap.data.clone(),
5107            foliage_count:   self.foliage.len(),
5108            lake_count:      self.lakes.len(),
5109            river_count:     self.rivers.len(),
5110            road_count:      self.road_network.segments.len(),
5111            utc_hour:        self.utc_hour,
5112            day_of_year:     self.day_of_year,
5113            weather_state:   self.weather.current.state,
5114            sea_level:       self.sea_level,
5115            world_name:      self.world_name.clone(),
5116        }
5117    }
5118
5119    pub fn restore_heightmap(&mut self, snapshot: &EditorStateSnapshot) {
5120        if snapshot.heightmap_data.len() == self.heightmap.data.len() {
5121            self.heightmap.data = snapshot.heightmap_data.clone();
5122            self.heightmap.recompute_minmax();
5123            self.heightmap_dirty = true;
5124        }
5125    }
5126}
5127
5128// ============================================================
5129//  STRESS TESTS / BENCHMARKING HELPERS
5130// ============================================================
5131
5132pub fn benchmark_noise(width: usize, height: usize, params: &FbmParams) -> f64 {
5133    let mut sum = 0.0f64;
5134    for y in 0..height {
5135        for x in 0..width {
5136            let nx = x as f32 / width  as f32;
5137            let ny = y as f32 / height as f32;
5138            sum += fbm_2d(nx, ny, params) as f64;
5139        }
5140    }
5141    sum / (width * height) as f64
5142}
5143
5144pub fn benchmark_erosion(size: usize) -> Heightmap {
5145    let mut hmap = Heightmap::new(size, size);
5146    let params = FbmParams::default_terrain();
5147    hmap.generate_fbm(&params, Vec2::ZERO);
5148    let ep = ErosionParams { num_particles: 10_000, ..Default::default() };
5149    hydraulic_erosion(&mut hmap, &ep);
5150    hmap
5151}
5152
5153pub fn benchmark_pathfinding(hmap: &Heightmap) -> Option<Vec<GridNode>> {
5154    let w = hmap.width  as i32;
5155    let h = hmap.height as i32;
5156    let start = GridNode::new(1, 1);
5157    let goal  = GridNode::new(w - 2, h - 2);
5158    astar_path(hmap, start, goal, &RoadCostParams::default())
5159}
5160
5161// ============================================================
5162//  TRAIT IMPLEMENTATIONS
5163// ============================================================
5164
5165impl Default for WorldEditor {
5166    fn default() -> Self {
5167        WorldEditor::new(512, 512, 1.0)
5168    }
5169}
5170
5171impl std::fmt::Display for WeatherState {
5172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5173        write!(f, "{}", self.name())
5174    }
5175}
5176
5177impl std::fmt::Display for BiomeId {
5178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5179        write!(f, "{:?}", self)
5180    }
5181}
5182
5183impl std::fmt::Display for WorldStats {
5184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5185        write!(f,
5186            "World: {}x{} cells | Land: {} | Ocean: {} | Rivers: {} | Lakes: {} | Roads: {} segs ({:.0}m) | Foliage: {}",
5187            (self.total_cells as f32).sqrt() as usize,
5188            (self.total_cells as f32).sqrt() as usize,
5189            self.land_cells,
5190            self.ocean_cells,
5191            self.river_count,
5192            self.lake_count,
5193            self.road_segments,
5194            self.road_total_length,
5195            self.foliage_count,
5196        )
5197    }
5198}
5199
5200impl Clone for EditAction {
5201    fn clone(&self) -> Self {
5202        match self {
5203            EditAction::SetHeightRegion { x, y, width, height, old_data, new_data } =>
5204                EditAction::SetHeightRegion { x: *x, y: *y, width: *width, height: *height, old_data: old_data.clone(), new_data: new_data.clone() },
5205            EditAction::PlaceFoliageInstances { instances, indices } =>
5206                EditAction::PlaceFoliageInstances { instances: instances.clone(), indices: indices.clone() },
5207            EditAction::RemoveFoliageInstances { indices, instances } =>
5208                EditAction::RemoveFoliageInstances { indices: indices.clone(), instances: instances.clone() },
5209            EditAction::AddRoadSegment { segment_id, segment } =>
5210                EditAction::AddRoadSegment { segment_id: *segment_id, segment: segment.clone() },
5211            EditAction::RemoveRoadSegment { segment_id, segment } =>
5212                EditAction::RemoveRoadSegment { segment_id: *segment_id, segment: segment.clone() },
5213            EditAction::SetBiomeOverride { x, y, old_biome, new_biome } =>
5214                EditAction::SetBiomeOverride { x: *x, y: *y, old_biome: *old_biome, new_biome: *new_biome },
5215            EditAction::AddWaterBody { lake, index } =>
5216                EditAction::AddWaterBody { lake: lake.clone(), index: *index },
5217            EditAction::RemoveWaterBody { lake, index } =>
5218                EditAction::RemoveWaterBody { lake: lake.clone(), index: *index },
5219            EditAction::CompoundAction { actions, description } =>
5220                EditAction::CompoundAction { actions: actions.clone(), description: description.clone() },
5221        }
5222    }
5223}
5224
5225// ============================================================
5226//  ROCK PLACEMENT SYSTEM
5227// ============================================================
5228
5229#[derive(Clone, Debug)]
5230pub struct RockInstance {
5231    pub position:  Vec3,
5232    pub rotation:  Quat,
5233    pub scale:     Vec3,
5234    pub rock_type: u8,
5235}
5236
5237pub fn place_rocks(
5238    hmap:      &Heightmap,
5239    cell_size: f32,
5240    min_slope: f32,  // radians — rocks appear on steep slopes
5241    max_alt:   f32,
5242    density:   f32,
5243    seed:      u64,
5244) -> Vec<RockInstance> {
5245    let w   = hmap.width  as f32;
5246    let h   = hmap.height as f32;
5247    let min_dist = 3.0 + (1.0 - density) * 7.0;
5248    let candidates = poisson_disk_2d(w, h, min_dist, 30, seed);
5249    let mut rng = LcgRng::new(seed ^ 0xB00B);
5250    let mut result = Vec::new();
5251
5252    for pos in &candidates {
5253        let ux = (pos.x / w).clamp(0.0, 1.0);
5254        let uy = (pos.y / h).clamp(0.0, 1.0);
5255        let alt   = hmap.sample_bilinear(ux, uy);
5256        if alt > max_alt { continue; }
5257
5258        let xi = pos.x as usize;
5259        let yi = pos.y as usize;
5260        let xi_c = xi.min(hmap.width  - 1);
5261        let yi_c = yi.min(hmap.height - 1);
5262        let slope = hmap.slope_at(xi_c, yi_c, cell_size);
5263        if slope < min_slope { continue; }
5264
5265        let angle = rng.next_f32() * TWO_PI;
5266        let tilt  = slope * 0.5;
5267        let rot   = Quat::from_rotation_y(angle) * Quat::from_rotation_x(tilt);
5268
5269        let sv  = 0.5 + rng.next_f32() * 2.0;
5270        let sxz = 0.7 + rng.next_f32() * 0.6;
5271        let scale = Vec3::new(sv * sxz, sv, sv * sxz);
5272
5273        let world_y = alt * 500.0;
5274        result.push(RockInstance {
5275            position:  Vec3::new(pos.x * cell_size, world_y, pos.y * cell_size),
5276            rotation:  rot,
5277            scale,
5278            rock_type: (rng.next_f32() * 8.0) as u8,
5279        });
5280    }
5281
5282    result
5283}
5284
5285// ============================================================
5286//  THERMAL GRADIENT MAP (for snow accumulation)
5287// ============================================================
5288
5289pub fn compute_snow_accumulation(
5290    hmap:        &Heightmap,
5291    temp_map:    &[f32],
5292    snow_line:   f32,        // normalised altitude above which snow can form
5293    temp_thresh: f32,        // temperature threshold for snow (°C)
5294) -> Vec<f32> {
5295    let w = hmap.width;
5296    let h = hmap.height;
5297    let mut snow_map = vec![0.0f32; w * h];
5298
5299    for y in 0..h {
5300        for x in 0..w {
5301            let idx = y * w + x;
5302            let alt  = hmap.get(x, y);
5303            let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
5304            if alt >= snow_line && temp <= temp_thresh {
5305                // More snow at higher altitudes and colder temperatures
5306                let alt_factor  = ((alt - snow_line) / (1.0 - snow_line)).clamp(0.0, 1.0);
5307                let temp_factor = ((temp_thresh - temp) / 30.0).clamp(0.0, 1.0);
5308                snow_map[idx]   = (alt_factor * 0.6 + temp_factor * 0.4).clamp(0.0, 1.0);
5309            }
5310        }
5311    }
5312    snow_map
5313}
5314
5315// ============================================================
5316//  LARGE CONSTANT DATA TABLES
5317// ============================================================
5318
5319/// Noise permutation indices for higher-quality scrambling (second set)
5320pub const PERM2: [u8; 256] = [
5321    198, 11,  59, 119, 138,  22,  40, 216,  69, 175,  89, 201,  90, 142,  76, 250,
5322    220,  37, 104,  82, 127, 248,  13,  99, 179,  42, 222, 194, 230, 106,  26, 155,
5323     36,  83,  18,  72,  67,  17, 162, 167, 147, 137,  50, 133,  23, 213,  80, 125,
5324    200, 192,  29, 180,  10, 218, 146, 183, 234,  60, 215,  38, 244, 239, 169,  91,
5325     34, 190, 185, 171,  27, 203, 240, 254, 158,  52, 249, 153, 214,  54,  47, 207,
5326    140,  55, 102, 182, 111, 170, 232, 101,  96, 173, 166, 136,  43,  20,  88, 115,
5327    129, 156, 126, 233, 221,  74,  62,  48,  86,  35, 109, 224, 165, 131, 187, 246,
5328     71,  63, 141, 108,  24, 148,  45,  79, 121, 210, 144, 196,  93, 228,  28,   9,
5329    177, 118, 110, 120, 243,  41, 251, 107,  49, 117, 160,  85, 247,  65,   6,  64,
5330    189,  58, 132, 235,  75,   7, 163, 205, 188,   3, 139, 197, 208, 150, 116, 168,
5331     15,  95,  16, 151, 217,  77,  66, 152, 204,  57, 199,  12, 161, 184,  81,  31,
5332    229, 211,  53,  39,  78, 206, 236,   4,  46,  25, 227, 241, 174, 159,  14, 253,
5333    154, 191,  73, 238, 135, 209, 181,  33, 226, 123,  68,  32, 130, 193,  21,  84,
5334    237, 123, 145, 172,  44,   5, 176, 143, 100, 219, 114,  56, 252, 149,  92, 245,
5335    103, 157,   2,   8,  19,  97, 122, 202, 134, 255, 112,  30,  70, 186,  61,  98,
5336    105,  94, 113,  87, 231, 178, 164, 124,  51,   1, 212,  76, 128, 242, 223, 195,
5337];
5338
5339/// Biome temperature-humidity classification lookup string (for debugging)
5340pub const BIOME_CLASSIFICATION_TABLE: &str = "\
5341T >24 H >0.80 -> Tropical Rainforest\n\
5342T >24 H >0.50 -> Tropical Savanna\n\
5343T >24 H >0.25 -> Xeric Shrubland\n\
5344T >24 H     * -> Hot Desert\n\
5345T >10 H >0.70 -> Temperate Rainforest\n\
5346T >10 H >0.50 -> Temperate Deciduous\n\
5347T >10 H >0.28 -> Mediterranean Shrub\n\
5348T >10 H     * -> Xeric Shrubland\n\
5349T  >0 H >0.65 -> Boreal Forest\n\
5350T  >0 H >0.40 -> Temperate Grassland\n\
5351T  >0 H     * -> Cold Desert\n\
5352T>-10 H >0.50 -> Taiga Spruce\n\
5353T>-10 H     * -> Tundra\n\
5354T    * H >0.30 -> Tundra\n\
5355T    * H     * -> Arctic Desert\n\
5356ALT >0.88      -> Polar Ice Cap\n\
5357ALT >0.75      -> Alpine Tundra\n\
5358ALT >0.62      -> Alpine Meadow\n";
5359
5360// ============================================================
5361//  ADDITIONAL WORLD EDITOR METHODS
5362// ============================================================
5363
5364impl WorldEditor {
5365    /// Generate a complete terrain from a user-provided seed value
5366    pub fn generate_from_seed(&mut self, seed: u64) {
5367        self.master_seed = seed;
5368        let mut rng = LcgRng::new(seed);
5369        self.terrain_fbm_params.octaves    = 7 + (rng.next_f32() * 3.0) as usize;
5370        self.terrain_fbm_params.lacunarity = 1.8 + rng.next_f32() * 0.5;
5371        self.terrain_fbm_params.gain       = 0.45 + rng.next_f32() * 0.15;
5372        self.warp_strength                 = 0.2  + rng.next_f32() * 0.4;
5373        self.sea_level                     = 0.15 + rng.next_f32() * 0.15;
5374        self.generate_terrain();
5375        self.apply_erosion();
5376        self.apply_thermal_erosion(3, 30.0 + rng.next_f32() * 15.0);
5377        self.generate_climate();
5378        self.generate_rivers(5 + (rng.next_f32() * 10.0) as usize);
5379        self.generate_lakes(3 + (rng.next_f32() * 7.0) as usize, 0.02);
5380        self.generate_shore();
5381        self.compute_stats();
5382    }
5383
5384    /// Get height at a world position (x, z in world units)
5385    pub fn height_at_world(&self, x: f32, z: f32) -> f32 {
5386        let ux = (x / (self.heightmap.width  as f32 * self.cell_size)).clamp(0.0, 1.0);
5387        let uz = (z / (self.heightmap.height as f32 * self.cell_size)).clamp(0.0, 1.0);
5388        self.heightmap.sample_bilinear(ux, uz) * self.height_scale
5389    }
5390
5391    /// Get surface normal at a world position
5392    pub fn normal_at_world(&self, x: f32, z: f32) -> Vec3 {
5393        let gx = (x / self.cell_size) as usize;
5394        let gz = (z / self.cell_size) as usize;
5395        let gx_c = gx.min(self.heightmap.width  - 1);
5396        let gz_c = gz.min(self.heightmap.height - 1);
5397        self.heightmap.normal_at(gx_c, gz_c, self.cell_size)
5398    }
5399
5400    /// Check if a world position is underwater
5401    pub fn is_underwater(&self, x: f32, z: f32) -> bool {
5402        let alt = self.height_at_world(x, z) / self.height_scale;
5403        alt <= self.sea_level
5404    }
5405
5406    /// Get biome at a world position
5407    pub fn biome_at_world(&self, x: f32, z: f32) -> BiomeId {
5408        let gx = ((x / self.cell_size) as usize).min(self.heightmap.width  - 1);
5409        let gz = ((z / self.cell_size) as usize).min(self.heightmap.height - 1);
5410        let idx  = gz * self.heightmap.width + gx;
5411        let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
5412        let hum  = if idx < self.humidity_map.len()    { self.humidity_map[idx]    } else { 0.5  };
5413        let alt  = self.heightmap.get(gx, gz);
5414        BiomeDescriptor::classify_point(temp, hum, alt)
5415    }
5416
5417    /// Resize the world (resample heightmap to new dimensions)
5418    pub fn resize_world(&mut self, new_width: usize, new_height: usize) {
5419        let mut new_hmap = Heightmap::new(new_width, new_height);
5420        for y in 0..new_height {
5421            for x in 0..new_width {
5422                let u = x as f32 / (new_width  - 1) as f32;
5423                let v = y as f32 / (new_height - 1) as f32;
5424                let h = self.heightmap.sample_bilinear(u, v);
5425                new_hmap.set(x, y, h);
5426            }
5427        }
5428        new_hmap.recompute_minmax();
5429        self.heightmap        = new_hmap;
5430        self.temperature_map  = vec![15.0; new_width * new_height];
5431        self.humidity_map     = vec![0.5;  new_width * new_height];
5432        self.climate_dirty    = true;
5433        self.heightmap_dirty  = true;
5434        self.foliage.clear();
5435        self.foliage_dirty    = true;
5436    }
5437
5438    /// Apply a heightmap from external data
5439    pub fn import_heightmap(&mut self, data: &[f32], width: usize, height: usize) {
5440        if data.len() != width * height { return; }
5441        self.heightmap = Heightmap {
5442            width,
5443            height,
5444            data:  data.to_vec(),
5445            min_h: 0.0,
5446            max_h: 1.0,
5447        };
5448        self.heightmap.recompute_minmax();
5449        self.heightmap.normalize_to_01();
5450        self.heightmap_dirty = true;
5451        self.climate_dirty   = true;
5452    }
5453
5454    /// Export heightmap as 16-bit grayscale image bytes (big-endian per pixel)
5455    pub fn export_heightmap_u16(&self) -> Vec<u8> {
5456        let mut out = Vec::with_capacity(self.heightmap.data.len() * 2);
5457        for &h in &self.heightmap.data {
5458            let v = (h.clamp(0.0, 1.0) * 65535.0) as u16;
5459            out.push((v >> 8) as u8);
5460            out.push((v & 0xFF) as u8);
5461        }
5462        out
5463    }
5464
5465    /// Export heightmap as 8-bit grayscale image bytes
5466    pub fn export_heightmap_u8(&self) -> Vec<u8> {
5467        self.heightmap.data.iter()
5468            .map(|&h| (h.clamp(0.0, 1.0) * 255.0) as u8)
5469            .collect()
5470    }
5471
5472    /// Compute horizon angle at a point (used for ambient lighting)
5473    pub fn horizon_angle_at(&self, x: usize, y: usize, direction: f32, max_dist: f32) -> f32 {
5474        let base_h = self.heightmap.get(x, y) * self.height_scale;
5475        let dx     = direction.cos();
5476        let dz     = direction.sin();
5477        let steps  = (max_dist / self.cell_size) as usize;
5478        let mut max_elev = 0.0f32;
5479
5480        for step in 1..=steps {
5481            let t   = step as f32 * self.cell_size;
5482            let nx  = x as f32 + dx * t / self.cell_size;
5483            let nz  = y as f32 + dz * t / self.cell_size;
5484            let w   = self.heightmap.width  as f32;
5485            let h   = self.heightmap.height as f32;
5486            if nx < 0.0 || nz < 0.0 || nx >= w || nz >= h { break; }
5487            let ux  = nx / w;
5488            let uz  = nz / h;
5489            let nh  = self.heightmap.sample_bilinear(ux, uz) * self.height_scale;
5490            let elev = ((nh - base_h) / t).atan();
5491            if elev > max_elev { max_elev = elev; }
5492        }
5493        max_elev
5494    }
5495}
5496
5497// ============================================================
5498//  FINAL UTILITY FUNCTIONS
5499// ============================================================
5500
5501/// Compute approximate visual radius of the world at a given altitude
5502pub fn visual_radius_from_altitude(altitude_km: f32) -> f32 {
5503    // Horizon distance for a sphere: sqrt(2 * R * h + h²) in km
5504    let r = EARTH_RADIUS as f32;
5505    let h = altitude_km;
5506    (2.0 * r * h + h * h).sqrt()
5507}
5508
5509/// Convert world height in normalised units to metres
5510pub fn normalised_to_metres(h: f32, height_scale_m: f32) -> f32 {
5511    h * height_scale_m
5512}
5513
5514/// Great-circle distance between two lat/lon points (Haversine formula), returns km
5515pub fn haversine_km(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
5516    let r    = EARTH_RADIUS;
5517    let dlat = (lat2 - lat1).to_radians();
5518    let dlon = (lon2 - lon1).to_radians();
5519    let a    = (dlat / 2.0).sin().powi(2)
5520             + lat1.to_radians().cos() * lat2.to_radians().cos() * (dlon / 2.0).sin().powi(2);
5521    let c = 2.0 * a.sqrt().asin();
5522    r * c
5523}
5524
5525/// Convert temperature from Celsius to Fahrenheit
5526#[inline] pub fn c_to_f(c: f32) -> f32 { c * 1.8 + 32.0 }
5527
5528/// Convert temperature from Fahrenheit to Celsius
5529#[inline] pub fn f_to_c(f: f32) -> f32 { (f - 32.0) / 1.8 }
5530
5531/// Dew point from temperature and humidity (Magnus formula)
5532pub fn dew_point(temp_c: f32, relative_humidity: f32) -> f32 {
5533    let a = 17.27f32;
5534    let b = 237.7f32;
5535    let alpha = (a * temp_c / (b + temp_c)) + (relative_humidity.max(1e-5)).ln();
5536    b * alpha / (a - alpha)
5537}
5538
5539/// Wind chill temperature (Steadman 1971 approximation)
5540pub fn wind_chill(temp_c: f32, wind_speed_ms: f32) -> f32 {
5541    if wind_speed_ms < 1.4 || temp_c > 10.0 { return temp_c; }
5542    let v = wind_speed_ms * 3.6; // to km/h
5543    13.12 + 0.6215 * temp_c - 11.37 * v.powf(0.16) + 0.3965 * temp_c * v.powf(0.16)
5544}
5545
5546/// Heat index (Rothfusz regression)
5547pub fn heat_index(temp_c: f32, humidity: f32) -> f32 {
5548    let t = c_to_f(temp_c);
5549    let r = humidity * 100.0; // percent
5550    let hi = -42.379
5551        + 2.04901523 * t
5552        + 10.14333127 * r
5553        - 0.22475541 * t * r
5554        - 0.00683783 * t * t
5555        - 0.05481717 * r * r
5556        + 0.00122874 * t * t * r
5557        + 0.00085282 * t * r * r
5558        - 0.00000199 * t * t * r * r;
5559    f_to_c(hi)
5560}
5561
5562/// Beaufort wind scale classification
5563pub fn beaufort_scale(wind_speed_ms: f32) -> u8 {
5564    match wind_speed_ms as u32 {
5565        0         => 0,
5566        1..=2     => 1,
5567        3..=5     => 2,
5568        6..=9     => 3,
5569        10..=14   => 4,
5570        15..=21   => 5,
5571        22..=29   => 6,
5572        30..=38   => 7,
5573        39..=49   => 8,
5574        50..=61   => 9,
5575        62..=74   => 10,
5576        75..=88   => 11,
5577        _         => 12,
5578    }
5579}
5580
5581// ============================================================
5582//  EXTENDED NOISE FUNCTIONS
5583// ============================================================
5584
5585/// Value noise (simpler than Perlin, interpolates grid values)
5586pub fn value_noise_2d(x: f32, y: f32) -> f32 {
5587    let xi = x.floor() as i32;
5588    let yi = y.floor() as i32;
5589    let xf = x - xi as f32;
5590    let yf = y - yi as f32;
5591    let u  = fade(xf);
5592    let v  = fade(yf);
5593    let aa = PERM[((PERM[(xi & 255) as usize] as i32 + (yi & 255)) & 255) as usize] as f32 / 255.0;
5594    let ba = PERM[((PERM[((xi+1) & 255) as usize] as i32 + (yi & 255)) & 255) as usize] as f32 / 255.0;
5595    let ab = PERM[((PERM[(xi & 255) as usize] as i32 + ((yi+1) & 255)) & 255) as usize] as f32 / 255.0;
5596    let bb = PERM[((PERM[((xi+1) & 255) as usize] as i32 + ((yi+1) & 255)) & 255) as usize] as f32 / 255.0;
5597    lerp_f(lerp_f(aa, ba, u), lerp_f(ab, bb, u), v)
5598}
5599
5600/// Voronoi noise — returns distance to nearest feature point and feature ID
5601pub fn voronoi_noise_2d(x: f32, y: f32, jitter: f32) -> (f32, u32) {
5602    let cx = x.floor() as i32;
5603    let cy = y.floor() as i32;
5604    let mut min_dist = f32::MAX;
5605    let mut min_id   = 0u32;
5606    for dy in -2..=2i32 {
5607        for dx in -2..=2i32 {
5608            let nx = cx + dx;
5609            let ny = cy + dy;
5610            let h  = worley_hash(nx, ny);
5611            let fx = nx as f32 + jitter * ((h & 0xFFFF) as f32 / 65535.0 - 0.5) * 2.0 + 0.5;
5612            let fy = ny as f32 + jitter * (((h >> 16) & 0xFFFF) as f32 / 65535.0 - 0.5) * 2.0 + 0.5;
5613            let dist = ((fx - x) * (fx - x) + (fy - y) * (fy - y)).sqrt();
5614            if dist < min_dist { min_dist = dist; min_id = h; }
5615        }
5616    }
5617    (min_dist, min_id)
5618}
5619
5620/// Ridged multifractal noise (mountain ridges)
5621pub fn ridged_multifractal_2d(x: f32, y: f32, octaves: usize, freq: f32, lacunarity: f32, gain: f32, offset: f32) -> f32 {
5622    let mut f      = freq;
5623    let mut amp    = 1.0f32;
5624    let mut value  = 0.0f32;
5625    let mut weight = 1.0f32;
5626    for _ in 0..octaves {
5627        let n      = (offset - perlin_noise_2d(x * f, y * f).abs()).abs();
5628        let signal = n * n * weight;
5629        weight     = (signal * 2.0).clamp(0.0, 1.0);
5630        value     += signal * amp;
5631        f         *= lacunarity;
5632        amp       *= gain;
5633    }
5634    value
5635}
5636
5637// ============================================================
5638//  MESH GENERATION FROM HEIGHTMAP
5639// ============================================================
5640
5641#[derive(Clone, Debug)]
5642pub struct TerrainMesh {
5643    pub vertices:  Vec<Vec3>,
5644    pub normals:   Vec<Vec3>,
5645    pub uvs:       Vec<Vec2>,
5646    pub indices:   Vec<u32>,
5647    pub lod_level: u8,
5648}
5649
5650impl TerrainMesh {
5651    pub fn new() -> Self {
5652        TerrainMesh { vertices: Vec::new(), normals: Vec::new(), uvs: Vec::new(), indices: Vec::new(), lod_level: 0 }
5653    }
5654}
5655
5656/// Generate a mesh from a chunk of the heightmap
5657pub fn generate_terrain_mesh(
5658    hmap:        &Heightmap,
5659    chunk_x:     usize,
5660    chunk_z:     usize,
5661    chunk_size:  usize,
5662    cell_size:   f32,
5663    height_scale: f32,
5664    lod_step:    usize,
5665) -> TerrainMesh {
5666    let step  = lod_step.max(1);
5667    let x_end = (chunk_x + chunk_size).min(hmap.width  - 1);
5668    let z_end = (chunk_z + chunk_size).min(hmap.height - 1);
5669    let mut mesh = TerrainMesh::new();
5670    let mut vert_idx_map: HashMap<(usize, usize), u32> = HashMap::new();
5671
5672    let mut xz = chunk_z;
5673    while xz <= z_end {
5674        let mut xx = chunk_x;
5675        while xx <= x_end {
5676            let h    = hmap.get(xx, xz) * height_scale;
5677            let pos  = Vec3::new(xx as f32 * cell_size, h, xz as f32 * cell_size);
5678            let norm = hmap.normal_at(xx, xz, cell_size);
5679            let uv   = Vec2::new(
5680                (xx - chunk_x) as f32 / chunk_size as f32,
5681                (xz - chunk_z) as f32 / chunk_size as f32,
5682            );
5683            let idx = mesh.vertices.len() as u32;
5684            vert_idx_map.insert((xx, xz), idx);
5685            mesh.vertices.push(pos);
5686            mesh.normals.push(norm);
5687            mesh.uvs.push(uv);
5688            xx += step;
5689        }
5690        xz += step;
5691    }
5692
5693    let mut xz = chunk_z;
5694    while xz + step <= z_end {
5695        let mut xx = chunk_x;
5696        while xx + step <= x_end {
5697            let nx = (xx + step).min(x_end);
5698            let nz = (xz + step).min(z_end);
5699            if let (Some(&i00), Some(&i10), Some(&i01), Some(&i11)) = (
5700                vert_idx_map.get(&(xx, xz)),
5701                vert_idx_map.get(&(nx, xz)),
5702                vert_idx_map.get(&(xx, nz)),
5703                vert_idx_map.get(&(nx, nz)),
5704            ) {
5705                mesh.indices.extend_from_slice(&[i00, i10, i01, i10, i11, i01]);
5706            }
5707            xx += step;
5708        }
5709        xz += step;
5710    }
5711    mesh
5712}
5713
5714/// Compute per-vertex tangents for normal mapping
5715pub fn compute_tangents(mesh: &mut TerrainMesh) -> Vec<Vec3> {
5716    let mut tangents = vec![Vec3::ZERO; mesh.vertices.len()];
5717    let tri_count    = mesh.indices.len() / 3;
5718    for t in 0..tri_count {
5719        let i0 = mesh.indices[t * 3]     as usize;
5720        let i1 = mesh.indices[t * 3 + 1] as usize;
5721        let i2 = mesh.indices[t * 3 + 2] as usize;
5722        let v0 = mesh.vertices[i0];
5723        let v1 = mesh.vertices[i1];
5724        let v2 = mesh.vertices[i2];
5725        let uv0 = mesh.uvs[i0];
5726        let uv1 = mesh.uvs[i1];
5727        let uv2 = mesh.uvs[i2];
5728        let e1  = v1 - v0;
5729        let e2  = v2 - v0;
5730        let du1 = uv1.x - uv0.x;
5731        let dv1 = uv1.y - uv0.y;
5732        let du2 = uv2.x - uv0.x;
5733        let dv2 = uv2.y - uv0.y;
5734        let det = du1 * dv2 - du2 * dv1;
5735        if det.abs() < 1e-10 { continue; }
5736        let tang = (e1 * dv2 - e2 * dv1) / det;
5737        tangents[i0] += tang;
5738        tangents[i1] += tang;
5739        tangents[i2] += tang;
5740    }
5741    tangents.iter().enumerate().map(|(i, t)| {
5742        let n = mesh.normals[i];
5743        (*t - n * n.dot(*t)).normalize_or_zero()
5744    }).collect()
5745}
5746
5747// ============================================================
5748//  WIND SIMULATION ON TERRAIN
5749// ============================================================
5750
5751#[derive(Clone, Debug)]
5752pub struct WindField {
5753    pub width:      usize,
5754    pub height:     usize,
5755    pub vectors:    Vec<Vec2>,
5756    pub turbulence: Vec<f32>,
5757}
5758
5759impl WindField {
5760    pub fn new(width: usize, height: usize) -> Self {
5761        WindField { width, height, vectors: vec![Vec2::ZERO; width*height], turbulence: vec![0.0; width*height] }
5762    }
5763
5764    pub fn generate_from_terrain(hmap: &Heightmap, base_wind: Vec2, turbulence_strength: f32, seed: u64) -> Self {
5765        let w = hmap.width;
5766        let h = hmap.height;
5767        let mut field = WindField::new(w, h);
5768        let fbm_p = FbmParams { octaves: 4, frequency: 2.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
5769        for y in 0..h {
5770            for x in 0..w {
5771                let idx   = y * w + x;
5772                let slope = hmap.slope_at(x, y, 1.0);
5773                let grad  = hmap.gradient_at(x, y);
5774                let upslope = base_wind.dot(grad);
5775                let deflect = -grad * upslope * slope * 2.0;
5776                let speed_m = 1.0 + (if upslope < 0.0 { 1.0 } else { 0.0 }) * slope * 0.5;
5777                let nx      = x as f32 / w as f32 + seed as f32 * 1e-5;
5778                let ny      = y as f32 / h as f32;
5779                let noise_x = fbm_2d(nx,         ny,         &fbm_p) * turbulence_strength;
5780                let noise_y = fbm_2d(nx + 100.0, ny + 100.0, &fbm_p) * turbulence_strength;
5781                field.vectors[idx]    = (base_wind + deflect) * speed_m + Vec2::new(noise_x, noise_y);
5782                field.turbulence[idx] = noise_x.abs() + noise_y.abs();
5783            }
5784        }
5785        field
5786    }
5787
5788    pub fn sample(&self, x: f32, y: f32) -> Vec2 {
5789        let gx = x.clamp(0.0, (self.width  - 1) as f32);
5790        let gy = y.clamp(0.0, (self.height - 1) as f32);
5791        let x0 = gx.floor() as usize;
5792        let y0 = gy.floor() as usize;
5793        let x1 = (x0 + 1).min(self.width  - 1);
5794        let y1 = (y0 + 1).min(self.height - 1);
5795        let tx = gx - x0 as f32;
5796        let ty = gy - y0 as f32;
5797        let a  = self.vectors[y0 * self.width + x0];
5798        let b  = self.vectors[y0 * self.width + x1];
5799        let c  = self.vectors[y1 * self.width + x0];
5800        let d  = self.vectors[y1 * self.width + x1];
5801        a.lerp(b, tx).lerp(c.lerp(d, tx), ty)
5802    }
5803}
5804
5805// ============================================================
5806//  HEIGHTMAP MASK OPERATIONS
5807// ============================================================
5808
5809/// Generate radial falloff mask (circular island shape)
5810pub fn generate_island_mask(width: usize, height: usize, falloff_exp: f32) -> Vec<f32> {
5811    let mut mask = vec![0.0f32; width * height];
5812    let cx   = width  as f32 * 0.5;
5813    let cy   = height as f32 * 0.5;
5814    let max_r = cx.min(cy) * 0.95;
5815    for y in 0..height {
5816        for x in 0..width {
5817            let dx   = x as f32 - cx;
5818            let dy   = y as f32 - cy;
5819            let t    = ((dx*dx + dy*dy).sqrt() / max_r).clamp(0.0, 1.0);
5820            mask[y * width + x] = (1.0 - t.powf(falloff_exp)).clamp(0.0, 1.0);
5821        }
5822    }
5823    mask
5824}
5825
5826/// Apply mask to heightmap
5827pub fn apply_mask(hmap: &mut Heightmap, mask: &[f32]) {
5828    let len = hmap.data.len().min(mask.len());
5829    for i in 0..len { hmap.data[i] *= mask[i]; }
5830    hmap.recompute_minmax();
5831}
5832
5833#[derive(Clone, Debug)]
5834pub enum MaskBlendMode { Add, Multiply, Screen, Max, Min, Subtract }
5835
5836pub fn blend_masks(a: &[f32], b: &[f32], mode: MaskBlendMode) -> Vec<f32> {
5837    let len = a.len().min(b.len());
5838    (0..len).map(|i| match mode {
5839        MaskBlendMode::Add      => (a[i] + b[i]).clamp(0.0, 1.0),
5840        MaskBlendMode::Multiply => a[i] * b[i],
5841        MaskBlendMode::Screen   => 1.0 - (1.0 - a[i]) * (1.0 - b[i]),
5842        MaskBlendMode::Max      => a[i].max(b[i]),
5843        MaskBlendMode::Min      => a[i].min(b[i]),
5844        MaskBlendMode::Subtract => (a[i] - b[i]).clamp(0.0, 1.0),
5845    }).collect()
5846}
5847
5848// ============================================================
5849//  BEZIER CURVE UTILITIES
5850// ============================================================
5851
5852/// Cubic Bezier: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
5853pub fn cubic_bezier(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
5854    let mt = 1.0 - t;
5855    p0 * (mt*mt*mt) + p1 * (3.0*mt*mt*t) + p2 * (3.0*mt*t*t) + p3 * (t*t*t)
5856}
5857
5858/// Cubic Bezier tangent
5859pub fn cubic_bezier_tangent(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
5860    let mt = 1.0 - t;
5861    (p1 - p0) * (3.0*mt*mt) + (p2 - p1) * (6.0*mt*t) + (p3 - p2) * (3.0*t*t)
5862}
5863
5864/// Auto-smooth polyline with Catmull-Rom to Bezier conversion
5865pub fn auto_smooth_polyline(pts: &[Vec2], tension: f32, samples: usize) -> Vec<Vec2> {
5866    if pts.len() < 2 { return pts.to_vec(); }
5867    let mut result = Vec::new();
5868    for i in 0..pts.len() - 1 {
5869        let p0 = if i == 0 { pts[0] + (pts[0] - pts[1]) } else { pts[i-1] };
5870        let p1 = pts[i];
5871        let p2 = pts[i+1];
5872        let p3 = if i+2 >= pts.len() { pts[pts.len()-1] + (pts[pts.len()-1] - pts[pts.len()-2]) } else { pts[i+2] };
5873        let cp1 = p1 + (p2 - p0) * (tension / 6.0);
5874        let cp2 = p2 - (p3 - p1) * (tension / 6.0);
5875        for s in 0..samples {
5876            result.push(cubic_bezier(p1, cp1, cp2, p2, s as f32 / samples as f32));
5877        }
5878    }
5879    result.push(*pts.last().unwrap());
5880    result
5881}
5882
5883/// Compute bezier arc length numerically
5884pub fn bezier_arc_length(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, steps: usize) -> f32 {
5885    let mut len  = 0.0f32;
5886    let mut prev = p0;
5887    for i in 1..=steps {
5888        let t    = i as f32 / steps as f32;
5889        let curr = cubic_bezier(p0, p1, p2, p3, t);
5890        len += (curr - prev).length();
5891        prev = curr;
5892    }
5893    len
5894}
5895
5896// ============================================================
5897//  HEIGHTMAP CROP / TILE / STITCH
5898// ============================================================
5899
5900/// Tile a heightmap to a larger grid
5901pub fn tile_heightmap(source: &Heightmap, tile_x: usize, tile_y: usize) -> Heightmap {
5902    let new_w = source.width  * tile_x;
5903    let new_h = source.height * tile_y;
5904    let mut out = Heightmap::new(new_w, new_h);
5905    for ty in 0..tile_y {
5906        for tx in 0..tile_x {
5907            for y in 0..source.height {
5908                for x in 0..source.width {
5909                    out.set(tx * source.width + x, ty * source.height + y, source.get(x, y));
5910                }
5911            }
5912        }
5913    }
5914    out.recompute_minmax();
5915    out
5916}
5917
5918/// Crop sub-region of a heightmap
5919pub fn crop_heightmap(source: &Heightmap, ox: usize, oy: usize, w: usize, h: usize) -> Heightmap {
5920    let x_end = (ox + w).min(source.width);
5921    let y_end = (oy + h).min(source.height);
5922    let out_w = x_end - ox;
5923    let out_h = y_end - oy;
5924    let mut out = Heightmap::new(out_w, out_h);
5925    for ry in 0..out_h { for rx in 0..out_w { out.set(rx, ry, source.get(ox + rx, oy + ry)); } }
5926    out.recompute_minmax();
5927    out
5928}
5929
5930/// Stitch two heightmaps side-by-side with blended seam
5931pub fn stitch_heightmaps_horizontal(left: &Heightmap, right: &Heightmap, seam_width: usize) -> Heightmap {
5932    assert_eq!(left.height, right.height);
5933    let total_w = left.width + right.width;
5934    let h       = left.height;
5935    let mut out = Heightmap::new(total_w, h);
5936    for y in 0..h {
5937        for x in 0..left.width  { out.set(x,             y, left.get(x, y));  }
5938        for x in 0..right.width { out.set(left.width + x, y, right.get(x, y)); }
5939    }
5940    let sw = seam_width.min(left.width).min(right.width);
5941    for y in 0..h {
5942        for s in 0..sw {
5943            let t       = smoothstep(0.0, 1.0, s as f32 / sw as f32);
5944            let lx      = left.width - sw + s;
5945            let rx      = left.width + s;
5946            let blended = lerp_f(out.get(lx, y), out.get(rx, y), t);
5947            out.set(lx, y, blended);
5948            out.set(rx, y, blended);
5949        }
5950    }
5951    out.recompute_minmax();
5952    out
5953}
5954
5955// ============================================================
5956//  VERTEX COLOR PAINTING
5957// ============================================================
5958
5959#[derive(Clone, Debug)]
5960pub struct VertexColorMap {
5961    pub width:  usize,
5962    pub height: usize,
5963    pub data:   Vec<Vec4>,
5964}
5965
5966impl VertexColorMap {
5967    pub fn new(width: usize, height: usize, fill: Vec4) -> Self {
5968        VertexColorMap { width, height, data: vec![fill; width * height] }
5969    }
5970
5971    pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, color: Vec4) {
5972        let r    = brush.radius.ceil() as i32;
5973        let x0   = ((cx as i32 - r).max(0)) as usize;
5974        let y0   = ((cy as i32 - r).max(0)) as usize;
5975        let x1   = ((cx as i32 + r).min(self.width  as i32 - 1)) as usize;
5976        let y1   = ((cy as i32 + r).min(self.height as i32 - 1)) as usize;
5977        for y in y0..=y1 {
5978            for x in x0..=x1 {
5979                let dist = ((x as f32 - cx).powi(2) + (y as f32 - cy).powi(2)).sqrt();
5980                if dist > brush.radius { continue; }
5981                let w   = brush.weight_at_radius(dist);
5982                let idx = y * self.width + x;
5983                let old = self.data[idx];
5984                let a   = color.w * w;
5985                self.data[idx] = Vec4::new(
5986                    lerp_f(old.x, color.x, a),
5987                    lerp_f(old.y, color.y, a),
5988                    lerp_f(old.z, color.z, a),
5989                    lerp_f(old.w, 1.0, a),
5990                );
5991            }
5992        }
5993    }
5994
5995    pub fn sample_bilinear(&self, u: f32, v: f32) -> Vec4 {
5996        let px = u * (self.width  - 1) as f32;
5997        let py = v * (self.height - 1) as f32;
5998        let x0 = px.floor() as usize;
5999        let y0 = py.floor() as usize;
6000        let x1 = (x0 + 1).min(self.width  - 1);
6001        let y1 = (y0 + 1).min(self.height - 1);
6002        let tx = px - x0 as f32;
6003        let ty = py - y0 as f32;
6004        fn l4(a: Vec4, b: Vec4, t: f32) -> Vec4 { a + (b - a) * t }
6005        let a = self.data[y0 * self.width + x0];
6006        let b = self.data[y0 * self.width + x1];
6007        let c = self.data[y1 * self.width + x0];
6008        let d = self.data[y1 * self.width + x1];
6009        l4(l4(a, b, tx), l4(c, d, tx), ty)
6010    }
6011}
6012
6013// ============================================================
6014//  DECAL SYSTEM
6015// ============================================================
6016
6017#[derive(Clone, Debug)]
6018pub struct Decal {
6019    pub id:         u32,
6020    pub position:   Vec3,
6021    pub rotation_y: f32,
6022    pub size:       Vec2,
6023    pub texture_id: u32,
6024    pub alpha:      f32,
6025    pub tint:       Vec4,
6026}
6027
6028impl Decal {
6029    pub fn transform(&self) -> Mat4 {
6030        Mat4::from_translation(self.position)
6031        * Mat4::from_rotation_y(self.rotation_y)
6032        * Mat4::from_scale(Vec3::new(self.size.x, 1.0, self.size.y))
6033    }
6034    pub fn world_aabb(&self) -> (Vec3, Vec3) {
6035        let hs = Vec3::new(self.size.x * 0.5, 1.0, self.size.y * 0.5);
6036        (self.position - hs, self.position + hs)
6037    }
6038}
6039
6040pub struct DecalLayer {
6041    pub decals:  Vec<Decal>,
6042    pub next_id: u32,
6043}
6044
6045impl DecalLayer {
6046    pub fn new() -> Self { DecalLayer { decals: Vec::new(), next_id: 0 } }
6047    pub fn add(&mut self, position: Vec3, rotation_y: f32, size: Vec2, texture_id: u32) -> u32 {
6048        let id = self.next_id; self.next_id += 1;
6049        self.decals.push(Decal { id, position, rotation_y, size, texture_id, alpha: 1.0, tint: Vec4::ONE });
6050        id
6051    }
6052    pub fn remove(&mut self, id: u32) { self.decals.retain(|d| d.id != id); }
6053    pub fn query_sphere(&self, center: Vec3, radius: f32) -> Vec<&Decal> {
6054        self.decals.iter().filter(|d| (d.position - center).length() <= radius + d.size.x.max(d.size.y)).collect()
6055    }
6056}
6057
6058// ============================================================
6059//  TERRAIN FEATURE DETECTION
6060// ============================================================
6061
6062pub fn find_peaks(hmap: &Heightmap, min_height: f32, search_radius: usize) -> Vec<(usize, usize, f32)> {
6063    let w = hmap.width;
6064    let h = hmap.height;
6065    let r = search_radius as i32;
6066    let mut peaks = Vec::new();
6067    for y in r as usize..h - r as usize {
6068        for x in r as usize..w - r as usize {
6069            let ch = hmap.get(x, y);
6070            if ch < min_height { continue; }
6071            let mut is_max = true;
6072            'chk: for dy in -r..=r { for dx in -r..=r {
6073                if dx == 0 && dy == 0 { continue; }
6074                if hmap.get_clamped(x as i32 + dx, y as i32 + dy) > ch { is_max = false; break 'chk; }
6075            }}
6076            if is_max { peaks.push((x, y, ch)); }
6077        }
6078    }
6079    peaks
6080}
6081
6082pub fn find_cliffs(hmap: &Heightmap, cliff_threshold: f32) -> Vec<(usize, usize)> {
6083    let w = hmap.width;
6084    let h = hmap.height;
6085    let mut cliffs = Vec::new();
6086    for y in 1..h-1 { for x in 1..w-1 {
6087        let center = hmap.get(x, y);
6088        let max_diff = [hmap.get(x+1,y), hmap.get(x-1,y), hmap.get(x,y+1), hmap.get(x,y-1)]
6089            .iter().map(|&n| (center - n).abs()).fold(0.0f32, f32::max);
6090        if max_diff >= cliff_threshold { cliffs.push((x, y)); }
6091    }}
6092    cliffs
6093}
6094
6095// ============================================================
6096//  LAYER STACK (non-destructive editing)
6097// ============================================================
6098
6099#[derive(Clone, Debug)]
6100pub enum LayerOperation { Add, Multiply, Subtract, Max, Min, Blend(f32) }
6101
6102#[derive(Clone, Debug)]
6103pub struct TerrainLayerStack {
6104    pub base:   Vec<f32>,
6105    pub layers: Vec<(Vec<f32>, LayerOperation, f32)>,
6106    pub width:  usize,
6107    pub height: usize,
6108}
6109
6110impl TerrainLayerStack {
6111    pub fn new(width: usize, height: usize) -> Self {
6112        TerrainLayerStack { base: vec![0.0; width*height], layers: Vec::new(), width, height }
6113    }
6114    pub fn push_layer(&mut self, data: Vec<f32>, op: LayerOperation, strength: f32) {
6115        self.layers.push((data, op, strength));
6116    }
6117    pub fn flatten(&self) -> Vec<f32> {
6118        let mut result = self.base.clone();
6119        let n = result.len();
6120        for (layer_data, op, strength) in &self.layers {
6121            for i in 0..n.min(layer_data.len()) {
6122                result[i] = match op {
6123                    LayerOperation::Add      => (result[i] + layer_data[i] * strength).clamp(0.0, 1.0),
6124                    LayerOperation::Multiply => result[i] * (1.0 + (layer_data[i] - 0.5) * strength * 2.0),
6125                    LayerOperation::Subtract => (result[i] - layer_data[i] * strength).clamp(0.0, 1.0),
6126                    LayerOperation::Max      => result[i].max(layer_data[i]),
6127                    LayerOperation::Min      => result[i].min(layer_data[i]),
6128                    LayerOperation::Blend(t) => lerp_f(result[i], layer_data[i], *t * strength),
6129                };
6130            }
6131        }
6132        result
6133    }
6134}
6135
6136
6137
6138// ============================================================
6139//  WORLD EDITOR EXTENDED METHODS
6140// ============================================================
6141
6142impl WorldEditor {
6143    pub fn apply_island_mask(&mut self, falloff_exp: f32) {
6144        let mask = generate_island_mask(self.heightmap.width, self.heightmap.height, falloff_exp);
6145        apply_mask(&mut self.heightmap, &mask);
6146        self.heightmap_dirty = true;
6147    }
6148
6149    pub fn compute_snow_map(&self, snow_line: f32, temp_thresh: f32) -> Vec<f32> {
6150        let w = self.heightmap.width;
6151        let h = self.heightmap.height;
6152        let mut snow = vec![0.0f32; w * h];
6153        for y in 0..h { for x in 0..w {
6154            let idx  = y * w + x;
6155            let alt  = self.heightmap.get(x, y);
6156            let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
6157            if alt >= snow_line && temp <= temp_thresh {
6158                let af = ((alt - snow_line) / (1.0 - snow_line)).clamp(0.0, 1.0);
6159                let tf = ((temp_thresh - temp) / 30.0).clamp(0.0, 1.0);
6160                snow[idx] = (af * 0.6 + tf * 0.4).clamp(0.0, 1.0);
6161            }
6162        }}
6163        snow
6164    }
6165
6166    pub fn build_chunk_mesh(&self, chunk_x: usize, chunk_z: usize, chunk_size: usize, lod_step: usize) -> TerrainMesh {
6167        generate_terrain_mesh(&self.heightmap, chunk_x, chunk_z, chunk_size, self.cell_size, self.height_scale, lod_step)
6168    }
6169
6170    pub fn compute_ao(&self, num_rays: usize, max_dist: f32) -> Vec<f32> {
6171        compute_terrain_ao(&self.heightmap, num_rays, max_dist, self.cell_size)
6172    }
6173
6174    pub fn compute_shadow_map(&self) -> Vec<f32> {
6175        compute_terrain_shadow_map(&self.heightmap, self.solar.direction, self.cell_size, self.height_scale)
6176    }
6177
6178    pub fn render_minimap(&self, out_w: usize, out_h: usize) -> Vec<u8> {
6179        let ramp = ColorRamp::terrain_default();
6180        let w    = self.heightmap.width;
6181        let h    = self.heightmap.height;
6182        let mut bytes = Vec::with_capacity(out_w * out_h * 4);
6183        for my in 0..out_h {
6184            for mx in 0..out_w {
6185                let u   = mx as f32 / out_w as f32;
6186                let v   = my as f32 / out_h as f32;
6187                let ht  = self.heightmap.sample_bilinear(u, v);
6188                let col = ramp.sample(ht);
6189                bytes.push((col.x * 255.0) as u8);
6190                bytes.push((col.y * 255.0) as u8);
6191                bytes.push((col.z * 255.0) as u8);
6192                bytes.push(255u8);
6193            }
6194        }
6195        bytes
6196    }
6197
6198    pub fn find_peaks(&self, min_height: f32, search_radius: usize) -> Vec<(usize, usize, f32)> {
6199        find_peaks(&self.heightmap, min_height, search_radius)
6200    }
6201
6202    pub fn generate_wind_field(&self, turbulence: f32) -> WindField {
6203        WindField::generate_from_terrain(&self.heightmap, self.weather.wind_vector(), turbulence, self.master_seed ^ 0xABCD0001)
6204    }
6205
6206    pub fn stamp_hill(&mut self, world_x: f32, world_z: f32, radius: f32, height: f32, sharpness: f32) {
6207        let cx  = world_x / self.cell_size;
6208        let cz  = world_z / self.cell_size;
6209        let r   = (radius / self.cell_size) as usize;
6210        let w   = self.heightmap.width;
6211        let h   = self.heightmap.height;
6212        let x0  = ((cx as usize).saturating_sub(r)).min(w.saturating_sub(1));
6213        let y0  = ((cz as usize).saturating_sub(r)).min(h.saturating_sub(1));
6214        let x1  = ((cx as usize + r + 1)).min(w);
6215        let y1  = ((cz as usize + r + 1)).min(h);
6216        let rw  = x1 - x0;
6217        let rh  = y1 - y0;
6218        let mut stamp = vec![0.0f32; rw * rh];
6219        for ry in 0..rh { for rx in 0..rw {
6220            let dx   = (x0 + rx) as f32 - cx;
6221            let dz   = (y0 + ry) as f32 - cz;
6222            let dist = (dx*dx + dz*dz).sqrt() * self.cell_size;
6223            stamp[ry * rw + rx] = (1.0 - (dist / radius).clamp(0.0, 1.0).powf(sharpness)).max(0.0) * height / self.height_scale;
6224        }}
6225        let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, rw, rh, 1.0);
6226        self.undo_redo.push(action);
6227        self.heightmap_dirty = true;
6228    }
6229
6230
6231    pub fn world_report(&self) -> String {
6232        let s = &self.stats;
6233        format!(
6234            "World '{}' {}x{} @ {:.1}m | H: {:.3}..{:.3} | Sea: {:.3}\n\
6235             Sun: alt={:.1}° az={:.1}° ({}) | DoY: {} Time: {:.1}h\n\
6236             Weather: {} Temp: {:.1}°C Wind: {:.1}m/s@{:.0}° Cloud: {:.0}%\n\
6237             Content: {} foliage | {} rivers | {} lakes | {} roads ({:.0}m)",
6238            self.world_name,
6239            self.heightmap.width, self.heightmap.height,
6240            self.cell_size,
6241            s.min_height, s.max_height, self.sea_level,
6242            self.solar.altitude_deg, self.solar.azimuth_deg,
6243            if self.solar.is_day { "day" } else { "night" },
6244            self.day_of_year as usize, self.utc_hour,
6245            self.weather.current.state.name(),
6246            self.weather.current.temperature_c,
6247            self.weather.current.wind_speed_ms,
6248            self.weather.current.wind_dir_deg,
6249            self.weather.current.cloud_cover * 100.0,
6250            s.foliage_count, s.river_count, s.lake_count,
6251            s.road_segments, s.road_total_length,
6252        )
6253    }
6254
6255    pub fn validate(&self) -> Vec<String> {
6256        let mut w = Vec::new();
6257        if self.heightmap.width  < 16 { w.push("Heightmap width very small".into()); }
6258        if self.heightmap.height < 16 { w.push("Heightmap height very small".into()); }
6259        if self.sea_level > 0.9  { w.push("Sea level very high".into()); }
6260        if self.foliage.len() > 500_000 { w.push("Very large foliage count".into()); }
6261        for (i, lake) in self.lakes.iter().enumerate() {
6262            if lake.water_level < self.sea_level { w.push(format!("Lake {} below sea level", i)); }
6263        }
6264        w
6265    }
6266
6267    pub fn is_navigable(&self, x: usize, y: usize, max_slope_deg: f32) -> bool {
6268        let alt   = self.heightmap.get(x, y);
6269        if alt <= self.sea_level { return false; }
6270        self.heightmap.slope_at(x, y, self.cell_size) * RAD2DEG <= max_slope_deg
6271    }
6272
6273    pub fn export_nav_passability(&self, max_slope_deg: f32) -> Vec<bool> {
6274        let w = self.heightmap.width;
6275        let h = self.heightmap.height;
6276        (0..h).flat_map(|y| (0..w).map(move |x| self.is_navigable(x, y, max_slope_deg))).collect()
6277    }
6278
6279    pub fn day_length_hours(&self) -> f64 {
6280        match sunrise_sunset(self.latitude, self.longitude, self.day_of_year) {
6281            Some((rise, set)) => set - rise,
6282            None => if self.solar.altitude_deg > 0.0 { 24.0 } else { 0.0 },
6283        }
6284    }
6285
6286    pub fn rebuild_all(&mut self) {
6287        self.generate_climate();
6288        self.compute_stats();
6289        self.update_solar();
6290        self.heightmap_dirty = false;
6291        self.climate_dirty   = false;
6292    }
6293
6294    pub fn sample_sky_color(&self, view_dir: Vec3) -> Vec3 {
6295        let clear = self.sky_at(view_dir);
6296        let cloud = Vec3::new(0.8, 0.85, 0.9);
6297        clear.lerp(cloud, self.weather.current.cloud_cover)
6298    }
6299
6300
6301    pub fn compute_viewshed(&self, obs_x: usize, obs_z: usize, obs_height: f32, max_dist: f32) -> Vec<bool> {
6302        let w     = self.heightmap.width;
6303        let h     = self.heightmap.height;
6304        let obs_h = self.heightmap.get(obs_x, obs_z) * self.height_scale + obs_height;
6305        let mut visible = vec![false; w * h];
6306        for tz in 0..h { for tx in 0..w {
6307            let dc = (((tx as i32 - obs_x as i32).pow(2) + (tz as i32 - obs_z as i32).pow(2)) as f32).sqrt();
6308            if dc * self.cell_size > max_dist { continue; }
6309            let steps = dc.ceil() as usize;
6310            if steps == 0 { visible[tz*w+tx] = true; continue; }
6311            let tgt_h = self.heightmap.get(tx.min(w-1), tz.min(h-1)) * self.height_scale;
6312            let mut los = true;
6313            for s in 1..steps {
6314                let t    = s as f32 / steps as f32;
6315                let lx   = obs_x as f32 + (tx as f32 - obs_x as f32) * t;
6316                let lz   = obs_z as f32 + (tz as f32 - obs_z as f32) * t;
6317                let ux   = (lx / w as f32).clamp(0.0, 1.0);
6318                let uz   = (lz / h as f32).clamp(0.0, 1.0);
6319                let th   = self.heightmap.sample_bilinear(ux, uz) * self.height_scale;
6320                let los_h = obs_h + (tgt_h - obs_h) * t;
6321                if th > los_h { los = false; break; }
6322            }
6323            visible[tz*w+tx] = los;
6324        }}
6325        visible
6326    }
6327}
6328
6329
6330// ============================================================
6331//  HALTON & FIBONACCI SAMPLING
6332// ============================================================
6333
6334pub fn halton(index: usize, base: usize) -> f32 {
6335    let mut f = 1.0f32;
6336    let mut r = 0.0f32;
6337    let mut i = index;
6338    while i > 0 {
6339        f /= base as f32;
6340        r += f * (i % base) as f32;
6341        i /= base;
6342    }
6343    r
6344}
6345
6346pub fn halton_2d(count: usize) -> Vec<Vec2> {
6347    (0..count).map(|i| Vec2::new(halton(i+1, 2), halton(i+1, 3))).collect()
6348}
6349
6350pub fn fibonacci_sphere_points(n: usize) -> Vec<Vec3> {
6351    let gr = (1.0 + 5.0_f32.sqrt()) * 0.5;
6352    (0..n).map(|i| {
6353        let theta = (1.0 - 2.0 * i as f32 / (n as f32 - 1.0)).acos();
6354        let phi   = TWO_PI * i as f32 / gr;
6355        Vec3::new(theta.sin() * phi.cos(), theta.sin() * phi.sin(), theta.cos())
6356    }).collect()
6357}
6358
6359
6360pub const BEAUFORT_NAMES: [&str; 13] = [
6361    "Calm","Light air","Light breeze","Gentle breeze","Moderate breeze",
6362    "Fresh breeze","Strong breeze","Near gale","Gale","Strong gale",
6363    "Storm","Violent storm","Hurricane",
6364];
6365
6366pub fn classify_cloud(coverage: f32, altitude_km: f32) -> &'static str {
6367    if coverage < 0.1 { return "Clear"; }
6368    if altitude_km < 2.0 { if coverage > 0.7 { "Stratus" } else { "Stratocumulus" } }
6369    else if altitude_km < 6.0 { if coverage > 0.6 { "Altostratus" } else { "Altocumulus" } }
6370    else { if coverage > 0.5 { "Cirrostratus" } else { "Cirrus" } }
6371}
6372
6373pub fn pressure_tendency(history: &VecDeque<WeatherSnapshot>) -> &'static str {
6374    if history.len() < 3 { return "Steady"; }
6375    let v: Vec<f32> = history.iter().rev().take(3).map(|s| s.pressure_hpa).collect();
6376    let trend = v[0] - v[2];
6377    if trend > 1.5 { "Rising rapidly" } else if trend > 0.5 { "Rising" }
6378    else if trend < -1.5 { "Falling rapidly" } else if trend < -0.5 { "Falling" }
6379    else { "Steady" }
6380}
6381
6382// ============================================================
6383//  BIOME WEIGHT MAP GENERATION
6384// ============================================================
6385
6386pub fn compute_biome_weight_map(
6387    hmap:      &Heightmap,
6388    temp_map:  &[f32],
6389    hum_map:   &[f32],
6390    biome_sys: &BiomeSystem,
6391) -> Vec<[f32; 25]> {
6392    let w = hmap.width;
6393    let h = hmap.height;
6394    let mut wmap = vec![[0.0f32; 25]; w * h];
6395    for y in 0..h { for x in 0..w {
6396        let idx  = y * w + x;
6397        let alt  = hmap.get(x, y);
6398        let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
6399        let hum  = if idx < hum_map.len()  { hum_map[idx]  } else { 0.5  };
6400        wmap[idx] = biome_sys.sample(temp, hum, alt).weights;
6401    }}
6402    wmap
6403}
6404
6405pub fn build_biome_id_map(weight_map: &[[f32; 25]]) -> Vec<u8> {
6406    weight_map.iter().map(|ws|
6407        ws.iter().enumerate()
6408            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
6409            .map(|(i, _)| i as u8).unwrap_or(0)
6410    ).collect()
6411}
6412
6413
6414// ============================================================
6415//  SHALLOW WATER EQUATIONS (simplified)
6416// ============================================================
6417
6418#[derive(Clone, Debug)]
6419pub struct ShallowWaterSim {
6420    pub width:    usize,
6421    pub height:   usize,
6422    pub height_h: Vec<f32>,
6423    pub vel_x:    Vec<f32>,
6424    pub vel_z:    Vec<f32>,
6425    pub depth:    Vec<f32>,
6426    pub cell_size: f32,
6427    pub gravity:  f32,
6428    pub friction: f32,
6429}
6430
6431impl ShallowWaterSim {
6432    pub fn new(width: usize, height: usize, cell_size: f32, gravity: f32) -> Self {
6433        let n = width * height;
6434        ShallowWaterSim { width, height, height_h: vec![0.0;n], vel_x: vec![0.0;n], vel_z: vec![0.0;n], depth: vec![0.0;n], cell_size, gravity, friction: 0.99 }
6435    }
6436
6437    pub fn init_from_heightmap(&mut self, terrain: &Heightmap, sea_level: f32, height_scale: f32) {
6438        for y in 0..self.height { for x in 0..self.width {
6439            let th = terrain.get(x.min(terrain.width-1), y.min(terrain.height-1)) * height_scale;
6440            let wh = sea_level * height_scale;
6441            let idx = y * self.width + x;
6442            self.height_h[idx] = wh;
6443            self.depth[idx]    = (wh - th).max(0.0);
6444        }}
6445    }
6446
6447    pub fn step(&mut self, terrain: &Heightmap, height_scale: f32, dt: f32) {
6448        let w = self.width; let h = self.height;
6449        let g = self.gravity; let cs = self.cell_size;
6450        let hh = self.height_h.clone();
6451        for y in 1..h-1 { for x in 1..w-1 {
6452            let idx = y*w+x;
6453            let depth = self.depth[idx];
6454            if depth < 0.001 { continue; }
6455            let dhdx = (hh[y*w+x+1] - hh[y*w+x-1]) / (2.0*cs);
6456            let dhdz = (hh[(y+1)*w+x] - hh[(y-1)*w+x]) / (2.0*cs);
6457            self.vel_x[idx] = (self.vel_x[idx] - g*dhdx*dt) * self.friction;
6458            self.vel_z[idx] = (self.vel_z[idx] - g*dhdz*dt) * self.friction;
6459        }}
6460        let vx = self.vel_x.clone();
6461        let vz = self.vel_z.clone();
6462        for y in 1..h-1 { for x in 1..w-1 {
6463            let idx   = y*w+x;
6464            let depth = self.depth[idx];
6465            if depth < 0.001 { continue; }
6466            let fx = vx[idx] * depth * dt / cs;
6467            let fz = vz[idx] * depth * dt / cs;
6468            let nx = (x as i32 + fx.signum() as i32).clamp(0, w as i32 - 1) as usize;
6469            let nz = (y as i32 + fz.signum() as i32).clamp(0, h as i32 - 1) as usize;
6470            let tx_a = fx.abs().min(depth * 0.5);
6471            let tz_a = fz.abs().min(depth * 0.5);
6472            self.height_h[idx]    -= tx_a + tz_a;
6473            self.height_h[nz*w+x] += tz_a;
6474            self.height_h[y*w+nx] += tx_a;
6475            let th = terrain.get(x.min(terrain.width-1), y.min(terrain.height-1)) * height_scale;
6476            self.depth[idx] = (self.height_h[idx] - th).max(0.0);
6477        }}
6478    }
6479}
6480
6481// ============================================================
6482//  PROCEDURAL TEXTURE HELPERS
6483// ============================================================
6484
6485pub fn gen_rock_texture(width: usize, height: usize, seed: u64) -> Vec<f32> {
6486    let params = FbmParams { octaves: 6, frequency: 4.0, lacunarity: 2.1, gain: 0.55, amplitude: 1.0, offset: 1.0, ridge: true };
6487    let off    = (seed as f32 * 1e-5, seed as f32 * 1e-5 + 50.0);
6488    let mut out = vec![0.0f32; width * height];
6489    for y in 0..height { for x in 0..width {
6490        out[y * width + x] = (fbm_2d(x as f32 / width as f32 + off.0, y as f32 / height as f32 + off.1, &params) * 0.5 + 0.5).clamp(0.0, 1.0);
6491    }}
6492    out
6493}
6494
6495pub fn gen_soil_texture(width: usize, height: usize, seed: u64) -> Vec<f32> {
6496    let base_p  = FbmParams { octaves: 4, frequency: 8.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false };
6497    let crack_p = FbmParams { octaves: 3, frequency: 12.0, lacunarity: 2.5, gain: 0.4, amplitude: 0.5, offset: 0.0, ridge: false };
6498    let off     = (seed as f32 * 1e-5 + 100.0, seed as f32 * 1e-5 + 200.0);
6499    let mut out = vec![0.0f32; width * height];
6500    for y in 0..height { for x in 0..width {
6501        let nx = x as f32 / width as f32 + off.0;
6502        let ny = y as f32 / height as f32 + off.1;
6503        let b  = fbm_2d(nx,       ny,       &base_p)  * 0.5 + 0.5;
6504        let cr = fbm_2d(nx * 0.5, ny * 0.5, &crack_p).abs();
6505        out[y * width + x] = (b * 0.7 + cr * 0.3).clamp(0.0, 1.0);
6506    }}
6507    out
6508}
6509
6510
6511// ============================================================
6512//  TERRAIN LAYER PAINTER
6513// ============================================================
6514
6515#[derive(Clone, Debug)]
6516pub struct TerrainPaintLayer {
6517    pub id:         usize,
6518    pub name:       String,
6519    pub weight_map: Vec<f32>,
6520    pub tiling:     f32,
6521    pub normal_strength: f32,
6522}
6523
6524impl TerrainPaintLayer {
6525    pub fn new(id: usize, name: &str, width: usize, height: usize) -> Self {
6526        TerrainPaintLayer { id, name: name.into(), weight_map: vec![0.0; width*height], tiling: 10.0, normal_strength: 1.0 }
6527    }
6528
6529    pub fn paint(&mut self, cx: f32, cy: f32, brush: &BrushSettings, width: usize, height: usize) {
6530        let r  = brush.radius.ceil() as i32;
6531        let x0 = ((cx as i32 - r).max(0)) as usize;
6532        let y0 = ((cy as i32 - r).max(0)) as usize;
6533        let x1 = ((cx as i32 + r).min(width  as i32 - 1)) as usize;
6534        let y1 = ((cy as i32 + r).min(height as i32 - 1)) as usize;
6535        for y in y0..=y1 { for x in x0..=x1 {
6536            let dist = ((x as f32 - cx).powi(2) + (y as f32 - cy).powi(2)).sqrt();
6537            if dist > brush.radius { continue; }
6538            let idx = y * width + x;
6539            self.weight_map[idx] = (self.weight_map[idx] + brush.weight_at_radius(dist)).clamp(0.0, 1.0);
6540        }}
6541    }
6542}
6543
6544// ============================================================
6545//  ATMOSPHERIC HAZE
6546// ============================================================
6547
6548#[derive(Clone, Debug)]
6549pub struct AtmosphericHaze {
6550    pub density:        f32,
6551    pub haze_color:     Vec3,
6552    pub fog_start:      f32,
6553    pub fog_end:        f32,
6554    pub height_falloff: f32,
6555}
6556
6557impl AtmosphericHaze {
6558    pub fn new(density: f32, color: Vec3) -> Self {
6559        AtmosphericHaze { density, haze_color: color, fog_start: 100.0, fog_end: 5000.0, height_falloff: 0.002 }
6560    }
6561    pub fn fog_factor(&self, distance: f32, height: f32) -> f32 {
6562        (smoothstep(self.fog_start, self.fog_end, distance) * (-(height * self.height_falloff)).exp() * self.density).clamp(0.0, 1.0)
6563    }
6564    pub fn apply(&self, color: Vec3, distance: f32, height: f32) -> Vec3 {
6565        color.lerp(self.haze_color, self.fog_factor(distance, height))
6566    }
6567}
6568
6569
6570// ============================================================
6571//  FIND EROSION SOURCES
6572// ============================================================
6573
6574pub fn find_erosion_sources(hmap: &Heightmap, count: usize, min_altitude: f32, min_slope: f32, seed: u64) -> Vec<Vec2> {
6575    let w = hmap.width;
6576    let h = hmap.height;
6577    let mut candidates: Vec<(f32, Vec2)> = Vec::new();
6578    for y in 0..h { for x in 0..w {
6579        let alt   = hmap.get(x, y);
6580        let slope = hmap.slope_at(x, y, 1.0);
6581        if alt >= min_altitude && slope >= min_slope {
6582            candidates.push((alt * slope, Vec2::new(x as f32, y as f32)));
6583        }
6584    }}
6585    candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
6586    let min_spacing = ((w * h) as f32 / count as f32).sqrt() * 0.3;
6587    let mut result = Vec::new();
6588    for (_, pos) in candidates.iter() {
6589        if result.len() >= count { break; }
6590        if !result.iter().any(|p: &Vec2| (*p - *pos).length() < min_spacing) {
6591            result.push(*pos);
6592        }
6593    }
6594    result
6595}
6596
6597// ============================================================
6598//  WARP HEIGHTMAP
6599// ============================================================
6600
6601pub fn warp_heightmap(hmap: &Heightmap, disp_x: &[f32], disp_z: &[f32], strength: f32) -> Heightmap {
6602    let w = hmap.width;
6603    let h = hmap.height;
6604    let mut out = Heightmap::new(w, h);
6605    for y in 0..h { for x in 0..w {
6606        let idx  = y * w + x;
6607        let dx   = if idx < disp_x.len() { disp_x[idx] * strength } else { 0.0 };
6608        let dz   = if idx < disp_z.len() { disp_z[idx] * strength } else { 0.0 };
6609        let src_x = (x as f32 + dx * w as f32).clamp(0.0, (w-1) as f32);
6610        let src_z = (y as f32 + dz * h as f32).clamp(0.0, (h-1) as f32);
6611        out.set(x, y, hmap.sample_bilinear(src_x / (w-1) as f32, src_z / (h-1) as f32));
6612    }}
6613    out.recompute_minmax();
6614    out
6615}
6616
6617
6618
6619#[derive(Clone, Debug)]
6620pub struct FlareElement { pub offset: f32, pub size: f32, pub color: Vec4, pub texture_id: u32 }
6621
6622#[derive(Clone, Debug)]
6623pub struct LensFlare {
6624    pub elements:     Vec<FlareElement>,
6625    pub intensity:    f32,
6626    pub streak_count: u8,
6627    pub streak_size:  f32,
6628}
6629
6630impl LensFlare {
6631    pub fn sun_flare() -> Self {
6632        LensFlare {
6633            elements: vec![
6634                FlareElement { offset: 0.0, size: 0.15, color: Vec4::new(1.0, 0.9, 0.7, 0.8), texture_id: 0 },
6635                FlareElement { offset: 0.2, size: 0.05, color: Vec4::new(0.8, 0.8, 1.0, 0.4), texture_id: 1 },
6636                FlareElement { offset: 0.5, size: 0.08, color: Vec4::new(1.0, 0.7, 0.3, 0.3), texture_id: 2 },
6637                FlareElement { offset: 0.8, size: 0.04, color: Vec4::new(0.7, 1.0, 0.7, 0.2), texture_id: 1 },
6638                FlareElement { offset: 1.2, size: 0.10, color: Vec4::new(0.6, 0.8, 1.0, 0.2), texture_id: 0 },
6639            ],
6640            intensity: 1.0, streak_count: 6, streak_size: 0.4,
6641        }
6642    }
6643    pub fn screen_positions<'a>(&'a self, sun_screen: Vec2, screen_center: Vec2) -> Vec<(Vec2, &'a FlareElement)> {
6644        let axis = screen_center - sun_screen;
6645        self.elements.iter().map(|e| (sun_screen + axis * e.offset, e)).collect()
6646    }
6647}
6648
6649
6650#[derive(Clone, Debug)]
6651pub struct NavCell {
6652    pub x:          usize,
6653    pub y:          usize,
6654    pub passable:   bool,
6655    pub cost:       f32,
6656    pub region_id:  u32,
6657}
6658
6659#[derive(Clone, Debug)]
6660pub struct NavGrid {
6661    pub width:  usize,
6662    pub height: usize,
6663    pub cells:  Vec<NavCell>,
6664}
6665
6666impl NavGrid {
6667    pub fn from_heightmap(hmap: &Heightmap, cell_size: f32, sea_level: f32, max_slope_deg: f32) -> Self {
6668        let w = hmap.width;
6669        let h = hmap.height;
6670        let cells: Vec<NavCell> = (0..h).flat_map(|y| (0..w).map(move |x| {
6671            let alt   = hmap.get(x, y);
6672            let slope = hmap.slope_at(x.min(w-1), y.min(h-1), cell_size) * RAD2DEG;
6673            let pass  = alt > sea_level && slope <= max_slope_deg;
6674            let cost  = 1.0 + slope / max_slope_deg;
6675            NavCell { x, y, passable: pass, cost, region_id: 0 }
6676        })).collect();
6677        NavGrid { width: w, height: h, cells }
6678    }
6679
6680    pub fn get(&self, x: usize, y: usize) -> &NavCell {
6681        &self.cells[y * self.width + x]
6682    }
6683
6684    pub fn label_regions(&mut self) {
6685        let w = self.width;
6686        let h = self.height;
6687        let mut region = 0u32;
6688        let mut visited = vec![false; w * h];
6689
6690        for sy in 0..h {
6691            for sx in 0..w {
6692                if visited[sy * w + sx] || !self.cells[sy * w + sx].passable { continue; }
6693                region += 1;
6694                let mut queue = VecDeque::new();
6695                queue.push_back((sx, sy));
6696                visited[sy * w + sx] = true;
6697                while let Some((cx, cy)) = queue.pop_front() {
6698                    self.cells[cy * w + cx].region_id = region;
6699                    let neighbors: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
6700                    for &(dx, dy) in &neighbors {
6701                        let nx = cx as i32 + dx;
6702                        let ny = cy as i32 + dy;
6703                        if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 { continue; }
6704                        let ni = ny as usize * w + nx as usize;
6705                        if !visited[ni] && self.cells[ni].passable {
6706                            visited[ni] = true;
6707                            queue.push_back((nx as usize, ny as usize));
6708                        }
6709                    }
6710                }
6711            }
6712        }
6713    }
6714
6715    pub fn region_count(&self) -> u32 {
6716        self.cells.iter().map(|c| c.region_id).max().unwrap_or(0)
6717    }
6718
6719    pub fn largest_region_size(&self) -> usize {
6720        let mut counts: HashMap<u32, usize> = HashMap::new();
6721        for c in &self.cells { if c.passable { *counts.entry(c.region_id).or_insert(0) += 1; } }
6722        counts.values().copied().max().unwrap_or(0)
6723    }
6724}
6725
6726// ============================================================
6727//  HEIGHTMAP STATISTICAL ANALYSIS
6728// ============================================================
6729
6730#[derive(Clone, Debug)]
6731pub struct HeightmapStats {
6732    pub min:    f32,
6733    pub max:    f32,
6734    pub mean:   f32,
6735    pub median: f32,
6736    pub stddev: f32,
6737    pub skewness: f32,
6738    pub percentile_25: f32,
6739    pub percentile_75: f32,
6740    pub histogram: Vec<u32>,  // 256 buckets
6741}
6742
6743pub fn compute_heightmap_stats(hmap: &Heightmap) -> HeightmapStats {
6744    let n = hmap.data.len();
6745    if n == 0 {
6746        return HeightmapStats { min:0.0, max:0.0, mean:0.0, median:0.0, stddev:0.0,
6747            skewness:0.0, percentile_25:0.0, percentile_75:0.0, histogram: vec![0;256] };
6748    }
6749
6750    let mut sorted = hmap.data.clone();
6751    sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6752
6753    let min   = *sorted.first().unwrap();
6754    let max   = *sorted.last().unwrap();
6755    let sum: f64 = sorted.iter().map(|&v| v as f64).sum();
6756    let mean  = (sum / n as f64) as f32;
6757    let median = sorted[n / 2];
6758    let p25   = sorted[n / 4];
6759    let p75   = sorted[3 * n / 4];
6760
6761    let variance: f64 = sorted.iter().map(|&v| { let d = v as f64 - mean as f64; d*d }).sum::<f64>() / n as f64;
6762    let stddev  = variance.sqrt() as f32;
6763
6764    let skewness: f64 = if stddev > 1e-10 {
6765        sorted.iter().map(|&v| { let d = (v as f64 - mean as f64) / stddev as f64; d*d*d }).sum::<f64>() / n as f64
6766    } else { 0.0 };
6767
6768    let range = max - min;
6769    let mut histogram = vec![0u32; 256];
6770    for &v in &hmap.data {
6771        if range > 1e-10 {
6772            let b = ((v - min) / range * 255.0).clamp(0.0, 255.0) as usize;
6773            histogram[b] += 1;
6774        }
6775    }
6776
6777    HeightmapStats { min, max, mean, median, stddev, skewness: skewness as f32, percentile_25: p25, percentile_75: p75, histogram }
6778}
6779
6780// ============================================================
6781//  EROSION PARAMETER PRESETS
6782// ============================================================
6783
6784impl ErosionParams {
6785    pub fn preset_light() -> Self {
6786        ErosionParams { num_particles: 20_000, inertia: 0.03, capacity: 3.0,
6787            deposition: 0.4, erosion_speed: 0.2, evaporation: 0.025, min_slope: 0.005,
6788            gravity: 3.0, max_steps: 48, erosion_radius: 2.0, seed: 0xDEAD }
6789    }
6790    pub fn preset_heavy() -> Self {
6791        ErosionParams { num_particles: 150_000, inertia: 0.06, capacity: 6.0,
6792            deposition: 0.2, erosion_speed: 0.5, evaporation: 0.015, min_slope: 0.01,
6793            gravity: 5.0, max_steps: 80, erosion_radius: 4.0, seed: 0xBEEF }
6794    }
6795    pub fn preset_rivers() -> Self {
6796        ErosionParams { num_particles: 80_000, inertia: 0.08, capacity: 8.0,
6797            deposition: 0.1, erosion_speed: 0.8, evaporation: 0.01, min_slope: 0.02,
6798            gravity: 6.0, max_steps: 120, erosion_radius: 5.0, seed: 0xFACE }
6799    }
6800}
6801
6802// ============================================================
6803//  FBM PRESET LIBRARY
6804// ============================================================
6805
6806impl FbmParams {
6807    pub fn mountains() -> Self {
6808        FbmParams { octaves: 8, frequency: 1.0, lacunarity: 2.1, gain: 0.52, amplitude: 1.0, offset: 1.0, ridge: true }
6809    }
6810    pub fn plains() -> Self {
6811        FbmParams { octaves: 4, frequency: 0.5, lacunarity: 2.0, gain: 0.6, amplitude: 0.4, offset: 0.0, ridge: false }
6812    }
6813    pub fn hills() -> Self {
6814        FbmParams { octaves: 6, frequency: 1.5, lacunarity: 2.0, gain: 0.55, amplitude: 0.7, offset: 0.0, ridge: false }
6815    }
6816    pub fn canyon() -> Self {
6817        FbmParams { octaves: 5, frequency: 1.2, lacunarity: 2.3, gain: 0.45, amplitude: 1.0, offset: 0.8, ridge: true }
6818    }
6819    pub fn island() -> Self {
6820        FbmParams { octaves: 7, frequency: 1.0, lacunarity: 2.0, gain: 0.5, amplitude: 1.0, offset: 0.0, ridge: false }
6821    }
6822}
6823
6824// ============================================================
6825//  INTERPOLATION UTILITIES
6826// ============================================================
6827
6828/// Cubic hermite interpolation
6829#[inline]
6830pub fn cubic_hermite(y0: f32, y1: f32, y2: f32, y3: f32, t: f32) -> f32 {
6831    let a = -0.5*y0 + 1.5*y1 - 1.5*y2 + 0.5*y3;
6832    let b =  y0 - 2.5*y1 + 2.0*y2 - 0.5*y3;
6833    let c = -0.5*y0 + 0.5*y2;
6834    let d =  y1;
6835    ((a*t + b)*t + c)*t + d
6836}
6837
6838/// Quintic interpolation (6th order smooth)
6839#[inline]
6840pub fn quintic_interp(t: f32) -> f32 {
6841    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
6842}
6843
6844/// Spherical linear interpolation for quaternions (SLERP)
6845pub fn slerp(a: Quat, b: Quat, t: f32) -> Quat {
6846    let dot = a.dot(b).clamp(-1.0, 1.0);
6847    let b_adj = if dot < 0.0 { Quat::from_array([-b.x,-b.y,-b.z,-b.w]) } else { b };
6848    let dot_adj = dot.abs();
6849    if dot_adj > 0.9995 {
6850        return Quat::from_array([
6851            a.x + (b_adj.x - a.x) * t,
6852            a.y + (b_adj.y - a.y) * t,
6853            a.z + (b_adj.z - a.z) * t,
6854            a.w + (b_adj.w - a.w) * t,
6855        ]).normalize();
6856    }
6857    let theta_0 = dot_adj.acos();
6858    let theta    = theta_0 * t;
6859    let sin_t0   = theta_0.sin();
6860    let sin_t    = theta.sin();
6861    let s1 = (theta_0 - theta).sin() / sin_t0;
6862    let s2 = sin_t / sin_t0;
6863    Quat::from_array([
6864        a.x * s1 + b_adj.x * s2,
6865        a.y * s1 + b_adj.y * s2,
6866        a.z * s1 + b_adj.z * s2,
6867        a.w * s1 + b_adj.w * s2,
6868    ])
6869}
6870
6871/// Inverse bilinear interpolation (find UV from world position in quad)
6872pub fn inverse_bilinear(p: Vec2, a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Option<Vec2> {
6873    // Solve the bilinear system: p = a*(1-u)*(1-v) + b*u*(1-v) + c*(1-u)*v + d*u*v
6874    let e = b - a;
6875    let f = c - a;
6876    let g = a - b - c + d;
6877    let h = p - a;
6878
6879    // Quadratic in v
6880    let k2 = g.perp_dot(e);
6881    let k1 = e.perp_dot(h) + g.perp_dot(f); // sign correction from standard formulation
6882    let k0 = f.perp_dot(h);
6883
6884    let (v, u);
6885    if k2.abs() < 1e-6 {
6886        if k1.abs() < 1e-6 { return None; }
6887        v = -k0 / k1;
6888        let denom = e.x + g.x * v;
6889        u = if denom.abs() > 1e-6 { (h.x - f.x * v) / denom } else { (h.y - f.y * v) / (e.y + g.y * v) };
6890    } else {
6891        let disc = k1 * k1 - 4.0 * k0 * k2;
6892        if disc < 0.0 { return None; }
6893        v = (-k1 - disc.sqrt()) / (2.0 * k2);
6894        let denom = e.x + g.x * v;
6895        u = if denom.abs() > 1e-6 { (h.x - f.x * v) / denom } else { (h.y - f.y * v) / (e.y + g.y * v) };
6896    }
6897
6898    Some(Vec2::new(u, v))
6899}
6900
6901// ============================================================
6902//  WORLD SEED CATALOG
6903// ============================================================
6904
6905#[derive(Clone, Debug)]
6906pub struct WorldSeedPreset {
6907    pub name:    &'static str,
6908    pub seed:    u64,
6909    pub style:   WorldStyle,
6910    pub size:    usize,
6911}
6912
6913#[derive(Clone, Debug, PartialEq, Eq)]
6914pub enum WorldStyle {
6915    Continental,
6916    Island,
6917    Archipelago,
6918    Mountains,
6919    Desert,
6920    Tundra,
6921    Jungle,
6922    Mixed,
6923}
6924
6925pub const WORLD_SEED_PRESETS: [WorldSeedPreset; 12] = [
6926    WorldSeedPreset { name: "Verdant Valley",   seed: 0x1A2B3C4D, style: WorldStyle::Continental, size: 512 },
6927    WorldSeedPreset { name: "Dragon's Peak",    seed: 0xDEAD1234, style: WorldStyle::Mountains,   size: 1024 },
6928    WorldSeedPreset { name: "Lost Atoll",       seed: 0x42424242, style: WorldStyle::Island,      size: 512 },
6929    WorldSeedPreset { name: "Frozen North",     seed: 0xCE000001, style: WorldStyle::Tundra,      size: 1024 },
6930    WorldSeedPreset { name: "Amber Waste",      seed: 0xDEAD5A1D, style: WorldStyle::Desert,      size: 512 },
6931    WorldSeedPreset { name: "Emerald Canopy",   seed: 0x74726545, style: WorldStyle::Jungle,      size: 1024 },
6932    WorldSeedPreset { name: "Shattered Isles",  seed: 0xB0CA5501, style: WorldStyle::Archipelago, size: 2048 },
6933    WorldSeedPreset { name: "Old Frontier",     seed: 0xF121E510, style: WorldStyle::Mixed,       size: 1024 },
6934    WorldSeedPreset { name: "Crystal Spires",   seed: 0xCCC00DDD, style: WorldStyle::Mountains,   size: 512 },
6935    WorldSeedPreset { name: "River Delta",      seed: 0xD37741AA, style: WorldStyle::Continental, size: 1024 },
6936    WorldSeedPreset { name: "Thunder Plains",   seed: 0xBADC0DE1, style: WorldStyle::Mixed,       size: 2048 },
6937    WorldSeedPreset { name: "Ancient Caldera",  seed: 0xCA1D3EA0, style: WorldStyle::Island,      size: 512 },
6938];
6939
6940// ============================================================
6941//  HEIGHTMAP OPERATIONS — FILL SINKS
6942// ============================================================
6943
6944/// Planchon-Darboux sink filling — fill all depressions for hydrological flow
6945pub fn fill_sinks(hmap: &mut Heightmap, epsilon: f32) {
6946    let w = hmap.width;
6947    let h = hmap.height;
6948    let big = 1e9f32;
6949    let mut wl = vec![big; w * h];
6950
6951    // Initialize border cells
6952    for x in 0..w {
6953        wl[0 * w + x] = hmap.get(x, 0);
6954        wl[(h-1) * w + x] = hmap.get(x, h-1);
6955    }
6956    for y in 0..h {
6957        wl[y * w + 0] = hmap.get(0, y);
6958        wl[y * w + w-1] = hmap.get(w-1, y);
6959    }
6960
6961    let dirs: [(i32, i32); 8] = [(1,0),(-1,0),(0,1),(0,-1),(1,1),(1,-1),(-1,1),(-1,-1)];
6962
6963    // Iteratively lower water level
6964    let mut changed = true;
6965    let mut iter = 0;
6966    while changed && iter < 1000 {
6967        changed = false;
6968        iter += 1;
6969        for y in 1..h-1 {
6970            for x in 1..w-1 {
6971                let idx  = y * w + x;
6972                let hval = hmap.data[idx];
6973                let mut new_wl = wl[idx];
6974                for &(dx, dy) in &dirs {
6975                    let nx = (x as i32 + dx) as usize;
6976                    let ny = (y as i32 + dy) as usize;
6977                    let nidx = ny * w + nx;
6978                    let candidate = wl[nidx] + epsilon;
6979                    if hval >= candidate {
6980                        new_wl = new_wl.min(hval);
6981                    } else {
6982                        new_wl = new_wl.min(candidate);
6983                    }
6984                }
6985                if new_wl < wl[idx] {
6986                    wl[idx]  = new_wl;
6987                    changed  = true;
6988                }
6989            }
6990        }
6991    }
6992
6993    // Apply water level as new terrain height
6994    for (i, v) in wl.iter().enumerate() {
6995        if *v < big * 0.5 {
6996            hmap.data[i] = hmap.data[i].max(*v);
6997        }
6998    }
6999    hmap.recompute_minmax();
7000}
7001
7002// ============================================================
7003//  GRID UTILITIES
7004// ============================================================
7005
7006/// Compute cell neighbors in a 2D grid (8-way or 4-way)
7007pub fn cell_neighbors_8(x: usize, y: usize, width: usize, height: usize) -> Vec<(usize, usize)> {
7008    let mut result = Vec::with_capacity(8);
7009    let xi = x as i32;
7010    let yi = y as i32;
7011    for dy in -1..=1i32 {
7012        for dx in -1..=1i32 {
7013            if dx == 0 && dy == 0 { continue; }
7014            let nx = xi + dx;
7015            let ny = yi + dy;
7016            if nx >= 0 && ny >= 0 && nx < width as i32 && ny < height as i32 {
7017                result.push((nx as usize, ny as usize));
7018            }
7019        }
7020    }
7021    result
7022}
7023
7024pub fn cell_neighbors_4(x: usize, y: usize, width: usize, height: usize) -> Vec<(usize, usize)> {
7025    let mut result = Vec::with_capacity(4);
7026    let xi = x as i32;
7027    let yi = y as i32;
7028    for &(dx, dy) in &[(1i32,0i32),(-1,0),(0,1),(0,-1)] {
7029        let nx = xi + dx;
7030        let ny = yi + dy;
7031        if nx >= 0 && ny >= 0 && nx < width as i32 && ny < height as i32 {
7032            result.push((nx as usize, ny as usize));
7033        }
7034    }
7035    result
7036}
7037
7038/// Flood-fill a boolean map starting from a seed cell
7039pub fn flood_fill_bool(mask: &mut Vec<bool>, width: usize, height: usize, sx: usize, sy: usize, fill_value: bool) {
7040    let init = mask[sy * width + sx];
7041    if init == fill_value { return; }
7042    let mut queue = VecDeque::new();
7043    queue.push_back((sx, sy));
7044    mask[sy * width + sx] = fill_value;
7045    while let Some((cx, cy)) = queue.pop_front() {
7046        for (nx, ny) in cell_neighbors_4(cx, cy, width, height) {
7047            if mask[ny * width + nx] != fill_value {
7048                mask[ny * width + nx] = fill_value;
7049                queue.push_back((nx, ny));
7050            }
7051        }
7052    }
7053}
7054
7055// ============================================================
7056//  WORLD GENERATION PROFILE
7057// ============================================================
7058
7059#[derive(Clone, Debug)]
7060pub struct WorldGenProfile {
7061    pub name:              String,
7062    pub width:             usize,
7063    pub height:            usize,
7064    pub cell_size_m:       f32,
7065    pub height_scale_m:    f32,
7066    pub sea_level_frac:    f32,
7067    pub fbm_params:        FbmParams,
7068    pub warp_strength:     f32,
7069    pub erosion_params:    ErosionParams,
7070    pub thermal_iters:     usize,
7071    pub thermal_talus_deg: f32,
7072    pub num_rivers:        usize,
7073    pub num_lakes:         usize,
7074    pub foliage_density:   f32,
7075    pub apply_island_mask: bool,
7076    pub island_falloff:    f32,
7077    pub fill_sinks:        bool,
7078    pub latitude:          f64,
7079    pub longitude:         f64,
7080    pub start_doy:         f64,
7081    pub start_utc:         f64,
7082}
7083
7084impl WorldGenProfile {
7085    pub fn default_continental() -> Self {
7086        WorldGenProfile {
7087            name: "Continental".into(), width: 1024, height: 1024, cell_size_m: 1.0,
7088            height_scale_m: 500.0, sea_level_frac: 0.22, fbm_params: FbmParams::default_terrain(),
7089            warp_strength: 0.35, erosion_params: ErosionParams::default(), thermal_iters: 5,
7090            thermal_talus_deg: 32.0, num_rivers: 8, num_lakes: 5, foliage_density: 0.8,
7091            apply_island_mask: false, island_falloff: 2.0, fill_sinks: true,
7092            latitude: 45.0, longitude: 0.0, start_doy: 180.0, start_utc: 12.0,
7093        }
7094    }
7095
7096    pub fn default_island() -> Self {
7097        WorldGenProfile {
7098            name: "Island".into(), width: 512, height: 512, cell_size_m: 1.0,
7099            height_scale_m: 300.0, sea_level_frac: 0.28, fbm_params: FbmParams::island(),
7100            warp_strength: 0.4, erosion_params: ErosionParams::preset_light(), thermal_iters: 3,
7101            thermal_talus_deg: 28.0, num_rivers: 4, num_lakes: 2, foliage_density: 1.0,
7102            apply_island_mask: true, island_falloff: 2.5, fill_sinks: false,
7103            latitude: 10.0, longitude: -30.0, start_doy: 80.0, start_utc: 10.0,
7104        }
7105    }
7106
7107    pub fn default_mountains() -> Self {
7108        WorldGenProfile {
7109            name: "Mountains".into(), width: 1024, height: 1024, cell_size_m: 1.0,
7110            height_scale_m: 1000.0, sea_level_frac: 0.12, fbm_params: FbmParams::mountains(),
7111            warp_strength: 0.2, erosion_params: ErosionParams::preset_heavy(), thermal_iters: 10,
7112            thermal_talus_deg: 40.0, num_rivers: 12, num_lakes: 6, foliage_density: 0.4,
7113            apply_island_mask: false, island_falloff: 2.0, fill_sinks: true,
7114            latitude: 55.0, longitude: 10.0, start_doy: 240.0, start_utc: 8.0,
7115        }
7116    }
7117}
7118
7119impl WorldEditor {
7120    /// Apply a complete WorldGenProfile — full procedural pipeline
7121    pub fn apply_profile(&mut self, profile: &WorldGenProfile, seed: u64) {
7122        // Resize if needed
7123        if self.heightmap.width != profile.width || self.heightmap.height != profile.height {
7124            self.resize_world(profile.width, profile.height);
7125        }
7126
7127        self.cell_size      = profile.cell_size_m;
7128        self.height_scale   = profile.height_scale_m;
7129        self.sea_level      = profile.sea_level_frac;
7130        self.latitude       = profile.latitude;
7131        self.longitude      = profile.longitude;
7132        self.day_of_year    = profile.start_doy;
7133        self.utc_hour       = profile.start_utc;
7134        self.master_seed    = seed;
7135        self.world_name     = profile.name.clone();
7136        self.terrain_fbm_params = profile.fbm_params.clone();
7137        self.warp_strength  = profile.warp_strength;
7138        self.erosion_params = profile.erosion_params.clone();
7139
7140        // Generate terrain
7141        self.generate_terrain();
7142
7143        // Island mask
7144        if profile.apply_island_mask {
7145            self.apply_island_mask(profile.island_falloff);
7146        }
7147
7148        // Fill sinks
7149        if profile.fill_sinks {
7150            fill_sinks(&mut self.heightmap, 0.0001);
7151        }
7152
7153        // Erosion
7154        self.apply_erosion();
7155        self.apply_thermal_erosion(profile.thermal_iters, profile.thermal_talus_deg);
7156
7157        // Climate
7158        self.generate_climate();
7159
7160        // Water
7161        self.generate_rivers(profile.num_rivers);
7162        self.generate_lakes(profile.num_lakes, 0.025);
7163        self.generate_shore();
7164
7165        // Foliage
7166        let density = profile.foliage_density;
7167        self.place_foliage_layer(FoliagePlacementParams {
7168            min_radius: 2.0, max_instances: (density * 100_000.0) as usize,
7169            max_slope_rad: 0.65, min_altitude: 0.05, max_altitude: 0.75,
7170            density_scale: density, use_density_map: false, random_rotation: true,
7171            scale_variance: 0.3, base_scale: Vec3::ONE, asset_id: 0, biome_id: 0,
7172            align_to_normal: false,
7173        }, seed ^ 0xF01_1A6E);
7174
7175        // Solar
7176        self.update_solar();
7177        self.compute_stats();
7178        self.heightmap_dirty = false;
7179        self.climate_dirty   = false;
7180    }
7181}
7182
7183// ============================================================
7184//  ADDITIONAL TERRAIN STAMP SHAPES
7185// ============================================================
7186
7187/// Generate a volcano stamp (ring with caldera depression)
7188pub fn volcano_stamp(size: usize, rim_radius: f32, rim_height: f32, caldera_depth: f32) -> Vec<f32> {
7189    let center = size as f32 * 0.5;
7190    let mut stamp = vec![0.0f32; size * size];
7191    for y in 0..size {
7192        for x in 0..size {
7193            let dx   = x as f32 - center;
7194            let dy   = y as f32 - center;
7195            let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7196            let t    = dist / rim_radius;
7197            let h = if t < 0.6 {
7198                // caldera
7199                rim_height - caldera_depth + caldera_depth * smoothstep(0.0, 0.6, t)
7200            } else if t <= 1.0 {
7201                // rim
7202                let rt = (t - 0.6) / 0.4;
7203                rim_height * (1.0 - smoothstep(0.0, 1.0, rt))
7204            } else {
7205                // outer slope
7206                rim_height * (1.0 - smoothstep(1.0, 2.0, t)).max(0.0)
7207            };
7208            stamp[y * size + x] = h.max(0.0);
7209        }
7210    }
7211    stamp
7212}
7213
7214/// Generate a mesa stamp (flat top, steep sides)
7215pub fn mesa_stamp(size: usize, top_radius: f32, cliff_steepness: f32, height: f32) -> Vec<f32> {
7216    let center = size as f32 * 0.5;
7217    let mut stamp = vec![0.0f32; size * size];
7218    for y in 0..size {
7219        for x in 0..size {
7220            let dx   = x as f32 - center;
7221            let dy   = y as f32 - center;
7222            let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7223            let h = if dist <= top_radius {
7224                height
7225            } else {
7226                let edge_dist = (dist - top_radius) / (1.0 - top_radius);
7227                height * (1.0 - edge_dist.powf(cliff_steepness)).max(0.0)
7228            };
7229            stamp[y * size + x] = h.max(0.0);
7230        }
7231    }
7232    stamp
7233}
7234
7235/// Generate a crater stamp (impact crater — raised rim, depressed interior)
7236pub fn crater_stamp(size: usize, crater_radius: f32, rim_height: f32, depth: f32) -> Vec<f32> {
7237    let center = size as f32 * 0.5;
7238    let mut stamp = vec![0.0f32; size * size];
7239    for y in 0..size {
7240        for x in 0..size {
7241            let dx   = x as f32 - center;
7242            let dy   = y as f32 - center;
7243            let dist = (dx*dx + dy*dy).sqrt() / (size as f32 * 0.5);
7244            let t    = dist / crater_radius;
7245            let h = if t < 0.8 {
7246                // interior floor — depressed
7247                -depth * (1.0 - smoothstep(0.6, 0.8, t))
7248            } else if t <= 1.0 {
7249                // rim
7250                let rt = (t - 0.8) / 0.2;
7251                rim_height * (1.0 - (rt * 2.0 - 1.0).powi(2))
7252            } else {
7253                // outer falloff
7254                rim_height * (1.0 - smoothstep(1.0, 1.5, t)).max(0.0)
7255            };
7256            stamp[y * size + x] = h;
7257        }
7258    }
7259    stamp
7260}
7261
7262// ============================================================
7263//  VORONOI REGION BUILDER
7264// ============================================================
7265
7266#[derive(Clone, Debug)]
7267pub struct VoronoiCell {
7268    pub site:    Vec2,
7269    pub id:      u32,
7270    pub biome:   BiomeId,
7271    pub area:    f32,
7272    pub members: Vec<(usize, usize)>,
7273}
7274
7275pub struct VoronoiMap {
7276    pub width:  usize,
7277    pub height: usize,
7278    pub cell_id: Vec<u32>,
7279    pub cells:  Vec<VoronoiCell>,
7280}
7281
7282impl VoronoiMap {
7283    pub fn generate(width: usize, height: usize, num_sites: usize, seed: u64) -> Self {
7284        let mut rng   = LcgRng::new(seed);
7285        let sites: Vec<Vec2> = (0..num_sites).map(|_| {
7286            Vec2::new(rng.next_f32() * width as f32, rng.next_f32() * height as f32)
7287        }).collect();
7288
7289        let mut cell_id = vec![0u32; width * height];
7290        let mut cells: Vec<VoronoiCell> = (0..num_sites).map(|i| VoronoiCell {
7291            site: sites[i],
7292            id: i as u32,
7293            biome: BiomeId::TemperateGrassland,
7294            area: 0.0,
7295            members: Vec::new(),
7296        }).collect();
7297
7298        for y in 0..height {
7299            for x in 0..width {
7300                let p = Vec2::new(x as f32, y as f32);
7301                let best_idx = sites.iter().enumerate()
7302                    .min_by(|(_, a), (_, b)| {
7303                        let da = (**a - p).length_squared();
7304                        let db = (**b - p).length_squared();
7305                        da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
7306                    })
7307                    .map(|(i, _)| i)
7308                    .unwrap_or(0);
7309                cell_id[y * width + x] = best_idx as u32;
7310                cells[best_idx].members.push((x, y));
7311                cells[best_idx].area += 1.0;
7312            }
7313        }
7314
7315        VoronoiMap { width, height, cell_id, cells }
7316    }
7317
7318    pub fn assign_biomes(&mut self, hmap: &Heightmap, temp_map: &[f32], hum_map: &[f32]) {
7319        let w = hmap.width;
7320        for cell in self.cells.iter_mut() {
7321            let sx  = cell.site.x as usize;
7322            let sy  = cell.site.y as usize;
7323            let sx  = sx.min(hmap.width  - 1);
7324            let sy  = sy.min(hmap.height - 1);
7325            let idx = sy * w + sx;
7326            let alt  = hmap.get(sx, sy);
7327            let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
7328            let hum  = if idx < hum_map.len()  { hum_map[idx]  } else { 0.5  };
7329            cell.biome = BiomeDescriptor::classify_point(temp, hum, alt);
7330        }
7331    }
7332
7333    pub fn biome_at(&self, x: usize, y: usize) -> BiomeId {
7334        let cid = self.cell_id[y * self.width + x] as usize;
7335        if cid < self.cells.len() { self.cells[cid].biome } else { BiomeId::TemperateGrassland }
7336    }
7337}
7338
7339// ============================================================
7340//  TEMPERATURE INVERSION (valley fog)
7341// ============================================================
7342
7343/// Compute temperature inversion areas (valley bottoms colder than surroundings)
7344pub fn compute_temperature_inversion(
7345    hmap:     &Heightmap,
7346    temp_map: &[f32],
7347    acc_map:  &[u32],   // flow accumulation
7348    threshold: u32,     // flow accumulation threshold for valley detection
7349) -> Vec<f32> {
7350    let w = hmap.width;
7351    let h = hmap.height;
7352    let mut inv_map = vec![0.0f32; w * h];
7353
7354    for y in 1..h-1 {
7355        for x in 1..w-1 {
7356            let idx = y * w + x;
7357            if acc_map[idx] < threshold { continue; }
7358            // This cell has high flow accumulation (valley)
7359            let alt = hmap.get(x, y);
7360            // Surrounding cells average height
7361            let mut sum_h = 0.0f32;
7362            let mut cnt   = 0;
7363            for &(dx, dy) in &[(2i32,0i32),(-2,0),(0,2),(0,-2)] {
7364                let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as usize;
7365                let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as usize;
7366                sum_h += hmap.get(nx, ny);
7367                cnt   += 1;
7368            }
7369            let avg_h = sum_h / cnt as f32;
7370            let inv   = (avg_h - alt).max(0.0) * 20.0; // 20°C per unit height inversion
7371            inv_map[idx] = inv;
7372        }
7373    }
7374    inv_map
7375}
7376
7377// ============================================================
7378//  EROSION SEDIMENT FLUX MAP
7379// ============================================================
7380
7381/// Compute sediment flux (how much sediment passes through each cell) from flow acc
7382pub fn compute_sediment_flux(
7383    hmap:    &Heightmap,
7384    acc_map: &[u32],
7385    k:       f32,   // erodibility constant
7386    m:       f32,   // drainage area exponent (typically 0.5)
7387    n:       f32,   // slope exponent (typically 1.0)
7388    cell_size: f32,
7389) -> Vec<f32> {
7390    let w = hmap.width;
7391    let h = hmap.height;
7392    let mut flux = vec![0.0f32; w * h];
7393    for y in 1..h-1 {
7394        for x in 1..w-1 {
7395            let idx   = y * w + x;
7396            let slope = hmap.slope_at(x, y, cell_size);
7397            let area  = acc_map[idx] as f32 * cell_size * cell_size;
7398            flux[idx] = k * area.powf(m) * slope.powf(n);
7399        }
7400    }
7401    flux
7402}
7403
7404// ============================================================
7405//  EXTRA MATH FUNCTIONS
7406// ============================================================
7407
7408/// Linear congruential pseudo-random on f32 input (useful for hash-based shaders)
7409#[inline]
7410pub fn hash_f32(x: f32) -> f32 {
7411    let mut h = (x.to_bits() ^ 0x9e3779b9u32).wrapping_mul(0x6c62272e);
7412    h ^= h >> 16;
7413    h = h.wrapping_mul(0x45d9f3b);
7414    h ^= h >> 16;
7415    (h as f32) / u32::MAX as f32
7416}
7417
7418#[inline]
7419pub fn hash_vec2(v: Vec2) -> f32 {
7420    hash_f32(v.x * 127.1 + v.y * 311.7)
7421}
7422
7423#[inline]
7424pub fn hash_vec3(v: Vec3) -> f32 {
7425    hash_f32(v.x * 127.1 + v.y * 311.7 + v.z * 74.7)
7426}
7427
7428/// Integer hash (Wang hash)
7429#[inline]
7430pub fn wang_hash(mut n: u32) -> u32 {
7431    n = (n ^ 61) ^ (n >> 16);
7432    n = n.wrapping_mul(9);
7433    n ^= n >> 4;
7434    n = n.wrapping_mul(0x27d4eb2d);
7435    n ^= n >> 15;
7436    n
7437}
7438
7439/// Float from integer hash, [0, 1)
7440#[inline]
7441pub fn float_from_hash(hash: u32) -> f32 {
7442    (hash as f32) / 4_294_967_296.0
7443}
7444
7445/// Vectorized clamp
7446#[inline]
7447pub fn clamp_vec3(v: Vec3, lo: Vec3, hi: Vec3) -> Vec3 {
7448    Vec3::new(v.x.clamp(lo.x, hi.x), v.y.clamp(lo.y, hi.y), v.z.clamp(lo.z, hi.z))
7449}
7450
7451/// Reflect a vector across a normal
7452#[inline]
7453pub fn reflect(v: Vec3, n: Vec3) -> Vec3 {
7454    v - n * (2.0 * v.dot(n))
7455}
7456
7457/// Refract a vector (Snell's law), returns None for total internal reflection
7458pub fn refract(v: Vec3, n: Vec3, eta: f32) -> Option<Vec3> {
7459    let cos_i  = -v.dot(n);
7460    let sin2_t = eta * eta * (1.0 - cos_i * cos_i);
7461    if sin2_t > 1.0 { return None; }
7462    let cos_t  = (1.0 - sin2_t).sqrt();
7463    Some(v * eta + n * (eta * cos_i - cos_t))
7464}
7465
7466/// Fresnel reflectance (Schlick approximation)
7467#[inline]
7468pub fn fresnel_schlick(cos_theta: f32, r0: f32) -> f32 {
7469    r0 + (1.0 - r0) * (1.0 - cos_theta).powi(5)
7470}
7471
7472/// Frenet-Serret frame along a path
7473pub fn frenet_frame(tangent: Vec3, up_hint: Vec3) -> (Vec3, Vec3, Vec3) {
7474    let t  = tangent.normalize();
7475    let b  = t.cross(up_hint).normalize();
7476    let n  = b.cross(t);
7477    (t, n, b) // tangent, normal, binormal
7478}
7479
7480// ============================================================
7481//  MATERIAL SYSTEM FOR TERRAIN RENDERING
7482// ============================================================
7483
7484#[derive(Clone, Debug)]
7485pub struct TerrainMaterial {
7486    pub albedo_color:    Vec4,
7487    pub roughness:       f32,
7488    pub metallic:        f32,
7489    pub normal_strength: f32,
7490    pub displacement:    f32,
7491    pub tiling_scale:    Vec2,
7492    pub texture_ids:     [u32; 4],  // albedo, normal, roughness, displacement
7493}
7494
7495impl TerrainMaterial {
7496    pub fn default_grass() -> Self {
7497        TerrainMaterial { albedo_color: Vec4::new(0.20, 0.55, 0.12, 1.0), roughness: 0.85, metallic: 0.0,
7498            normal_strength: 0.8, displacement: 0.05, tiling_scale: Vec2::new(8.0, 8.0), texture_ids: [0,1,2,3] }
7499    }
7500    pub fn default_rock() -> Self {
7501        TerrainMaterial { albedo_color: Vec4::new(0.50, 0.45, 0.40, 1.0), roughness: 0.90, metallic: 0.0,
7502            normal_strength: 1.2, displacement: 0.15, tiling_scale: Vec2::new(4.0, 4.0), texture_ids: [4,5,6,7] }
7503    }
7504    pub fn default_snow() -> Self {
7505        TerrainMaterial { albedo_color: Vec4::new(0.95, 0.97, 1.0, 1.0), roughness: 0.30, metallic: 0.0,
7506            normal_strength: 0.3, displacement: 0.02, tiling_scale: Vec2::new(6.0, 6.0), texture_ids: [8,9,10,11] }
7507    }
7508    pub fn default_sand() -> Self {
7509        TerrainMaterial { albedo_color: Vec4::new(0.87, 0.79, 0.55, 1.0), roughness: 0.95, metallic: 0.0,
7510            normal_strength: 0.5, displacement: 0.08, tiling_scale: Vec2::new(10.0, 10.0), texture_ids: [12,13,14,15] }
7511    }
7512    pub fn default_water() -> Self {
7513        TerrainMaterial { albedo_color: Vec4::new(0.10, 0.35, 0.65, 0.85), roughness: 0.05, metallic: 0.0,
7514            normal_strength: 1.5, displacement: 0.0, tiling_scale: Vec2::new(20.0, 20.0), texture_ids: [16,17,18,19] }
7515    }
7516
7517    /// Blend two materials by a weight [0..1]
7518    pub fn blend(&self, other: &TerrainMaterial, t: f32) -> TerrainMaterial {
7519        let lf = |a: f32, b: f32| a + (b - a) * t;
7520        let lv4 = |a: Vec4, b: Vec4| a + (b - a) * t;
7521        let lv2 = |a: Vec2, b: Vec2| a + (b - a) * t;
7522        TerrainMaterial {
7523            albedo_color:    lv4(self.albedo_color, other.albedo_color),
7524            roughness:       lf(self.roughness, other.roughness),
7525            metallic:        lf(self.metallic, other.metallic),
7526            normal_strength: lf(self.normal_strength, other.normal_strength),
7527            displacement:    lf(self.displacement, other.displacement),
7528            tiling_scale:    lv2(self.tiling_scale, other.tiling_scale),
7529            texture_ids:     if t < 0.5 { self.texture_ids } else { other.texture_ids },
7530        }
7531    }
7532}
7533
7534// ============================================================
7535//  EXTRA EDITOR OBJECT METHODS
7536// ============================================================
7537
7538impl WorldEditor {
7539    /// Place a volcano at a world position
7540    pub fn place_volcano(&mut self, world_x: f32, world_z: f32, radius: f32, rim_height: f32, caldera_depth: f32) {
7541        let stamp_size = (radius * 2.0 / self.cell_size) as usize + 4;
7542        let stamp = volcano_stamp(stamp_size, 0.6, rim_height / self.height_scale, caldera_depth / self.height_scale);
7543        let cx = world_x / self.cell_size;
7544        let cz = world_z / self.cell_size;
7545        let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7546        self.undo_redo.push(action);
7547        self.heightmap_dirty = true;
7548    }
7549
7550    /// Place a mesa (flat-topped plateau) at a world position
7551    pub fn place_mesa(&mut self, world_x: f32, world_z: f32, radius: f32, height: f32, steepness: f32) {
7552        let stamp_size = (radius * 2.5 / self.cell_size) as usize + 4;
7553        let stamp = mesa_stamp(stamp_size, 0.5, steepness, height / self.height_scale);
7554        let cx = world_x / self.cell_size;
7555        let cz = world_z / self.cell_size;
7556        let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7557        self.undo_redo.push(action);
7558        self.heightmap_dirty = true;
7559    }
7560
7561    /// Place an impact crater at a world position
7562    pub fn place_crater(&mut self, world_x: f32, world_z: f32, radius: f32, rim_height: f32, depth: f32) {
7563        let stamp_size = (radius * 3.0 / self.cell_size) as usize + 4;
7564        let stamp = crater_stamp(stamp_size, 0.55, rim_height / self.height_scale, depth / self.height_scale);
7565        let cx = world_x / self.cell_size;
7566        let cz = world_z / self.cell_size;
7567        let action = terrain_stamp(&mut self.heightmap, cx, cz, &stamp, stamp_size, stamp_size, 1.0);
7568        self.undo_redo.push(action);
7569        self.heightmap_dirty = true;
7570    }
7571
7572    /// Fill terrain sinks (hydrological preprocessing)
7573    pub fn fill_terrain_sinks(&mut self) {
7574        fill_sinks(&mut self.heightmap, 0.0001);
7575        self.heightmap_dirty = true;
7576    }
7577
7578    /// Compute statistics for a specific region
7579    pub fn stats_for_region(&self, x0: usize, y0: usize, x1: usize, y1: usize) -> WorldStats {
7580        let w = self.heightmap.width;
7581        let h = self.heightmap.height;
7582        let x1 = x1.min(w);
7583        let y1 = y1.min(h);
7584        let total = (x1 - x0) * (y1 - y0);
7585        let mut ocean = 0usize;
7586        let mut mountain = 0usize;
7587        let mut sum = 0.0f64;
7588        let mut min_h = f32::MAX;
7589        let mut max_h = f32::MIN;
7590        for y in y0..y1 { for x in x0..x1 {
7591            let ht = self.heightmap.get(x, y);
7592            sum += ht as f64;
7593            if ht < min_h { min_h = ht; }
7594            if ht > max_h { max_h = ht; }
7595            if ht <= self.sea_level { ocean += 1; }
7596            if ht > 0.7 { mountain += 1; }
7597        }}
7598        WorldStats {
7599            total_cells: total, ocean_cells: ocean, land_cells: total - ocean, mountain_cells: mountain,
7600            river_count: 0, lake_count: 0, road_segments: 0, road_total_length: 0.0,
7601            foliage_count: 0, min_height: min_h, max_height: max_h,
7602            mean_height: (sum / total as f64) as f32, dominant_biome: None,
7603        }
7604    }
7605
7606    /// Smooth the entire heightmap with a Gaussian filter
7607    pub fn gaussian_smooth(&mut self, sigma: f32) {
7608        let w = self.heightmap.width;
7609        let h = self.heightmap.height;
7610        let old = self.heightmap.data.clone();
7611        let smoothed = gaussian_blur_2d(&old, w, h, sigma);
7612        let x0 = 0; let y0 = 0;
7613        let action = EditAction::SetHeightRegion {
7614            x: 0, y: 0, width: w, height: h,
7615            old_data: old,
7616            new_data: smoothed.clone(),
7617        };
7618        self.heightmap.data = smoothed;
7619        self.heightmap.recompute_minmax();
7620        self.undo_redo.push(action);
7621        self.heightmap_dirty = true;
7622    }
7623
7624    /// Generate full navigation data for the world
7625    pub fn build_nav_grid(&self, max_slope_deg: f32) -> NavGrid {
7626        let mut grid = NavGrid::from_heightmap(&self.heightmap, self.cell_size, self.sea_level, max_slope_deg);
7627        grid.label_regions();
7628        grid
7629    }
7630
7631    /// Get biome statistics (percentage of each biome type)
7632    pub fn biome_percentages(&self) -> [(BiomeId, f32); 25] {
7633        let w = self.heightmap.width;
7634        let h = self.heightmap.height;
7635        let total = (w * h) as f32;
7636        let mut counts = [0usize; 25];
7637        for y in 0..h { for x in 0..w {
7638            let idx  = y * w + x;
7639            let alt  = self.heightmap.get(x, y);
7640            let temp = if idx < self.temperature_map.len() { self.temperature_map[idx] } else { 15.0 };
7641            let hum  = if idx < self.humidity_map.len()    { self.humidity_map[idx]    } else { 0.5  };
7642            let biome = BiomeDescriptor::classify_point(temp, hum, alt);
7643            counts[biome as usize] += 1;
7644        }}
7645        [
7646            (BiomeId::TropicalRainforest,  counts[0]  as f32 / total),
7647            (BiomeId::TropicalSavanna,     counts[1]  as f32 / total),
7648            (BiomeId::HotDesert,           counts[2]  as f32 / total),
7649            (BiomeId::ColdDesert,          counts[3]  as f32 / total),
7650            (BiomeId::XericShrubland,      counts[4]  as f32 / total),
7651            (BiomeId::MediterraneanShrub,  counts[5]  as f32 / total),
7652            (BiomeId::TemperateGrassland,  counts[6]  as f32 / total),
7653            (BiomeId::TemperateRainforest, counts[7]  as f32 / total),
7654            (BiomeId::TemperateDeciduous,  counts[8]  as f32 / total),
7655            (BiomeId::BorealForest,        counts[9]  as f32 / total),
7656            (BiomeId::TaigaSpruce,         counts[10] as f32 / total),
7657            (BiomeId::Tundra,              counts[11] as f32 / total),
7658            (BiomeId::ArcticDesert,        counts[12] as f32 / total),
7659            (BiomeId::AlpineMeadow,        counts[13] as f32 / total),
7660            (BiomeId::AlpineTundra,        counts[14] as f32 / total),
7661            (BiomeId::PolarIceCap,         counts[15] as f32 / total),
7662            (BiomeId::Mangrove,            counts[16] as f32 / total),
7663            (BiomeId::Wetland,             counts[17] as f32 / total),
7664            (BiomeId::FloodPlain,          counts[18] as f32 / total),
7665            (BiomeId::VolcanicLandscape,   counts[19] as f32 / total),
7666            (BiomeId::SaltFlat,            counts[20] as f32 / total),
7667            (BiomeId::GlacialValley,       counts[21] as f32 / total),
7668            (BiomeId::CoastalDunes,        counts[22] as f32 / total),
7669            (BiomeId::DeepOceanFloor,      counts[23] as f32 / total),
7670            (BiomeId::CoralReef,           counts[24] as f32 / total),
7671        ]
7672    }
7673}
7674
7675// ============================================================
7676//  ATMOSPHERIC SCATTERING LOOKUP TABLE
7677// ============================================================
7678
7679/// Pre-bake transmittance table for faster sky rendering
7680pub struct TransmittanceLut {
7681    pub width:  usize,
7682    pub height: usize,
7683    pub data:   Vec<[f32; 3]>,  // RGB transmittance per sample
7684}
7685
7686impl TransmittanceLut {
7687    pub fn bake(params: &AtmosphereParams, width: usize, height: usize) -> Self {
7688        let mut data = vec![[0.0f32; 3]; width * height];
7689
7690        for v_idx in 0..height {
7691            for u_idx in 0..width {
7692                // u = altitude fraction [0..1], v = cos(zenith angle) [-1..1]
7693                let u         = u_idx as f64 / (width  - 1) as f64;
7694                let v         = v_idx as f64 / (height - 1) as f64 * 2.0 - 1.0;
7695                let altitude_km = u * (params.atmo_radius - params.planet_radius);
7696                let cos_zenith  = v;
7697
7698                let h = altitude_km;
7699                let hr = (-(h / params.rayleigh_scale_height)).exp();
7700                let hm = (-(h / params.mie_scale_height)).exp();
7701
7702                // Simple path length approximation
7703                let path_len = if cos_zenith.abs() < 1e-6 {
7704                    params.atmo_radius - params.planet_radius
7705                } else {
7706                    ((params.atmo_radius * params.atmo_radius
7707                      - (params.planet_radius + h) * (params.planet_radius + h) * (1.0 - cos_zenith * cos_zenith)).sqrt()
7708                     - (params.planet_radius + h) * cos_zenith).max(0.0)
7709                };
7710
7711                let tau_r = [
7712                    params.rayleigh_coeff[0] * hr * path_len,
7713                    params.rayleigh_coeff[1] * hr * path_len,
7714                    params.rayleigh_coeff[2] * hr * path_len,
7715                ];
7716                let tau_m_val = 1.1 * params.mie_coeff * hm * path_len;
7717
7718                data[v_idx * width + u_idx] = [
7719                    (-(tau_r[0] + tau_m_val)).exp() as f32,
7720                    (-(tau_r[1] + tau_m_val)).exp() as f32,
7721                    (-(tau_r[2] + tau_m_val)).exp() as f32,
7722                ];
7723            }
7724        }
7725
7726        TransmittanceLut { width, height, data }
7727    }
7728
7729    pub fn sample(&self, altitude_norm: f32, cos_zenith: f32) -> Vec3 {
7730        let u = altitude_norm.clamp(0.0, 1.0) * (self.width  - 1) as f32;
7731        let v = ((cos_zenith + 1.0) * 0.5).clamp(0.0, 1.0) * (self.height - 1) as f32;
7732        let x0 = u.floor() as usize;
7733        let y0 = v.floor() as usize;
7734        let x1 = (x0 + 1).min(self.width  - 1);
7735        let y1 = (y0 + 1).min(self.height - 1);
7736        let tx = u - x0 as f32;
7737        let ty = v - y0 as f32;
7738        let s = |xi: usize, yi: usize| { let d = self.data[yi * self.width + xi]; Vec3::new(d[0], d[1], d[2]) };
7739        let a = s(x0, y0).lerp(s(x1, y0), tx);
7740        let b = s(x0, y1).lerp(s(x1, y1), tx);
7741        a.lerp(b, ty)
7742    }
7743}
7744
7745// ============================================================
7746//  FINAL CONSTANTS AND VERSION INFO
7747// ============================================================
7748
7749pub const WORLD_EDITOR_VERSION: &str = "0.1.0";
7750pub const WORLD_EDITOR_BUILD: u32    = 10001;
7751
7752
7753/// Returns a short version string
7754pub fn editor_version() -> String {
7755    format!("WorldEditor v{} (build {})", WORLD_EDITOR_VERSION, WORLD_EDITOR_BUILD)
7756}
7757
7758/// Distance between two grid cells in world units
7759#[inline]
7760pub fn grid_distance(x0: usize, y0: usize, x1: usize, y1: usize, cell_size: f32) -> f32 {
7761    let dx = (x1 as i32 - x0 as i32) as f32;
7762    let dy = (y1 as i32 - y0 as i32) as f32;
7763    (dx*dx + dy*dy).sqrt() * cell_size
7764}
7765
7766/// Check if two AABB volumes overlap
7767#[inline]
7768pub fn aabb_overlap(min_a: Vec3, max_a: Vec3, min_b: Vec3, max_b: Vec3) -> bool {
7769    min_a.x <= max_b.x && max_a.x >= min_b.x &&
7770    min_a.y <= max_b.y && max_a.y >= min_b.y &&
7771    min_a.z <= max_b.z && max_a.z >= min_b.z
7772}
7773
7774/// Compute the area of a triangle given three 2D vertices
7775#[inline]
7776pub fn triangle_area_2d(a: Vec2, b: Vec2, c: Vec2) -> f32 {
7777    ((b - a).perp_dot(c - a)).abs() * 0.5
7778}
7779
7780/// Barycentric coordinates of point P in triangle (A, B, C)
7781pub fn barycentric(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> Vec3 {
7782    let v0 = c - a;
7783    let v1 = b - a;
7784    let v2 = p - a;
7785    let dot00 = v0.dot(v0);
7786    let dot01 = v0.dot(v1);
7787    let dot02 = v0.dot(v2);
7788    let dot11 = v1.dot(v1);
7789    let dot12 = v1.dot(v2);
7790    let inv_denom = 1.0 / (dot00 * dot11 - dot01 * dot01);
7791    let u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
7792    let v = (dot00 * dot12 - dot01 * dot02) * inv_denom;
7793    Vec3::new(1.0 - u - v, v, u)
7794}
7795
7796/// Point-in-triangle test using barycentric coordinates
7797#[inline]
7798pub fn point_in_triangle(p: Vec2, a: Vec2, b: Vec2, c: Vec2) -> bool {
7799    let bary = barycentric(p, a, b, c);
7800    bary.x >= 0.0 && bary.y >= 0.0 && bary.z >= 0.0
7801}
7802
7803/// Clamp a point to the boundary of an AABB
7804#[inline]
7805pub fn clamp_to_aabb(p: Vec3, min: Vec3, max: Vec3) -> Vec3 {
7806    Vec3::new(p.x.clamp(min.x, max.x), p.y.clamp(min.y, max.y), p.z.clamp(min.z, max.z))
7807}
7808
7809/// Signed distance from a point to a plane
7810#[inline]
7811pub fn signed_distance_to_plane(point: Vec3, plane_normal: Vec3, plane_d: f32) -> f32 {
7812    plane_normal.dot(point) + plane_d
7813}
7814
7815// ============================================================
7816//  SPLINE PATH EDITOR TOOL
7817// ============================================================
7818
7819#[derive(Clone, Debug)]
7820pub struct SplinePath {
7821    pub id:           u32,
7822    pub control_pts:  Vec<Vec3>,
7823    pub name:         String,
7824    pub closed:       bool,
7825    pub tangents:     Vec<Vec3>,
7826}
7827
7828impl SplinePath {
7829    pub fn new(id: u32, name: &str) -> Self {
7830        SplinePath { id, control_pts: Vec::new(), name: name.into(), closed: false, tangents: Vec::new() }
7831    }
7832
7833    pub fn add_point(&mut self, p: Vec3) {
7834        self.control_pts.push(p);
7835        self.recompute_tangents();
7836    }
7837
7838    pub fn remove_point(&mut self, idx: usize) {
7839        if idx < self.control_pts.len() {
7840            self.control_pts.remove(idx);
7841            self.recompute_tangents();
7842        }
7843    }
7844
7845    pub fn move_point(&mut self, idx: usize, new_pos: Vec3) {
7846        if idx < self.control_pts.len() {
7847            self.control_pts[idx] = new_pos;
7848            self.recompute_tangents();
7849        }
7850    }
7851
7852    pub fn recompute_tangents(&mut self) {
7853        let n = self.control_pts.len();
7854        self.tangents = vec![Vec3::ZERO; n];
7855        if n < 2 { return; }
7856        for i in 0..n {
7857            let prev = if i == 0 { self.control_pts[0] } else { self.control_pts[i - 1] };
7858            let next = if i == n-1 { self.control_pts[n-1] } else { self.control_pts[i + 1] };
7859            self.tangents[i] = (next - prev).normalize_or_zero();
7860        }
7861    }
7862
7863    pub fn evaluate(&self, t: f32) -> Vec3 {
7864        let n = self.control_pts.len();
7865        if n == 0 { return Vec3::ZERO; }
7866        if n == 1 { return self.control_pts[0]; }
7867        let total_t = if self.closed { n as f32 } else { (n - 1) as f32 };
7868        let t_clamped = t.clamp(0.0, 1.0) * total_t;
7869        let seg = t_clamped.floor() as usize;
7870        let local_t = t_clamped - seg as f32;
7871        let i0 = seg.min(n - 1);
7872        let i1 = (seg + 1).min(n - 1);
7873        let p0 = self.control_pts[i0];
7874        let p1 = self.control_pts[i1];
7875        let tan0 = self.tangents[i0] * (p1 - p0).length() * 0.3;
7876        let tan1 = self.tangents[i1] * (p1 - p0).length() * 0.3;
7877        // Hermite interpolation
7878        let h00 =  2.0 * local_t.powi(3) - 3.0 * local_t.powi(2) + 1.0;
7879        let h10 =       local_t.powi(3) - 2.0 * local_t.powi(2) + local_t;
7880        let h01 = -2.0 * local_t.powi(3) + 3.0 * local_t.powi(2);
7881        let h11 =       local_t.powi(3) -       local_t.powi(2);
7882        p0 * h00 + tan0 * h10 + p1 * h01 + tan1 * h11
7883    }
7884
7885    pub fn arc_length(&self, steps_per_seg: usize) -> f32 {
7886        let n = self.control_pts.len();
7887        if n < 2 { return 0.0; }
7888        let total_steps = (n - 1) * steps_per_seg;
7889        let mut len  = 0.0f32;
7890        let mut prev = self.evaluate(0.0);
7891        for i in 1..=total_steps {
7892            let t    = i as f32 / total_steps as f32;
7893            let curr = self.evaluate(t);
7894            len += (curr - prev).length();
7895            prev = curr;
7896        }
7897        len
7898    }
7899
7900    /// Sample evenly-spaced points along the spline
7901    pub fn sample_uniform(&self, count: usize) -> Vec<Vec3> {
7902        if count == 0 { return Vec::new(); }
7903        if count == 1 { return vec![self.evaluate(0.5)]; }
7904        (0..count).map(|i| self.evaluate(i as f32 / (count - 1) as f32)).collect()
7905    }
7906}
7907
7908// ============================================================
7909//  HEIGHTMAP OPERATION QUEUE (async-style)
7910// ============================================================
7911
7912#[derive(Clone, Debug)]
7913pub enum HeightmapOp {
7914    Noise      { params: FbmParams, offset: Vec2 },
7915    Erosion    { params: ErosionParams },
7916    Thermal    { iterations: usize, talus_deg: f32 },
7917    Blur       { sigma: f32 },
7918    Normalize,
7919    Clamp      { min: f32, max: f32 },
7920    Multiply   { factor: f32 },
7921    Add        { value: f32 },
7922    FillSinks,
7923    IslandMask { falloff: f32 },
7924}
7925
7926pub struct HeightmapOpQueue {
7927    pub ops:   VecDeque<HeightmapOp>,
7928    pub dirty: bool,
7929}
7930
7931impl HeightmapOpQueue {
7932    pub fn new() -> Self { HeightmapOpQueue { ops: VecDeque::new(), dirty: false } }
7933
7934    pub fn push(&mut self, op: HeightmapOp) { self.ops.push_back(op); self.dirty = true; }
7935
7936    pub fn execute_all(&mut self, hmap: &mut Heightmap) {
7937        while let Some(op) = self.ops.pop_front() {
7938            match op {
7939                HeightmapOp::Noise { params, offset } => {
7940                    hmap.generate_fbm(&params, offset);
7941                }
7942                HeightmapOp::Erosion { params } => {
7943                    hydraulic_erosion(hmap, &params);
7944                }
7945                HeightmapOp::Thermal { iterations, talus_deg } => {
7946                    thermal_erosion(hmap, iterations, talus_deg * DEG2RAD);
7947                }
7948                HeightmapOp::Blur { sigma } => {
7949                    let w = hmap.width; let h = hmap.height;
7950                    let blurred = gaussian_blur_2d(&hmap.data.clone(), w, h, sigma);
7951                    hmap.data = blurred;
7952                    hmap.recompute_minmax();
7953                }
7954                HeightmapOp::Normalize => {
7955                    hmap.normalize_to_01();
7956                }
7957                HeightmapOp::Clamp { min, max } => {
7958                    for v in hmap.data.iter_mut() { *v = v.clamp(min, max); }
7959                    hmap.recompute_minmax();
7960                }
7961                HeightmapOp::Multiply { factor } => {
7962                    for v in hmap.data.iter_mut() { *v = (*v * factor).clamp(0.0, 1.0); }
7963                    hmap.recompute_minmax();
7964                }
7965                HeightmapOp::Add { value } => {
7966                    for v in hmap.data.iter_mut() { *v = (*v + value).clamp(0.0, 1.0); }
7967                    hmap.recompute_minmax();
7968                }
7969                HeightmapOp::FillSinks => {
7970                    fill_sinks(hmap, 0.0001);
7971                }
7972                HeightmapOp::IslandMask { falloff } => {
7973                    let mask = generate_island_mask(hmap.width, hmap.height, falloff);
7974                    apply_mask(hmap, &mask);
7975                }
7976            }
7977        }
7978        self.dirty = false;
7979    }
7980}
7981
7982// ============================================================
7983//  SOUND SOURCE SYSTEM
7984// ============================================================
7985
7986#[derive(Clone, Debug)]
7987pub struct SoundSource {
7988    pub id:           u32,
7989    pub position:     Vec3,
7990    pub max_dist:     f32,
7991    pub base_volume:  f32,
7992    pub sound_id:     u32,
7993    pub looping:      bool,
7994    pub terrain_occ:  bool,
7995}
7996
7997impl SoundSource {
7998    pub fn volume_at(&self, listener: Vec3, hmap: &Heightmap, cell_size: f32, height_scale: f32) -> f32 {
7999        let dist = (self.position - listener).length();
8000        if dist >= self.max_dist { return 0.0; }
8001        let atten  = (1.0 - dist / self.max_dist).powi(2);
8002        if !self.terrain_occ { return (self.base_volume * atten).clamp(0.0, 1.0); }
8003
8004        let dir   = (listener - self.position).normalize();
8005        let steps = (dist / cell_size) as usize;
8006        let blocked = (1..steps).any(|s| {
8007            let p  = self.position + dir * s as f32 * cell_size;
8008            let ux = (p.x / (hmap.width  as f32 * cell_size)).clamp(0.0, 1.0);
8009            let uz = (p.z / (hmap.height as f32 * cell_size)).clamp(0.0, 1.0);
8010            hmap.sample_bilinear(ux, uz) * height_scale > p.y + 2.0
8011        });
8012        (self.base_volume * atten * if blocked { 0.15 } else { 1.0 }).clamp(0.0, 1.0)
8013    }
8014}
8015
8016// ============================================================
8017//  WATER CAUSTICS TEXTURE GENERATION
8018// ============================================================
8019
8020/// Generate an animated water caustics pattern using interference of waves
8021pub fn generate_caustics_pattern(width: usize, height: usize, time: f32, wave_count: usize) -> Vec<f32> {
8022    let mut out = vec![0.0f32; width * height];
8023    let mut rng = LcgRng::new(0xCA05710C);
8024
8025    let waves: Vec<(f32, f32, f32, f32)> = (0..wave_count).map(|_| {
8026        let angle = rng.next_f32() * TWO_PI;
8027        let freq  = 3.0 + rng.next_f32() * 8.0;
8028        let phase = rng.next_f32() * TWO_PI;
8029        let amp   = 0.5 + rng.next_f32() * 0.5;
8030        (angle, freq, phase, amp)
8031    }).collect();
8032
8033    for y in 0..height {
8034        for x in 0..width {
8035            let ux = x as f32 / width  as f32;
8036            let uy = y as f32 / height as f32;
8037            let mut v = 0.0f32;
8038            for &(angle, freq, phase, amp) in &waves {
8039                let proj = ux * angle.cos() + uy * angle.sin();
8040                v += amp * (proj * freq * TWO_PI + phase + time * 2.0).sin();
8041            }
8042            v = v / wave_count as f32 * 0.5 + 0.5;
8043            out[y * width + x] = v.powi(2); // sharpen caustics
8044        }
8045    }
8046    out
8047}
8048
8049// ============================================================
8050//  VEGETATION DISTRIBUTION BY SLOPE AND ALTITUDE
8051// ============================================================
8052
8053#[derive(Clone, Debug)]
8054pub struct VegetationRule {
8055    pub asset_id:    u32,
8056    pub name:        &'static str,
8057    pub min_alt:     f32,
8058    pub max_alt:     f32,
8059    pub min_slope:   f32,
8060    pub max_slope:   f32,
8061    pub min_temp:    f32,
8062    pub max_temp:    f32,
8063    pub min_hum:     f32,
8064    pub max_hum:     f32,
8065    pub density:     f32,
8066    pub min_radius:  f32,
8067}
8068
8069pub fn build_default_vegetation_rules() -> Vec<VegetationRule> {
8070    vec![
8071        VegetationRule { asset_id:0,  name:"Oak Tree",      min_alt:0.05, max_alt:0.60, min_slope:0.0, max_slope:0.5, min_temp:5.0,  max_temp:25.0, min_hum:0.40, max_hum:0.80, density:0.5, min_radius:4.0 },
8072        VegetationRule { asset_id:1,  name:"Pine Tree",     min_alt:0.20, max_alt:0.75, min_slope:0.0, max_slope:0.6, min_temp:-5.0, max_temp:15.0, min_hum:0.35, max_hum:0.75, density:0.6, min_radius:3.5 },
8073        VegetationRule { asset_id:2,  name:"Palm Tree",     min_alt:0.00, max_alt:0.20, min_slope:0.0, max_slope:0.3, min_temp:20.0, max_temp:40.0, min_hum:0.30, max_hum:0.80, density:0.4, min_radius:5.0 },
8074        VegetationRule { asset_id:3,  name:"Spruce",        min_alt:0.30, max_alt:0.70, min_slope:0.0, max_slope:0.5, min_temp:-15.0, max_temp:8.0, min_hum:0.40, max_hum:0.80, density:0.7, min_radius:3.0 },
8075        VegetationRule { asset_id:4,  name:"Cactus",        min_alt:0.00, max_alt:0.40, min_slope:0.0, max_slope:0.4, min_temp:15.0, max_temp:50.0, min_hum:0.00, max_hum:0.20, density:0.2, min_radius:2.0 },
8076        VegetationRule { asset_id:5,  name:"Birch",         min_alt:0.05, max_alt:0.55, min_slope:0.0, max_slope:0.5, min_temp:-5.0, max_temp:20.0, min_hum:0.45, max_hum:0.75, density:0.5, min_radius:3.5 },
8077        VegetationRule { asset_id:6,  name:"Bamboo",        min_alt:0.02, max_alt:0.35, min_slope:0.0, max_slope:0.4, min_temp:15.0, max_temp:35.0, min_hum:0.60, max_hum:1.00, density:0.8, min_radius:1.5 },
8078        VegetationRule { asset_id:7,  name:"Fern Shrub",    min_alt:0.00, max_alt:0.50, min_slope:0.0, max_slope:0.6, min_temp:5.0,  max_temp:30.0, min_hum:0.50, max_hum:1.00, density:0.7, min_radius:1.0 },
8079        VegetationRule { asset_id:8,  name:"Bush",          min_alt:0.00, max_alt:0.60, min_slope:0.0, max_slope:0.5, min_temp:0.0,  max_temp:35.0, min_hum:0.25, max_hum:0.75, density:0.6, min_radius:1.5 },
8080        VegetationRule { asset_id:9,  name:"Tundra Grass",  min_alt:0.00, max_alt:0.65, min_slope:0.0, max_slope:0.4, min_temp:-25.0, max_temp:5.0, min_hum:0.20, max_hum:0.60, density:0.5, min_radius:0.5 },
8081        VegetationRule { asset_id:10, name:"Tall Grass",    min_alt:0.00, max_alt:0.45, min_slope:0.0, max_slope:0.4, min_temp:5.0,  max_temp:30.0, min_hum:0.30, max_hum:0.70, density:0.9, min_radius:0.3 },
8082        VegetationRule { asset_id:11, name:"Reed",          min_alt:0.00, max_alt:0.10, min_slope:0.0, max_slope:0.1, min_temp:5.0,  max_temp:35.0, min_hum:0.75, max_hum:1.00, density:0.8, min_radius:0.5 },
8083        VegetationRule { asset_id:12, name:"Mangrove Root", min_alt:0.00, max_alt:0.08, min_slope:0.0, max_slope:0.1, min_temp:20.0, max_temp:36.0, min_hum:0.75, max_hum:1.00, density:0.6, min_radius:3.0 },
8084    ]
8085}
8086
8087pub fn apply_vegetation_rules(
8088    hmap:     &Heightmap,
8089    temp_map: &[f32],
8090    hum_map:  &[f32],
8091    rules:    &[VegetationRule],
8092    cell_size: f32,
8093    seed:     u64,
8094) -> Vec<FoliageInstance> {
8095    let mut result    = Vec::new();
8096    let w = hmap.width  as f32;
8097    let h = hmap.height as f32;
8098
8099    for (ri, rule) in rules.iter().enumerate() {
8100        let candidates = poisson_disk_2d(w, h, rule.min_radius, 30, seed ^ (ri as u64 * 31337));
8101        let mut rng    = LcgRng::new(seed ^ ri as u64 * 997);
8102
8103        for pos in &candidates {
8104            let ux  = (pos.x / w).clamp(0.0, 1.0);
8105            let uy  = (pos.y / h).clamp(0.0, 1.0);
8106            let alt = hmap.sample_bilinear(ux, uy);
8107            if alt < rule.min_alt || alt > rule.max_alt { continue; }
8108
8109            let xi    = (pos.x as usize).min(hmap.width  - 1);
8110            let yi    = (pos.y as usize).min(hmap.height - 1);
8111            let slope = hmap.slope_at(xi, yi, cell_size);
8112            if slope < rule.min_slope || slope > rule.max_slope { continue; }
8113
8114            let idx  = yi * hmap.width + xi;
8115            let temp = if idx < temp_map.len() { temp_map[idx] } else { 15.0 };
8116            let hum  = if idx < hum_map.len()  { hum_map[idx]  } else { 0.5  };
8117            if temp < rule.min_temp || temp > rule.max_temp { continue; }
8118            if hum  < rule.min_hum  || hum  > rule.max_hum  { continue; }
8119
8120            if rng.next_f32() > rule.density { continue; }
8121
8122            let angle = rng.next_f32() * TWO_PI;
8123            let sv    = 0.75 + rng.next_f32() * 0.5;
8124            result.push(FoliageInstance {
8125                position:  Vec3::new(pos.x * cell_size, alt * 500.0, pos.y * cell_size),
8126                rotation:  Quat::from_rotation_y(angle),
8127                scale:     Vec3::new(sv, sv * (0.8 + rng.next_f32() * 0.4), sv),
8128                asset_id:  rule.asset_id,
8129                biome_id:  0,
8130                lod_factor: 1.0,
8131            });
8132        }
8133    }
8134    result
8135}
8136
8137// ============================================================
8138//  TERRAIN LEVEL-OF-DETAIL DISTANCE BANDS
8139// ============================================================
8140
8141#[derive(Clone, Debug)]
8142pub struct LodBand {
8143    pub max_distance: f32,
8144    pub mesh_step:    usize,  // 1 = full, 2 = half, 4 = quarter
8145    pub texture_lod:  u8,
8146    pub foliage:      bool,
8147    pub shadows:      bool,
8148}
8149
8150pub const LOD_BANDS: [LodBand; 5] = [
8151    LodBand { max_distance:   50.0, mesh_step: 1, texture_lod: 0, foliage: true,  shadows: true  },
8152    LodBand { max_distance:  150.0, mesh_step: 1, texture_lod: 0, foliage: true,  shadows: true  },
8153    LodBand { max_distance:  400.0, mesh_step: 2, texture_lod: 1, foliage: true,  shadows: false },
8154    LodBand { max_distance: 1000.0, mesh_step: 4, texture_lod: 2, foliage: false, shadows: false },
8155    LodBand { max_distance: 3000.0, mesh_step: 8, texture_lod: 3, foliage: false, shadows: false },
8156];
8157
8158pub fn select_lod_band(distance: f32) -> &'static LodBand {
8159    for band in &LOD_BANDS {
8160        if distance < band.max_distance { return band; }
8161    }
8162    &LOD_BANDS[LOD_BANDS.len() - 1]
8163}
8164
8165// ============================================================
8166//  WATER SHIMMER / SPECULAR HIGHLIGHT COMPUTATION
8167// ============================================================
8168
8169/// Compute water specular highlight intensity for a view and sun direction
8170pub fn water_specular(view_dir: Vec3, sun_dir: Vec3, water_normal: Vec3, roughness: f32) -> f32 {
8171    let half_vec    = (view_dir + sun_dir).normalize();
8172    let n_dot_h     = water_normal.dot(half_vec).max(0.0);
8173    let alpha       = roughness * roughness;
8174    let alpha2      = alpha * alpha;
8175    let denom       = n_dot_h * n_dot_h * (alpha2 - 1.0) + 1.0;
8176    let ggx_ndf     = alpha2 / (PI * denom * denom);
8177    let n_dot_l     = water_normal.dot(sun_dir).max(0.0);
8178    let n_dot_v     = water_normal.dot(view_dir).max(0.0);
8179    let r0          = 0.02; // water Fresnel R0
8180    let fresnel     = fresnel_schlick(n_dot_v, r0);
8181    ggx_ndf * fresnel * n_dot_l
8182}
8183
8184/// Gerstner wave displacement for water surface
8185pub fn gerstner_wave(pos: Vec2, amplitude: f32, wavelength: f32, direction: Vec2, speed: f32, steepness: f32, time: f32) -> Vec3 {
8186    let k    = TWO_PI / wavelength;
8187    let c    = speed;
8188    let d    = direction.normalize();
8189    let f    = k * d.dot(pos) - c * time;
8190    let q   = steepness / (k * amplitude);
8191    Vec3::new(
8192        q * amplitude * d.x * f.cos(),
8193        amplitude * f.sin(),
8194        q * amplitude * d.y * f.cos(),
8195    )
8196}
8197
8198/// Sum multiple Gerstner waves for realistic water surface
8199pub fn gerstner_wave_sum(pos: Vec2, time: f32) -> Vec3 {
8200    let waves: [(f32, f32, Vec2, f32, f32); 4] = [
8201        (0.15, 8.0,  Vec2::new(1.0, 0.3).normalize(), 1.5, 0.3),
8202        (0.08, 5.0,  Vec2::new(0.5, 1.0).normalize(), 2.0, 0.25),
8203        (0.05, 3.0,  Vec2::new(-0.3, 1.0).normalize(), 2.5, 0.2),
8204        (0.03, 2.0,  Vec2::new(0.8, -0.5).normalize(), 3.0, 0.15),
8205    ];
8206    let mut disp = Vec3::ZERO;
8207    for &(amp, wl, dir, speed, steep) in &waves {
8208        disp += gerstner_wave(pos, amp, wl, dir, speed, steep, time);
8209    }
8210    disp
8211}
8212
8213// ============================================================
8214//  EXTRA WORLD EDITOR METHODS — FINAL BATCH
8215// ============================================================
8216
8217impl WorldEditor {
8218    /// Place vegetation according to rules database
8219    pub fn place_vegetation_by_rules(&mut self, seed: u64) {
8220        let rules = build_default_vegetation_rules();
8221        let instances = apply_vegetation_rules(
8222            &self.heightmap,
8223            &self.temperature_map,
8224            &self.humidity_map,
8225            &rules,
8226            self.cell_size,
8227            seed,
8228        );
8229        self.foliage.extend(instances);
8230        self.foliage_dirty = false;
8231    }
8232
8233    /// Build a Voronoi biome map
8234    pub fn build_voronoi_biome_map(&self, num_sites: usize) -> VoronoiMap {
8235        let mut vmap = VoronoiMap::generate(self.heightmap.width, self.heightmap.height, num_sites, self.master_seed ^ 0x707010);
8236        vmap.assign_biomes(&self.heightmap, &self.temperature_map, &self.humidity_map);
8237        vmap
8238    }
8239
8240    /// Get a material for a terrain cell based on slope/altitude/biome
8241    pub fn terrain_material_at(&self, x: usize, y: usize) -> TerrainMaterial {
8242        let alt   = self.heightmap.get(x, y);
8243        let slope = self.heightmap.slope_at(x, y, self.cell_size) * RAD2DEG;
8244        let snow  = self.compute_snow_map(0.75, 2.0);
8245        let w     = self.heightmap.width;
8246        let snow_v = if y * w + x < snow.len() { snow[y * w + x] } else { 0.0 };
8247
8248        let rock_blend   = smoothstep(25.0, 45.0, slope);
8249        let snow_blend   = snow_v;
8250        let water_blend  = if alt <= self.sea_level { 1.0 } else { 0.0 };
8251
8252        let grass = TerrainMaterial::default_grass();
8253        let rock  = TerrainMaterial::default_rock();
8254        let snow  = TerrainMaterial::default_snow();
8255        let water = TerrainMaterial::default_water();
8256
8257        if water_blend > 0.5 { return water; }
8258        let base = grass.blend(&rock, rock_blend);
8259        base.blend(&snow, snow_blend)
8260    }
8261
8262    /// Add metadata to the world
8263    pub fn set_metadata(&mut self, key: &str, value: &str) {
8264        self.metadata.insert(key.to_string(), value.to_string());
8265    }
8266
8267    pub fn get_metadata(&self, key: &str) -> Option<&String> {
8268        self.metadata.get(key)
8269    }
8270
8271    /// Simulate a single rain event (increases humidity, may trigger erosion)
8272    pub fn simulate_rain_event(&mut self, intensity: f32, duration_hours: f32) {
8273        // Increase humidity temporarily
8274        for v in self.humidity_map.iter_mut() {
8275            *v = (*v + intensity * 0.3).clamp(0.0, 1.0);
8276        }
8277        // Apply light erosion proportional to intensity
8278        if intensity > 0.5 {
8279            let mut ep = self.erosion_params.clone();
8280            ep.num_particles = (ep.num_particles as f32 * intensity * 0.5) as usize;
8281            hydraulic_erosion(&mut self.heightmap, &ep);
8282            self.heightmap_dirty = true;
8283        }
8284        // Weather state update
8285        self.weather.current.precipitation_mm += intensity * 10.0 * duration_hours;
8286        self.weather.current.humidity = (self.weather.current.humidity + intensity * 0.2).clamp(0.0, 1.0);
8287    }
8288
8289    /// Move all foliage onto the current terrain surface (after terrain edit)
8290    pub fn reseat_foliage_to_terrain(&mut self) {
8291        let new_ys: Vec<f32> = self.foliage.iter()
8292            .map(|fi| self.height_at_world(fi.position.x, fi.position.z))
8293            .collect();
8294        for (fi, new_y) in self.foliage.iter_mut().zip(new_ys) {
8295            fi.position.y = new_y;
8296        }
8297    }
8298
8299    /// Remove all foliage below sea level (e.g. after sea level change)
8300    pub fn cull_underwater_foliage(&mut self) {
8301        let sea_h = self.sea_level * self.height_scale;
8302        self.foliage.retain(|fi| fi.position.y >= sea_h - 0.5);
8303    }
8304
8305    /// Compute the total number of triangles in the terrain mesh at full LOD
8306    pub fn terrain_triangle_count(&self) -> usize {
8307        let w = self.heightmap.width;
8308        let h = self.heightmap.height;
8309        (w - 1) * (h - 1) * 2
8310    }
8311
8312    /// Estimate terrain memory usage in bytes
8313    pub fn terrain_memory_bytes(&self) -> usize {
8314        let hmap_bytes     = self.heightmap.data.len() * 4;
8315        let temp_bytes     = self.temperature_map.len() * 4;
8316        let hum_bytes      = self.humidity_map.len() * 4;
8317        let foliage_bytes  = self.foliage.len() * std::mem::size_of::<FoliageInstance>();
8318        hmap_bytes + temp_bytes + hum_bytes + foliage_bytes
8319    }
8320
8321    /// Recalculate all rivers from scratch using current heightmap
8322    pub fn recalculate_rivers(&mut self, num_rivers: usize) {
8323        self.rivers.clear();
8324        self.generate_rivers(num_rivers);
8325    }
8326
8327    /// Serialise all editor state to bytes
8328    pub fn full_save(&self) -> Vec<u8> {
8329        self.serialize()
8330    }
8331}
8332
8333// ============================================================
8334//  PERLIN NOISE 2D DERIVATIVE (for slope-based operations)
8335// ============================================================
8336
8337/// Returns (value, dvalue/dx, dvalue/dy) for analytical gradient
8338pub fn perlin_noise_2d_deriv(x: f32, y: f32) -> (f32, f32, f32) {
8339    let xi = x.floor() as i32;
8340    let yi = y.floor() as i32;
8341    let xf = x - xi as f32;
8342    let yf = y - yi as f32;
8343    let u  = fade(xf);
8344    let v  = fade(yf);
8345    // fade derivative: 30t^4 - 60t^3 + 30t^2
8346    let du = 30.0 * xf * xf * (xf * xf - 2.0 * xf + 1.0);
8347    let dv = 30.0 * yf * yf * (yf * yf - 2.0 * yf + 1.0);
8348
8349    let xi_u = (xi & 255) as usize;
8350    let yi_u = (yi & 255) as usize;
8351    let a  = PERM[xi_u     + PERM[yi_u    ] as usize];
8352    let b  = PERM[xi_u + 1 + PERM[yi_u    ] as usize];
8353    let c  = PERM[xi_u     + PERM[yi_u + 1] as usize];
8354    let d  = PERM[xi_u + 1 + PERM[yi_u + 1] as usize];
8355
8356    fn g2(h: u8, x: f32, y: f32) -> f32 {
8357        let hh = (h & 7) as usize;
8358        let gx: f32 = [1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 0.0, 0.0][hh];
8359        let gy: f32 = [0.0,  0.0, 1.0,  1.0,-1.0, -1.0, 1.0,-1.0][hh];
8360        gx * x + gy * y
8361    }
8362
8363    let a00 = g2(a, xf,       yf);
8364    let b00 = g2(b, xf - 1.0, yf);
8365    let a10 = g2(c, xf,       yf - 1.0);
8366    let b10 = g2(d, xf - 1.0, yf - 1.0);
8367
8368    let val  = lerp_f(lerp_f(a00, b00, u), lerp_f(a10, b10, u), v);
8369    let dx   = du * lerp_f(b00 - a00, b10 - a10, v)
8370             + u  * lerp_f(0.0,       0.0,        dv); // simplified
8371    let dy   = dv * (lerp_f(a10, b10, u) - lerp_f(a00, b00, u));
8372
8373    (val, dx, dy)
8374}
8375
8376// ============================================================
8377//  ADDITIONAL EDITOR CAMERA PRESETS
8378// ============================================================
8379
8380impl EditorCamera {
8381    pub fn preset_top_down(center: Vec3) -> Self {
8382        EditorCamera {
8383            position:    center + Vec3::new(0.0, 1000.0, 0.0),
8384            target:      center,
8385            up:          Vec3::new(0.0, 0.0, -1.0),
8386            fov_deg:     45.0,
8387            aspect:      16.0 / 9.0,
8388            near:        1.0,
8389            far:         20000.0,
8390            orbit_yaw:   0.0,
8391            orbit_pitch: 90.0,
8392            orbit_dist:  1000.0,
8393        }
8394    }
8395
8396    pub fn preset_horizon(center: Vec3) -> Self {
8397        let mut cam = EditorCamera {
8398            position:    center + Vec3::new(0.0, 200.0, 800.0),
8399            target:      center,
8400            up:          Vec3::Y,
8401            fov_deg:     70.0,
8402            aspect:      16.0 / 9.0,
8403            near:        0.5,
8404            far:         50000.0,
8405            orbit_yaw:   0.0,
8406            orbit_pitch: 15.0,
8407            orbit_dist:  800.0,
8408        };
8409        cam.update_orbit();
8410        cam
8411    }
8412
8413    pub fn clamp_to_terrain(&mut self, hmap: &Heightmap, cell_size: f32, height_scale: f32, min_height_above: f32) {
8414        let ux = (self.position.x / (hmap.width  as f32 * cell_size)).clamp(0.0, 1.0);
8415        let uz = (self.position.z / (hmap.height as f32 * cell_size)).clamp(0.0, 1.0);
8416        let terrain_h = hmap.sample_bilinear(ux, uz) * height_scale;
8417        if self.position.y < terrain_h + min_height_above {
8418            let diff = terrain_h + min_height_above - self.position.y;
8419            self.position.y += diff;
8420            self.target.y   += diff;
8421        }
8422    }
8423}
8424
8425
8426// ============================================================
8427//  RENDER SETTINGS
8428// ============================================================
8429
8430#[derive(Clone, Debug)]
8431pub struct WorldRenderSettings {
8432    pub enable_shadows:    bool,
8433    pub shadow_distance:   f32,
8434    pub shadow_cascades:   u8,
8435    pub enable_ao:         bool,
8436    pub ao_radius:         f32,
8437    pub ao_samples:        u32,
8438    pub enable_fog:        bool,
8439    pub fog_start:         f32,
8440    pub fog_end:           f32,
8441    pub fog_color:         Vec3,
8442    pub enable_bloom:      bool,
8443    pub bloom_threshold:   f32,
8444    pub bloom_intensity:   f32,
8445    pub exposure:          f32,
8446    pub gamma:             f32,
8447    pub tonemap_mode:      TonemapMode,
8448    pub enable_ssao:       bool,
8449    pub enable_motion_blur: bool,
8450    pub motion_blur_amount: f32,
8451    pub enable_vignette:   bool,
8452    pub vignette_strength: f32,
8453    pub enable_chromatic:  bool,
8454    pub chromatic_amount:  f32,
8455    pub water_tessellation: u8,
8456    pub terrain_max_lod:   u8,
8457    pub foliage_distance:  f32,
8458    pub foliage_density_scale: f32,
8459    pub sky_samples:       u32,
8460    pub render_wireframe:  bool,
8461    pub render_colliders:  bool,
8462    pub render_navmesh:    bool,
8463}
8464
8465#[derive(Clone, Debug, PartialEq, Eq)]
8466pub enum TonemapMode {
8467    Linear,
8468    Reinhard,
8469    ACES,
8470    Filmic,
8471    Uncharted2,
8472}
8473
8474impl Default for WorldRenderSettings {
8475    fn default() -> Self {
8476        WorldRenderSettings {
8477            enable_shadows: true, shadow_distance: 500.0, shadow_cascades: 4,
8478            enable_ao: true, ao_radius: 2.0, ao_samples: 16,
8479            enable_fog: true, fog_start: 200.0, fog_end: 4000.0, fog_color: Vec3::new(0.7, 0.8, 0.9),
8480            enable_bloom: true, bloom_threshold: 1.2, bloom_intensity: 0.4,
8481            exposure: 1.0, gamma: 2.2, tonemap_mode: TonemapMode::ACES,
8482            enable_ssao: true, enable_motion_blur: false, motion_blur_amount: 0.5,
8483            enable_vignette: true, vignette_strength: 0.3,
8484            enable_chromatic: false, chromatic_amount: 0.003,
8485            water_tessellation: 4, terrain_max_lod: 4,
8486            foliage_distance: 500.0, foliage_density_scale: 1.0,
8487            sky_samples: 16, render_wireframe: false, render_colliders: false, render_navmesh: false,
8488        }
8489    }
8490}
8491
8492// ============================================================
8493//  FINAL TRAIT IMPLEMENTATIONS AND MISC
8494// ============================================================
8495
8496
8497impl std::fmt::Display for BiomeDescriptor {
8498    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8499        write!(f, "Biome[{}] '{}' T:{:.0}..{:.0}°C H:{:.0}..{:.0}%",
8500            self.id as usize, self.name,
8501            self.temp_min, self.temp_max,
8502            self.humidity_min * 100.0, self.humidity_max * 100.0)
8503    }
8504}
8505
8506impl std::fmt::Display for RoadType {
8507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8508        write!(f, "{}", match self {
8509            RoadType::Dirt    => "Dirt",
8510            RoadType::Gravel  => "Gravel",
8511            RoadType::Paved   => "Paved",
8512            RoadType::Highway => "Highway",
8513            RoadType::Trail   => "Trail",
8514        })
8515    }
8516}
8517
8518impl std::fmt::Display for EditorTool {
8519    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8520        write!(f, "{:?}", self)
8521    }
8522}
8523
8524/// Utility: convert a normalised height value to a color using default terrain ramp
8525pub fn height_to_color(h: f32) -> Vec3 {
8526    ColorRamp::terrain_default().sample(h)
8527}
8528
8529/// Debug visualization: create a checkerboard pattern
8530pub fn checkerboard_pattern(width: usize, height: usize, cell_size: usize) -> Vec<f32> {
8531    (0..height).flat_map(|y| (0..width).map(move |x| {
8532        let cx = x / cell_size;
8533        let cy = y / cell_size;
8534        if (cx + cy) % 2 == 0 { 1.0 } else { 0.0 }
8535    })).collect()
8536}
8537
8538/// Compute bounding sphere of a set of points
8539pub fn bounding_sphere(points: &[Vec3]) -> (Vec3, f32) {
8540    if points.is_empty() { return (Vec3::ZERO, 0.0); }
8541    let center = points.iter().fold(Vec3::ZERO, |acc, &p| acc + p) / points.len() as f32;
8542    let radius = points.iter().map(|&p| (p - center).length()).fold(0.0f32, f32::max);
8543    (center, radius)
8544}
8545
8546/// Compute axis-aligned bounding box of a set of points
8547pub fn bounding_aabb(points: &[Vec3]) -> (Vec3, Vec3) {
8548    if points.is_empty() { return (Vec3::ZERO, Vec3::ZERO); }
8549    let mut mn = Vec3::splat(f32::MAX);
8550    let mut mx = Vec3::splat(f32::MIN);
8551    for &p in points {
8552        mn.x = mn.x.min(p.x); mn.y = mn.y.min(p.y); mn.z = mn.z.min(p.z);
8553        mx.x = mx.x.max(p.x); mx.y = mx.y.max(p.y); mx.z = mx.z.max(p.z);
8554    }
8555    (mn, mx)
8556}
8557
8558/// Uniform random point on unit sphere
8559pub fn random_on_sphere(rng: &mut LcgRng) -> Vec3 {
8560    loop {
8561        let v = Vec3::new(
8562            rng.next_f32() * 2.0 - 1.0,
8563            rng.next_f32() * 2.0 - 1.0,
8564            rng.next_f32() * 2.0 - 1.0,
8565        );
8566        let len = v.length();
8567        if len > 0.0001 && len <= 1.0 { return v / len; }
8568    }
8569}
8570
8571/// Random point on unit disk
8572pub fn random_on_disk(rng: &mut LcgRng) -> Vec2 {
8573    loop {
8574        let v = Vec2::new(rng.next_f32() * 2.0 - 1.0, rng.next_f32() * 2.0 - 1.0);
8575        if v.length_squared() <= 1.0 { return v; }
8576    }
8577}
8578
8579// ============================================================
8580//  END OF FILE
8581// ============================================================