aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-terrain/src
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-30 17:01:37 +0300
committerValentin Popov <valentin@popov.link>2026-10-30 17:01:37 +0300
commit437e8d63073c1a5a360e48e7796eb42720cf64c2 (patch)
tree05598afab62f88d258524f238a8a3ca5e498681c /crates/fparkan-terrain/src
parent146db05e12a7ef40e1e313307127cb73919772eb (diff)
downloadfparkan-437e8d63073c1a5a360e48e7796eb42720cf64c2.tar.xz
fparkan-437e8d63073c1a5a360e48e7796eb42720cf64c2.zip
fix(camera): constrain free flight to terrain surface bounds
Diffstat (limited to 'crates/fparkan-terrain/src')
-rw-r--r--crates/fparkan-terrain/src/lib.rs16
1 files changed, 16 insertions, 0 deletions
diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs
index 54946e7..5e17a23 100644
--- a/crates/fparkan-terrain/src/lib.rs
+++ b/crates/fparkan-terrain/src/lib.rs
@@ -409,6 +409,13 @@ impl TerrainWorld {
self.surfaces.len()
}
+ /// Bounds of the indexed surface geometry, excluding unused source vertices.
+ #[must_use]
+ pub fn surface_bounds(&self) -> Option<[[f32; 3]; 2]> {
+ let root = self.surface_bvh.nodes.get(self.surface_bvh.root?)?;
+ Some([root.min, root.max])
+ }
+
/// Returns the validated source mesh retained for renderer integration.
///
/// Runtime collision queries use their own compact triangle representation;
@@ -1215,6 +1222,15 @@ mod tests {
}
#[test]
+ fn surface_bounds_cover_query_geometry_and_ignore_unused_positions() {
+ let mut mesh = synthetic_land_mesh();
+ mesh.positions.push([1000.0, -1000.0, 1000.0]);
+ let world = TerrainWorld::from_land_msh(&mesh).expect("world");
+ assert_eq!(world.surface_bounds(), Some([[0.0; 3], [1.0, 1.0, 2.0]]));
+ assert_eq!(TerrainWorld::default().surface_bounds(), None);
+ }
+
+ #[test]
fn synthetic_surface_height_and_raycast_work() {
let world = TerrainWorld::from_land_msh(&synthetic_land_mesh()).expect("world");