Skip to main content

oxiphysics_geometry/heightfield/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6#[allow(unused_imports)]
7use super::functions_2::*;
8use oxiphysics_core::math::{Real, Vec3};
9
10#[allow(unused_imports)]
11use super::functions::*;
12use super::functions::{heightfield_tessellate, ray_aabb_xz, ray_triangle, tri_area};
13
14/// A grid-based height field for terrain representation.
15///
16/// Heights are stored in row-major order. The field spans from
17/// (0,0) to (scale_x * (cols-1), scale_z * (rows-1)) in the XZ plane.
18#[derive(Debug, Clone)]
19pub struct HeightField {
20    /// Height values in row-major order.
21    pub heights: Vec<Real>,
22    /// Number of rows (Z direction).
23    pub rows: usize,
24    /// Number of columns (X direction).
25    pub cols: usize,
26    /// Spacing in the X direction.
27    pub scale_x: Real,
28    /// Spacing in the Z direction.
29    pub scale_z: Real,
30}
31impl HeightField {
32    /// Create a new height field.
33    pub fn new(heights: Vec<Real>, rows: usize, cols: usize, scale_x: Real, scale_z: Real) -> Self {
34        assert_eq!(heights.len(), rows * cols);
35        Self {
36            heights,
37            rows,
38            cols,
39            scale_x,
40            scale_z,
41        }
42    }
43    /// Get the height at grid position (row, col).
44    pub fn height_at(&self, row: usize, col: usize) -> Real {
45        self.heights[row * self.cols + col]
46    }
47    /// Create a height field from a function.
48    ///
49    /// The function `f(col, row)` returns the height at each grid point.
50    #[allow(dead_code)]
51    pub fn from_fn(
52        cols: usize,
53        rows: usize,
54        cell_size: Real,
55        f: impl Fn(usize, usize) -> Real,
56    ) -> Self {
57        let mut heights = Vec::with_capacity(rows * cols);
58        for row in 0..rows {
59            for col in 0..cols {
60                heights.push(f(col, row));
61            }
62        }
63        Self {
64            heights,
65            rows,
66            cols,
67            scale_x: cell_size,
68            scale_z: cell_size,
69        }
70    }
71    /// Bilinear interpolation of height at normalized coordinates (u, v).
72    ///
73    /// `u` and `v` are in `[0, 1]`, mapping to the full extent of the grid.
74    #[allow(dead_code)]
75    pub fn height_at_uv(&self, u: Real, v: Real) -> Real {
76        let u = u.clamp(0.0, 1.0);
77        let v = v.clamp(0.0, 1.0);
78        let fx = u * (self.cols - 1) as Real;
79        let fz = v * (self.rows - 1) as Real;
80        let col0 = (fx as usize).min(self.cols - 2);
81        let row0 = (fz as usize).min(self.rows - 2);
82        let col1 = col0 + 1;
83        let row1 = row0 + 1;
84        let tx = fx - col0 as Real;
85        let tz = fz - row0 as Real;
86        let h00 = self.height_at(row0, col0);
87        let h10 = self.height_at(row0, col1);
88        let h01 = self.height_at(row1, col0);
89        let h11 = self.height_at(row1, col1);
90        let h0 = h00 * (1.0 - tx) + h10 * tx;
91        let h1 = h01 * (1.0 - tx) + h11 * tx;
92        h0 * (1.0 - tz) + h1 * tz
93    }
94    /// Compute the surface normal at grid point (col, row) via finite differences.
95    #[allow(dead_code)]
96    pub fn normal_at_grid(&self, col: usize, row: usize) -> [Real; 3] {
97        let dh_dx = if col == 0 {
98            (self.height_at(row, col + 1) - self.height_at(row, col)) / self.scale_x
99        } else if col == self.cols - 1 {
100            (self.height_at(row, col) - self.height_at(row, col - 1)) / self.scale_x
101        } else {
102            (self.height_at(row, col + 1) - self.height_at(row, col - 1)) / (2.0 * self.scale_x)
103        };
104        let dh_dz = if row == 0 {
105            (self.height_at(row + 1, col) - self.height_at(row, col)) / self.scale_z
106        } else if row == self.rows - 1 {
107            (self.height_at(row, col) - self.height_at(row - 1, col)) / self.scale_z
108        } else {
109            (self.height_at(row + 1, col) - self.height_at(row - 1, col)) / (2.0 * self.scale_z)
110        };
111        let nx = -dh_dx;
112        let ny = 1.0;
113        let nz = -dh_dz;
114        let len = (nx * nx + ny * ny + nz * nz).sqrt();
115        [nx / len, ny / len, nz / len]
116    }
117    /// Tessellate the height field into a triangle mesh.
118    ///
119    /// Returns (vertices, triangles) where each vertex is `[x, y, z]`
120    /// and each triangle is 3 vertex indices.
121    #[allow(dead_code)]
122    pub fn to_triangle_mesh(&self) -> (Vec<[Real; 3]>, Vec<[usize; 3]>) {
123        let mut vertices = Vec::with_capacity(self.rows * self.cols);
124        for row in 0..self.rows {
125            for col in 0..self.cols {
126                vertices.push([
127                    col as Real * self.scale_x,
128                    self.height_at(row, col),
129                    row as Real * self.scale_z,
130                ]);
131            }
132        }
133        let mut triangles = Vec::with_capacity((self.rows - 1) * (self.cols - 1) * 2);
134        for row in 0..(self.rows - 1) {
135            for col in 0..(self.cols - 1) {
136                let i00 = row * self.cols + col;
137                let i10 = row * self.cols + col + 1;
138                let i01 = (row + 1) * self.cols + col;
139                let i11 = (row + 1) * self.cols + col + 1;
140                triangles.push([i00, i10, i11]);
141                triangles.push([i00, i11, i01]);
142            }
143        }
144        (vertices, triangles)
145    }
146    /// Return the minimum height in the field.
147    #[allow(dead_code)]
148    pub fn min_height(&self) -> Real {
149        self.height_bounds().0
150    }
151    /// Return the maximum height in the field.
152    #[allow(dead_code)]
153    pub fn max_height(&self) -> Real {
154        self.height_bounds().1
155    }
156    /// Compute the total surface area by summing triangle areas from tessellation.
157    #[allow(dead_code)]
158    pub fn surface_area(&self) -> Real {
159        if self.rows < 2 || self.cols < 2 {
160            return 0.0;
161        }
162        let mut area = 0.0;
163        for row in 0..(self.rows - 1) {
164            for col in 0..(self.cols - 1) {
165                let v00 = [
166                    col as Real * self.scale_x,
167                    self.height_at(row, col),
168                    row as Real * self.scale_z,
169                ];
170                let v10 = [
171                    (col + 1) as Real * self.scale_x,
172                    self.height_at(row, col + 1),
173                    row as Real * self.scale_z,
174                ];
175                let v01 = [
176                    col as Real * self.scale_x,
177                    self.height_at(row + 1, col),
178                    (row + 1) as Real * self.scale_z,
179                ];
180                let v11 = [
181                    (col + 1) as Real * self.scale_x,
182                    self.height_at(row + 1, col + 1),
183                    (row + 1) as Real * self.scale_z,
184                ];
185                area += tri_area(&v00, &v10, &v11);
186                area += tri_area(&v00, &v11, &v01);
187            }
188        }
189        area
190    }
191    /// Apply Laplacian smoothing to the height field.
192    ///
193    /// Each interior height is replaced by the average of its 4-connected neighbors.
194    /// Boundary heights are unchanged.
195    #[allow(dead_code)]
196    pub fn smooth(&mut self, iterations: usize) {
197        for _ in 0..iterations {
198            let mut new_heights = self.heights.clone();
199            for row in 1..(self.rows - 1) {
200                for col in 1..(self.cols - 1) {
201                    let up = self.height_at(row - 1, col);
202                    let down = self.height_at(row + 1, col);
203                    let left = self.height_at(row, col - 1);
204                    let right = self.height_at(row, col + 1);
205                    new_heights[row * self.cols + col] = (up + down + left + right) / 4.0;
206                }
207            }
208            self.heights = new_heights;
209        }
210    }
211    /// Ray cast using grid DDA traversal and per-cell triangle intersection.
212    ///
213    /// Returns `Some((toi, [nx, ny, nz]))` for the closest hit within `max_toi`,
214    /// or `None` if no intersection.
215    #[allow(dead_code)]
216    pub fn ray_cast_grid(
217        &self,
218        origin: [Real; 3],
219        dir: [Real; 3],
220        max_toi: Real,
221    ) -> Option<(Real, [Real; 3])> {
222        if self.rows < 2 || self.cols < 2 {
223            return None;
224        }
225        let grid_w = (self.cols - 1) as Real * self.scale_x;
226        let grid_h = (self.rows - 1) as Real * self.scale_z;
227        let (t_enter, t_exit) = ray_aabb_xz(
228            origin[0], origin[2], dir[0], dir[2], grid_w, grid_h, max_toi,
229        )?;
230        let eps = 1e-6;
231        let mut t = t_enter.max(0.0);
232        let step = self.scale_x.min(self.scale_z) * 0.5;
233        let mut best: Option<(Real, [Real; 3])> = None;
234        while t <= t_exit.min(max_toi) {
235            let px = origin[0] + dir[0] * t;
236            let pz = origin[2] + dir[2] * t;
237            let col = ((px / self.scale_x).floor() as isize)
238                .max(0)
239                .min((self.cols - 2) as isize) as usize;
240            let row = ((pz / self.scale_z).floor() as isize)
241                .max(0)
242                .min((self.rows - 2) as isize) as usize;
243            let v00 = Vec3::new(
244                col as Real * self.scale_x,
245                self.height_at(row, col),
246                row as Real * self.scale_z,
247            );
248            let v10 = Vec3::new(
249                (col + 1) as Real * self.scale_x,
250                self.height_at(row, col + 1),
251                row as Real * self.scale_z,
252            );
253            let v01 = Vec3::new(
254                col as Real * self.scale_x,
255                self.height_at(row + 1, col),
256                (row + 1) as Real * self.scale_z,
257            );
258            let v11 = Vec3::new(
259                (col + 1) as Real * self.scale_x,
260                self.height_at(row + 1, col + 1),
261                (row + 1) as Real * self.scale_z,
262            );
263            let o = Vec3::new(origin[0], origin[1], origin[2]);
264            let d = Vec3::new(dir[0], dir[1], dir[2]);
265            for (a, b, c) in [(&v00, &v10, &v11), (&v00, &v11, &v01)] {
266                if let Some(hit) = ray_triangle(&o, &d, max_toi, a, b, c)
267                    && best.as_ref().is_none_or(|(bt, _)| hit.toi < *bt)
268                {
269                    let dot = hit.normal.dot(&d);
270                    let n = if dot > 0.0 {
271                        [-hit.normal.x, -hit.normal.y, -hit.normal.z]
272                    } else {
273                        [hit.normal.x, hit.normal.y, hit.normal.z]
274                    };
275                    best = Some((hit.toi, n));
276                }
277            }
278            if best.is_some() {
279                return best;
280            }
281            t += step + eps;
282        }
283        best
284    }
285    /// Get min and max heights.
286    pub(super) fn height_bounds(&self) -> (Real, Real) {
287        let mut min = Real::INFINITY;
288        let mut max = Real::NEG_INFINITY;
289        for &h in &self.heights {
290            if h < min {
291                min = h;
292            }
293            if h > max {
294                max = h;
295            }
296        }
297        (min, max)
298    }
299}
300impl HeightField {
301    /// Generate a downsampled (LOD) version of the height field.
302    ///
303    /// `factor` must be ≥ 1.  A factor of 2 halves the resolution by averaging
304    /// 2×2 blocks of cells.  If the grid is too small the original is returned.
305    #[allow(dead_code)]
306    pub fn lod_downsample(&self, factor: usize) -> HeightField {
307        if factor <= 1 {
308            return self.clone();
309        }
310        let new_cols = self.cols.div_ceil(factor);
311        let new_rows = self.rows.div_ceil(factor);
312        if new_cols < 2 || new_rows < 2 {
313            return self.clone();
314        }
315        let mut heights = Vec::with_capacity(new_rows * new_cols);
316        for r in 0..new_rows {
317            for c in 0..new_cols {
318                let r0 = r * factor;
319                let c0 = c * factor;
320                let r1 = (r0 + factor).min(self.rows);
321                let c1 = (c0 + factor).min(self.cols);
322                let mut sum = 0.0;
323                let mut count = 0usize;
324                for rr in r0..r1 {
325                    for cc in c0..c1 {
326                        sum += self.height_at(rr, cc);
327                        count += 1;
328                    }
329                }
330                heights.push(if count > 0 { sum / count as Real } else { 0.0 });
331            }
332        }
333        HeightField::new(
334            heights,
335            new_rows,
336            new_cols,
337            self.scale_x * factor as Real,
338            self.scale_z * factor as Real,
339        )
340    }
341    /// Generate multiple LOD levels.
342    ///
343    /// Returns a Vec where index 0 is the original, index 1 is 2× downsampled,
344    /// index 2 is 4× downsampled, etc., until the grid is too small.
345    #[allow(dead_code)]
346    pub fn lod_pyramid(&self, levels: usize) -> Vec<HeightField> {
347        let mut result = vec![self.clone()];
348        let mut factor = 2;
349        for _ in 1..levels {
350            let prev = result.last().expect("collection should not be empty");
351            if prev.rows < 4 || prev.cols < 4 {
352                break;
353            }
354            result.push(prev.lod_downsample(2));
355            factor *= 2;
356        }
357        let _ = factor;
358        result
359    }
360    /// Compute the normal at an arbitrary world-space XZ position via bilinear
361    /// interpolation of the four surrounding grid normals.
362    #[allow(dead_code)]
363    pub fn normal_at_world(&self, x: Real, z: Real) -> [Real; 3] {
364        let fx = (x / self.scale_x).clamp(0.0, (self.cols - 1) as Real);
365        let fz = (z / self.scale_z).clamp(0.0, (self.rows - 1) as Real);
366        let c0 = (fx as usize).min(self.cols - 1);
367        let r0 = (fz as usize).min(self.rows - 1);
368        let c1 = (c0 + 1).min(self.cols - 1);
369        let r1 = (r0 + 1).min(self.rows - 1);
370        let tx = fx - c0 as Real;
371        let tz = fz - r0 as Real;
372        let n00 = self.normal_at_grid(c0, r0);
373        let n10 = self.normal_at_grid(c1, r0);
374        let n01 = self.normal_at_grid(c0, r1);
375        let n11 = self.normal_at_grid(c1, r1);
376        let mut n = [0.0f64; 3];
377        for i in 0..3 {
378            let a = n00[i] * (1.0 - tx) + n10[i] * tx;
379            let b = n01[i] * (1.0 - tx) + n11[i] * tx;
380            n[i] = a * (1.0 - tz) + b * tz;
381        }
382        let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
383        if len > 1e-10 {
384            [n[0] / len, n[1] / len, n[2] / len]
385        } else {
386            [0.0, 1.0, 0.0]
387        }
388    }
389    /// Serialize the height field to a flat `Vec`f64` prefixed by metadata.
390    ///
391    /// Format: `\[rows, cols, scale_x, scale_z, h0, h1, ...\]`
392    #[allow(dead_code)]
393    pub fn serialize(&self) -> Vec<Real> {
394        let mut data = Vec::with_capacity(4 + self.heights.len());
395        data.push(self.rows as Real);
396        data.push(self.cols as Real);
397        data.push(self.scale_x);
398        data.push(self.scale_z);
399        data.extend_from_slice(&self.heights);
400        data
401    }
402    /// Deserialize a height field from a flat `Vec`f64` (inverse of `serialize`).
403    #[allow(dead_code)]
404    pub fn deserialize(data: &[Real]) -> Option<HeightField> {
405        if data.len() < 4 {
406            return None;
407        }
408        let rows = data[0] as usize;
409        let cols = data[1] as usize;
410        let scale_x = data[2];
411        let scale_z = data[3];
412        if data.len() != 4 + rows * cols {
413            return None;
414        }
415        Some(HeightField::new(
416            data[4..].to_vec(),
417            rows,
418            cols,
419            scale_x,
420            scale_z,
421        ))
422    }
423    /// Compute per-vertex normals for all grid vertices.
424    ///
425    /// Returns a flat `Vec<[Real; 3]>` in row-major order.
426    #[allow(dead_code)]
427    pub fn compute_all_normals(&self) -> Vec<[Real; 3]> {
428        let mut normals = Vec::with_capacity(self.rows * self.cols);
429        for row in 0..self.rows {
430            for col in 0..self.cols {
431                normals.push(self.normal_at_grid(col, row));
432            }
433        }
434        normals
435    }
436    /// Height at arbitrary world-space XZ position (bilinear interpolation).
437    ///
438    /// Clamps to grid extents.
439    #[allow(dead_code)]
440    pub fn height_at_world(&self, x: Real, z: Real) -> Real {
441        let u = (x / (self.scale_x * (self.cols - 1) as Real)).clamp(0.0, 1.0);
442        let v = (z / (self.scale_z * (self.rows - 1) as Real)).clamp(0.0, 1.0);
443        self.height_at_uv(u, v)
444    }
445    /// Ray cast with DDA and a maximum number of steps (bounded version).
446    ///
447    /// Avoids unbounded loops on very large grids.
448    #[allow(dead_code)]
449    pub fn ray_cast_bounded(
450        &self,
451        origin: [Real; 3],
452        dir: [Real; 3],
453        max_toi: Real,
454        max_steps: usize,
455    ) -> Option<(Real, [Real; 3])> {
456        let step = self.scale_x.min(self.scale_z) * 0.25;
457        let mut t = 0.0;
458        let mut best: Option<(Real, [Real; 3])> = None;
459        for _ in 0..max_steps {
460            if t > max_toi {
461                break;
462            }
463            let px = origin[0] + dir[0] * t;
464            let pz = origin[2] + dir[2] * t;
465            if px < 0.0
466                || px > (self.cols - 1) as Real * self.scale_x
467                || pz < 0.0
468                || pz > (self.rows - 1) as Real * self.scale_z
469            {
470                t += step;
471                continue;
472            }
473            let py = origin[1] + dir[1] * t;
474            let terrain_y = self.height_at_world(px, pz);
475            if py <= terrain_y {
476                let refined_t = if t > step { t - step * 0.5 } else { 0.0 };
477                let col = ((px / self.scale_x) as usize).min(self.cols.saturating_sub(2));
478                let row = ((pz / self.scale_z) as usize).min(self.rows.saturating_sub(2));
479                let normal = self.normal_at_grid(col, row);
480                best = Some((refined_t.max(0.0), normal));
481                break;
482            }
483            t += step;
484        }
485        best
486    }
487}
488impl HeightField {
489    /// Axis-aligned bounding box of this height field in world space.
490    ///
491    /// Returns `(min, max)` where each is `[x, y, z]`.
492    #[allow(dead_code)]
493    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
494        let (min_h, max_h) = self.height_bounds();
495        let max_x = (self.cols - 1) as f64 * self.scale_x;
496        let max_z = (self.rows - 1) as f64 * self.scale_z;
497        ([0.0, min_h, 0.0], [max_x, max_h, max_z])
498    }
499    /// Bilinear height interpolation at world-space position `(x, z)`.
500    ///
501    /// Unlike `height_at(row, col)` (grid indices) and `height_at_world`,
502    /// this method accepts `(x, z)` world coordinates and returns the
503    /// interpolated height, clamping to the grid extents.
504    #[allow(dead_code)]
505    pub fn height_at_xz(&self, x: f64, z: f64) -> f64 {
506        self.height_at_world(x, z)
507    }
508    /// Per-vertex normals for the entire grid in row-major order.
509    ///
510    /// Each normal is computed via finite differences over the 4-connected
511    /// neighborhood and returned as a unit vector `[nx, ny, nz]`.
512    #[allow(dead_code)]
513    pub fn normals(&self) -> Vec<[f64; 3]> {
514        self.compute_all_normals()
515    }
516    /// Tessellate the height field into a triangle mesh.
517    ///
518    /// Returns `(vertices, indices)` where:
519    /// - `vertices` is a list of world-space `[x, y, z]` positions,
520    ///   one per grid point, in row-major order.
521    /// - `indices` is a list of `[i0, i1, i2]` triangles (two per quad cell).
522    #[allow(dead_code)]
523    pub fn tessellate(&self) -> (Vec<[f64; 3]>, Vec<[usize; 3]>) {
524        let vertices: Vec<[f64; 3]> = (0..self.rows)
525            .flat_map(|row| {
526                (0..self.cols).map(move |col| {
527                    [
528                        col as f64 * self.scale_x,
529                        self.height_at(row, col),
530                        row as f64 * self.scale_z,
531                    ]
532                })
533            })
534            .collect();
535        let indices = heightfield_tessellate(self);
536        (vertices, indices)
537    }
538    /// DDA ray intersection returning `(t, normal)` for the first hit.
539    ///
540    /// Uses grid-cell DDA traversal in the XZ plane, testing the two
541    /// triangles of each cell with Möller–Trumbore intersection.
542    /// Returns `None` if the ray misses the terrain or is going away from it.
543    ///
544    /// The search is limited to a reasonable distance (diagonal of the AABB).
545    #[allow(dead_code)]
546    pub fn ray_intersect(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<(f64, [f64; 3])> {
547        let (_, max_h) = self.height_bounds();
548        if dir[1] > 0.0 && origin[1] >= max_h {
549            return None;
550        }
551        let grid_w = (self.cols - 1) as f64 * self.scale_x;
552        let grid_h = (self.rows - 1) as f64 * self.scale_z;
553        let diag = (grid_w * grid_w + grid_h * grid_h + (max_h - 0.0).abs().powi(2)).sqrt();
554        let max_toi = (origin[1] - max_h).abs() / dir[1].abs().max(1e-12) + diag * 2.0;
555        let max_toi = max_toi.min(1e9);
556        self.ray_cast_grid(origin, dir, max_toi)
557    }
558}
559impl HeightField {
560    /// Compute the slope magnitude at grid point `(col, row)`.
561    ///
562    /// Slope is defined as `sqrt((dh/dx)² + (dh/dz)²)` using the same
563    /// finite-difference stencil as `normal_at_grid`.
564    #[allow(dead_code)]
565    pub fn slope_at(&self, col: usize, row: usize) -> f64 {
566        let dh_dx = if col == 0 {
567            (self.height_at(row, col + 1) - self.height_at(row, col)) / self.scale_x
568        } else if col == self.cols - 1 {
569            (self.height_at(row, col) - self.height_at(row, col - 1)) / self.scale_x
570        } else {
571            (self.height_at(row, col + 1) - self.height_at(row, col - 1)) / (2.0 * self.scale_x)
572        };
573        let dh_dz = if row == 0 {
574            (self.height_at(row + 1, col) - self.height_at(row, col)) / self.scale_z
575        } else if row == self.rows - 1 {
576            (self.height_at(row, col) - self.height_at(row - 1, col)) / self.scale_z
577        } else {
578            (self.height_at(row + 1, col) - self.height_at(row - 1, col)) / (2.0 * self.scale_z)
579        };
580        (dh_dx * dh_dx + dh_dz * dh_dz).sqrt()
581    }
582    /// Compute the mean curvature at grid point `(col, row)`.
583    ///
584    /// Uses a second-order central difference approximation:
585    /// `κ ≈ (d²h/dx² + d²h/dz²) / 2`.
586    #[allow(dead_code)]
587    pub fn curvature_at(&self, col: usize, row: usize) -> f64 {
588        let h = self.height_at(row, col);
589        let d2h_dx2 = if col == 0 || col == self.cols - 1 {
590            0.0
591        } else {
592            let hl = self.height_at(row, col - 1);
593            let hr = self.height_at(row, col + 1);
594            (hl - 2.0 * h + hr) / (self.scale_x * self.scale_x)
595        };
596        let d2h_dz2 = if row == 0 || row == self.rows - 1 {
597            0.0
598        } else {
599            let hd = self.height_at(row - 1, col);
600            let hu = self.height_at(row + 1, col);
601            (hd - 2.0 * h + hu) / (self.scale_z * self.scale_z)
602        };
603        (d2h_dx2 + d2h_dz2) * 0.5
604    }
605    /// Return a flat `Vec`f64` of slope magnitudes in row-major order.
606    #[allow(dead_code)]
607    pub fn slope_map(&self) -> Vec<f64> {
608        let mut out = Vec::with_capacity(self.rows * self.cols);
609        for row in 0..self.rows {
610            for col in 0..self.cols {
611                out.push(self.slope_at(col, row));
612            }
613        }
614        out
615    }
616    /// Return a flat `Vec`f64` of mean curvatures in row-major order.
617    #[allow(dead_code)]
618    pub fn curvature_map(&self) -> Vec<f64> {
619        let mut out = Vec::with_capacity(self.rows * self.cols);
620        for row in 0..self.rows {
621            for col in 0..self.cols {
622                out.push(self.curvature_at(col, row));
623            }
624        }
625        out
626    }
627    /// Find the closest surface point to query point `q` by brute-force search
628    /// over all grid vertices.
629    ///
630    /// Returns `(closest_world_point, row, col)`.
631    #[allow(dead_code)]
632    pub fn closest_vertex(&self, q: [f64; 3]) -> ([f64; 3], usize, usize) {
633        let mut best_dist2 = f64::INFINITY;
634        let mut best_pt = [0.0f64; 3];
635        let mut best_row = 0usize;
636        let mut best_col = 0usize;
637        for row in 0..self.rows {
638            for col in 0..self.cols {
639                let vx = col as f64 * self.scale_x;
640                let vy = self.height_at(row, col);
641                let vz = row as f64 * self.scale_z;
642                let dx = q[0] - vx;
643                let dy = q[1] - vy;
644                let dz = q[2] - vz;
645                let d2 = dx * dx + dy * dy + dz * dz;
646                if d2 < best_dist2 {
647                    best_dist2 = d2;
648                    best_pt = [vx, vy, vz];
649                    best_row = row;
650                    best_col = col;
651                }
652            }
653        }
654        (best_pt, best_row, best_col)
655    }
656    /// Resample the height field to new grid dimensions by bilinear interpolation.
657    ///
658    /// `new_cols` and `new_rows` must be ≥ 2.  Returns a new `HeightField` with
659    /// the same world extents but a different resolution.
660    #[allow(dead_code)]
661    pub fn resample(&self, new_cols: usize, new_rows: usize) -> HeightField {
662        assert!(new_cols >= 2 && new_rows >= 2);
663        let new_scale_x = (self.scale_x * (self.cols - 1) as f64) / (new_cols - 1) as f64;
664        let new_scale_z = (self.scale_z * (self.rows - 1) as f64) / (new_rows - 1) as f64;
665        let mut heights = Vec::with_capacity(new_rows * new_cols);
666        for nr in 0..new_rows {
667            for nc in 0..new_cols {
668                let x = nc as f64 * new_scale_x;
669                let z = nr as f64 * new_scale_z;
670                heights.push(self.height_at_world(x, z));
671            }
672        }
673        HeightField::new(heights, new_rows, new_cols, new_scale_x, new_scale_z)
674    }
675    /// Simple hydraulic erosion step: for each cell, water flows to the
676    /// steepest-descent neighbor and carries a fraction `sediment_rate` of
677    /// height difference.
678    ///
679    /// `iterations` controls how many passes are performed.  For correctness
680    /// the caller should keep `sediment_rate` ≤ 0.5.
681    #[allow(dead_code)]
682    pub fn hydraulic_erode(&mut self, sediment_rate: f64, iterations: usize) {
683        for _ in 0..iterations {
684            let mut delta = vec![0.0f64; self.rows * self.cols];
685            for row in 0..self.rows {
686                for col in 0..self.cols {
687                    let h = self.height_at(row, col);
688                    let neighbors: [(isize, isize); 4] = [
689                        (row as isize - 1, col as isize),
690                        (row as isize + 1, col as isize),
691                        (row as isize, col as isize - 1),
692                        (row as isize, col as isize + 1),
693                    ];
694                    let mut steepest_diff = 0.0f64;
695                    let mut steepest_nr = -1isize;
696                    let mut steepest_nc = -1isize;
697                    for (nr, nc) in neighbors {
698                        if nr >= 0 && nr < self.rows as isize && nc >= 0 && nc < self.cols as isize
699                        {
700                            let nh = self.height_at(nr as usize, nc as usize);
701                            let diff = h - nh;
702                            if diff > steepest_diff {
703                                steepest_diff = diff;
704                                steepest_nr = nr;
705                                steepest_nc = nc;
706                            }
707                        }
708                    }
709                    if steepest_nr >= 0 && steepest_diff > 0.0 {
710                        let transfer = sediment_rate * steepest_diff;
711                        delta[row * self.cols + col] -= transfer;
712                        delta[steepest_nr as usize * self.cols + steepest_nc as usize] += transfer;
713                    }
714                }
715            }
716            for i in 0..self.heights.len() {
717                self.heights[i] += delta[i];
718            }
719        }
720    }
721    /// Compute a simple flow-accumulation map (watershed proxy).
722    ///
723    /// Each cell accumulates 1 unit plus the total of all upstream cells that
724    /// drain into it following steepest descent.  Returns a flat `Vec`f64`
725    /// in row-major order; larger values indicate valleys/channels.
726    #[allow(dead_code)]
727    pub fn flow_accumulation(&self) -> Vec<f64> {
728        let n = self.rows * self.cols;
729        let mut drain: Vec<Option<usize>> = vec![None; n];
730        for row in 0..self.rows {
731            for col in 0..self.cols {
732                let h = self.height_at(row, col);
733                let neighbors: [(isize, isize); 4] = [
734                    (row as isize - 1, col as isize),
735                    (row as isize + 1, col as isize),
736                    (row as isize, col as isize - 1),
737                    (row as isize, col as isize + 1),
738                ];
739                let mut best_diff = 0.0f64;
740                let mut best_idx: Option<usize> = None;
741                for (nr, nc) in neighbors {
742                    if nr >= 0 && nr < self.rows as isize && nc >= 0 && nc < self.cols as isize {
743                        let nh = self.height_at(nr as usize, nc as usize);
744                        let diff = h - nh;
745                        if diff > best_diff {
746                            best_diff = diff;
747                            best_idx = Some(nr as usize * self.cols + nc as usize);
748                        }
749                    }
750                }
751                drain[row * self.cols + col] = best_idx;
752            }
753        }
754        let mut order: Vec<usize> = (0..n).collect();
755        order.sort_by(|&a, &b| {
756            let ha = self.heights[a];
757            let hb = self.heights[b];
758            hb.partial_cmp(&ha).unwrap_or(std::cmp::Ordering::Equal)
759        });
760        let mut accum = vec![1.0f64; n];
761        for &idx in &order {
762            if let Some(target) = drain[idx] {
763                let val = accum[idx];
764                accum[target] += val;
765            }
766        }
767        accum
768    }
769    /// Clamp all height values to the range `\[min_h, max_h\]`.
770    #[allow(dead_code)]
771    pub fn clamp_heights(&mut self, min_h: f64, max_h: f64) {
772        for h in &mut self.heights {
773            *h = h.clamp(min_h, max_h);
774        }
775    }
776    /// Scale all heights by a uniform factor.
777    #[allow(dead_code)]
778    pub fn scale_heights(&mut self, factor: f64) {
779        for h in &mut self.heights {
780            *h *= factor;
781        }
782    }
783    /// Offset all heights by a constant value.
784    #[allow(dead_code)]
785    pub fn offset_heights(&mut self, offset: f64) {
786        for h in &mut self.heights {
787            *h += offset;
788        }
789    }
790    /// Normalize heights to the range `\[0, 1\]`.  If all heights are equal, all
791    /// become 0.
792    #[allow(dead_code)]
793    pub fn normalize_heights(&mut self) {
794        let (min_h, max_h) = self.height_bounds();
795        let range = max_h - min_h;
796        if range < 1e-15 {
797            for h in &mut self.heights {
798                *h = 0.0;
799            }
800        } else {
801            for h in &mut self.heights {
802                *h = (*h - min_h) / range;
803            }
804        }
805    }
806    /// Invert heights: each height `h` becomes `max_height - (h - min_height)`.
807    #[allow(dead_code)]
808    pub fn invert_heights(&mut self) {
809        let (min_h, max_h) = self.height_bounds();
810        for h in &mut self.heights {
811            *h = max_h - (*h - min_h);
812        }
813    }
814    /// Compute the average height over the entire grid.
815    #[allow(dead_code)]
816    pub fn mean_height(&self) -> f64 {
817        if self.heights.is_empty() {
818            return 0.0;
819        }
820        self.heights.iter().sum::<f64>() / self.heights.len() as f64
821    }
822    /// Compute the variance of heights.
823    #[allow(dead_code)]
824    pub fn height_variance(&self) -> f64 {
825        if self.heights.is_empty() {
826            return 0.0;
827        }
828        let mean = self.mean_height();
829        self.heights
830            .iter()
831            .map(|&h| (h - mean) * (h - mean))
832            .sum::<f64>()
833            / self.heights.len() as f64
834    }
835    /// Count the number of local maxima (peaks) in the grid.
836    ///
837    /// A cell is a peak if it is strictly higher than all 4-connected neighbors.
838    #[allow(dead_code)]
839    pub fn count_peaks(&self) -> usize {
840        let mut count = 0usize;
841        for row in 0..self.rows {
842            for col in 0..self.cols {
843                let h = self.height_at(row, col);
844                let neighbors: [(isize, isize); 4] = [
845                    (row as isize - 1, col as isize),
846                    (row as isize + 1, col as isize),
847                    (row as isize, col as isize - 1),
848                    (row as isize, col as isize + 1),
849                ];
850                let is_peak = neighbors.iter().all(|&(nr, nc)| {
851                    if nr < 0 || nr >= self.rows as isize || nc < 0 || nc >= self.cols as isize {
852                        true
853                    } else {
854                        self.height_at(nr as usize, nc as usize) < h
855                    }
856                });
857                if is_peak {
858                    count += 1;
859                }
860            }
861        }
862        count
863    }
864    /// Approximate volume under the height field (above y=0) using the trapezoidal rule.
865    ///
866    /// Each grid cell contributes `(average_height) * cell_area`.
867    #[allow(dead_code)]
868    pub fn volume(&self) -> f64 {
869        if self.rows < 2 || self.cols < 2 {
870            return 0.0;
871        }
872        let mut vol = 0.0_f64;
873        let cell_area = self.scale_x * self.scale_z;
874        for row in 0..self.rows - 1 {
875            for col in 0..self.cols - 1 {
876                let h00 = self.height_at(row, col);
877                let h10 = self.height_at(row, col + 1);
878                let h01 = self.height_at(row + 1, col);
879                let h11 = self.height_at(row + 1, col + 1);
880                let avg = (h00 + h10 + h01 + h11) * 0.25;
881                vol += avg * cell_area;
882            }
883        }
884        vol
885    }
886    /// Cast a ray against the height field. Returns hit information if intersection found.
887    ///
888    /// Uses the tessellated triangle mesh for accurate intersection.
889    #[allow(dead_code)]
890    pub fn ray_cast(
891        &self,
892        ray_origin: &oxiphysics_core::math::Vec3,
893        ray_direction: &oxiphysics_core::math::Vec3,
894        max_toi: f64,
895    ) -> Option<HeightfieldRayHit> {
896        let (verts, tris) = self.to_triangle_mesh();
897        let mut closest_toi = max_toi;
898        let mut hit = None;
899        for tri_idx in &tris {
900            let v0 = oxiphysics_core::math::Vec3::new(
901                verts[tri_idx[0]][0],
902                verts[tri_idx[0]][1],
903                verts[tri_idx[0]][2],
904            );
905            let v1 = oxiphysics_core::math::Vec3::new(
906                verts[tri_idx[1]][0],
907                verts[tri_idx[1]][1],
908                verts[tri_idx[1]][2],
909            );
910            let v2 = oxiphysics_core::math::Vec3::new(
911                verts[tri_idx[2]][0],
912                verts[tri_idx[2]][1],
913                verts[tri_idx[2]][2],
914            );
915            if let Some(ray_hit) =
916                ray_triangle(ray_origin, ray_direction, closest_toi, &v0, &v1, &v2)
917                && ray_hit.toi >= 0.0
918                && ray_hit.toi < closest_toi
919            {
920                closest_toi = ray_hit.toi;
921                let point = oxiphysics_core::math::Vec3::new(
922                    ray_origin.x + ray_direction.x * ray_hit.toi,
923                    ray_origin.y + ray_direction.y * ray_hit.toi,
924                    ray_origin.z + ray_direction.z * ray_hit.toi,
925                );
926                hit = Some(HeightfieldRayHit {
927                    toi: ray_hit.toi,
928                    point,
929                });
930            }
931        }
932        hit
933    }
934}
935/// A ray-hit result from `HeightField::ray_cast`.
936#[derive(Debug, Clone)]
937pub struct HeightfieldRayHit {
938    /// Time of impact (distance along ray).
939    pub toi: f64,
940    /// World-space hit point.
941    pub point: oxiphysics_core::math::Vec3,
942}
943/// Result of a heightfield ray traversal.
944#[derive(Debug, Clone)]
945#[allow(dead_code)]
946pub struct HeightfieldRaycast {
947    /// Column index of the cell that was hit.
948    pub cell_ix: usize,
949    /// Row index of the cell that was hit.
950    pub cell_iz: usize,
951    /// Ray parameter t at the intersection.
952    pub t: f64,
953    /// Surface normal at the intersection.
954    pub normal: [f64; 3],
955}
956/// State for a DDA grid-traversal ray cast over a height field.
957///
958/// Call [`HeightfieldRayTraversal::new`] to set up the traversal, then call
959/// [`HeightfieldRayTraversal::next_cell`] repeatedly to visit each cell along
960/// the ray in XZ-projection order.
961#[derive(Debug, Clone)]
962#[allow(dead_code)]
963pub struct HeightfieldRayTraversal {
964    /// Current cell column index.
965    pub cell_col: isize,
966    /// Current cell row index.
967    pub cell_row: isize,
968    /// Direction step in the column dimension (+1 or -1).
969    pub(super) step_col: isize,
970    /// Direction step in the row dimension (+1 or -1).
971    pub(super) step_row: isize,
972    /// Parameter t at the next X-boundary crossing.
973    pub(super) t_max_x: f64,
974    /// Parameter t at the next Z-boundary crossing.
975    pub(super) t_max_z: f64,
976    /// Change in t per cell step in X.
977    pub(super) t_delta_x: f64,
978    /// Change in t per cell step in Z.
979    pub(super) t_delta_z: f64,
980    /// Maximum t to traverse.
981    pub(super) t_max: f64,
982    /// True once the traversal has finished.
983    pub done: bool,
984}
985#[allow(dead_code)]
986impl HeightfieldRayTraversal {
987    /// Initialize a DDA traversal over `hf` starting at `ray_origin` in direction
988    /// `ray_dir` (need not be normalised).  `max_t` bounds the traversal.
989    ///
990    /// Returns `None` if the ray does not enter the XZ footprint of the grid.
991    pub fn new(
992        hf: &HeightField,
993        ray_origin: [f64; 3],
994        ray_dir: [f64; 3],
995        max_t: f64,
996    ) -> Option<Self> {
997        if hf.rows < 2 || hf.cols < 2 {
998            return None;
999        }
1000        let grid_w = (hf.cols - 1) as f64 * hf.scale_x;
1001        let grid_h = (hf.rows - 1) as f64 * hf.scale_z;
1002        let (t_enter, t_exit) = ray_aabb_xz(
1003            ray_origin[0],
1004            ray_origin[2],
1005            ray_dir[0],
1006            ray_dir[2],
1007            grid_w,
1008            grid_h,
1009            max_t,
1010        )?;
1011        let t_start = t_enter.max(0.0);
1012        if t_start > t_exit {
1013            return None;
1014        }
1015        let px = ray_origin[0] + ray_dir[0] * t_start;
1016        let pz = ray_origin[2] + ray_dir[2] * t_start;
1017        let cell_col = ((px / hf.scale_x).floor() as isize).clamp(0, (hf.cols - 2) as isize);
1018        let cell_row = ((pz / hf.scale_z).floor() as isize).clamp(0, (hf.rows - 2) as isize);
1019        let step_col = if ray_dir[0] >= 0.0 { 1_isize } else { -1 };
1020        let step_row = if ray_dir[2] >= 0.0 { 1_isize } else { -1 };
1021        let t_delta_x = if ray_dir[0].abs() < 1e-12 {
1022            f64::INFINITY
1023        } else {
1024            hf.scale_x / ray_dir[0].abs()
1025        };
1026        let t_delta_z = if ray_dir[2].abs() < 1e-12 {
1027            f64::INFINITY
1028        } else {
1029            hf.scale_z / ray_dir[2].abs()
1030        };
1031        let x_boundary = if step_col > 0 {
1032            (cell_col + 1) as f64 * hf.scale_x
1033        } else {
1034            cell_col as f64 * hf.scale_x
1035        };
1036        let z_boundary = if step_row > 0 {
1037            (cell_row + 1) as f64 * hf.scale_z
1038        } else {
1039            cell_row as f64 * hf.scale_z
1040        };
1041        let t_max_x = if ray_dir[0].abs() < 1e-12 {
1042            f64::INFINITY
1043        } else {
1044            t_start + (x_boundary - px) / ray_dir[0]
1045        };
1046        let t_max_z = if ray_dir[2].abs() < 1e-12 {
1047            f64::INFINITY
1048        } else {
1049            t_start + (z_boundary - pz) / ray_dir[2]
1050        };
1051        Some(Self {
1052            cell_col,
1053            cell_row,
1054            step_col,
1055            step_row,
1056            t_max_x,
1057            t_max_z,
1058            t_delta_x,
1059            t_delta_z,
1060            t_max: t_exit.min(max_t),
1061            done: false,
1062        })
1063    }
1064    /// Advance to the next cell along the ray.
1065    ///
1066    /// Returns `(col, row, t_entry)` for the cell just entered, or `None`
1067    /// when the traversal exits the grid.
1068    pub fn next_cell(&mut self) -> Option<(usize, usize, f64)> {
1069        if self.done {
1070            return None;
1071        }
1072        let col = self.cell_col;
1073        let row = self.cell_row;
1074        let t_entry = self.t_max_x.min(self.t_max_z);
1075        if t_entry > self.t_max {
1076            self.done = true;
1077            return None;
1078        }
1079        if self.t_max_x < self.t_max_z {
1080            self.cell_col += self.step_col;
1081            self.t_max_x += self.t_delta_x;
1082        } else {
1083            self.cell_row += self.step_row;
1084            self.t_max_z += self.t_delta_z;
1085        }
1086        if col < 0 || row < 0 {
1087            self.done = true;
1088            return None;
1089        }
1090        Some((col as usize, row as usize, t_entry))
1091    }
1092}