use glam::DVec3;
use rustial_engine as rustial_math;
use rustial_engine::{
ColumnInstanceSet, GeoGrid, MapState, PointInstance, PointInstanceSet, WorldBounds, WorldCoord,
};
pub fn grid_intersects_scene_viewport(
grid: &GeoGrid,
state: &MapState,
scene_origin: DVec3,
) -> bool {
let bounds = grid_world_bounds(grid, state, scene_origin);
bounds.intersects(state.scene_viewport_bounds())
}
pub fn points_intersect_scene_viewport(
points: &PointInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> bool {
let Some(bounds) = point_world_bounds(points, state, scene_origin) else {
return false;
};
bounds.intersects(state.scene_viewport_bounds())
}
pub fn columns_intersect_scene_viewport(
columns: &ColumnInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> bool {
let Some(bounds) = column_world_bounds(columns, state, scene_origin) else {
return false;
};
bounds.intersects(state.scene_viewport_bounds())
}
fn grid_world_bounds(grid: &GeoGrid, state: &MapState, scene_origin: DVec3) -> WorldBounds {
let corners = [
project_grid_corner(grid, 0, 0, state, scene_origin),
project_grid_corner(grid, 0, grid.cols, state, scene_origin),
project_grid_corner(grid, grid.rows, 0, state, scene_origin),
project_grid_corner(grid, grid.rows, grid.cols, state, scene_origin),
];
let mut bounds = WorldBounds::new(
WorldCoord::new(corners[0].x, corners[0].y, corners[0].z),
WorldCoord::new(corners[0].x, corners[0].y, corners[0].z),
);
for corner in corners.into_iter().skip(1) {
bounds.extend_point(&WorldCoord::new(corner.x, corner.y, corner.z));
}
bounds
}
fn project_grid_corner(
grid: &GeoGrid,
row: usize,
col: usize,
state: &MapState,
scene_origin: DVec3,
) -> glam::DVec3 {
let dx = col as f64 * grid.cell_width;
let dy = row as f64 * grid.cell_height;
let (sin_r, cos_r) = grid.rotation.sin_cos();
let rx = dx * cos_r - dy * sin_r;
let ry = dx * sin_r + dy * cos_r;
let coord = offset_geo_coord(&grid.origin, rx, ry);
let altitude = resolve_grid_surface_altitude(grid, &coord, state);
let projected = state
.camera()
.projection()
.project(&rustial_math::GeoCoord::new(coord.lat, coord.lon, altitude));
glam::DVec3::new(
projected.position.x - scene_origin.x,
projected.position.y - scene_origin.y,
projected.position.z - scene_origin.z,
)
}
fn column_world_bounds(
columns: &ColumnInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> Option<WorldBounds> {
let mut bounds: Option<WorldBounds> = None;
for column in &columns.columns {
let projected = state.camera().projection().project(&column.position);
let base_z = resolve_column_base_altitude(column, state);
let half_width = column.width * 0.5;
let min = WorldCoord::new(
projected.position.x - scene_origin.x - half_width,
projected.position.y - scene_origin.y - half_width,
base_z - scene_origin.z,
);
let max = WorldCoord::new(
projected.position.x - scene_origin.x + half_width,
projected.position.y - scene_origin.y + half_width,
base_z - scene_origin.z + column.height,
);
let column_bounds = WorldBounds::new(min, max);
if let Some(existing) = bounds.as_mut() {
existing.extend(&column_bounds);
} else {
bounds = Some(column_bounds);
}
}
bounds
}
fn point_world_bounds(
points: &PointInstanceSet,
state: &MapState,
scene_origin: DVec3,
) -> Option<WorldBounds> {
let mut bounds: Option<WorldBounds> = None;
for point in &points.points {
let projected = state.camera().projection().project(&point.position);
let center_z = resolve_point_altitude(point, state);
let radius = point.radius;
let min = WorldCoord::new(
projected.position.x - scene_origin.x - radius,
projected.position.y - scene_origin.y - radius,
center_z - scene_origin.z - radius,
);
let max = WorldCoord::new(
projected.position.x - scene_origin.x + radius,
projected.position.y - scene_origin.y + radius,
center_z - scene_origin.z + radius,
);
let point_bounds = WorldBounds::new(min, max);
if let Some(existing) = bounds.as_mut() {
existing.extend(&point_bounds);
} else {
bounds = Some(point_bounds);
}
}
bounds
}
fn offset_geo_coord(
origin: &rustial_math::GeoCoord,
dx_meters: f64,
dy_meters: f64,
) -> rustial_math::GeoCoord {
const METERS_PER_DEG_LAT: f64 = 111_320.0;
let lat = origin.lat - dy_meters / METERS_PER_DEG_LAT;
let cos_lat = origin.lat.to_radians().cos().max(1e-10);
let lon = origin.lon + dx_meters / (METERS_PER_DEG_LAT * cos_lat);
rustial_math::GeoCoord::new(lat, lon, origin.alt)
}
fn resolve_column_base_altitude(column: &rustial_engine::ColumnInstance, state: &MapState) -> f64 {
let terrain = state.elevation_at(&column.position).unwrap_or(0.0);
match column.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain + column.base,
rustial_engine::AltitudeMode::RelativeToGround => {
terrain + column.position.alt + column.base
}
rustial_engine::AltitudeMode::Absolute => column.position.alt + column.base,
}
}
fn resolve_point_altitude(point: &PointInstance, state: &MapState) -> f64 {
let terrain = state.elevation_at(&point.position).unwrap_or(0.0);
match point.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain,
rustial_engine::AltitudeMode::RelativeToGround => terrain + point.position.alt,
rustial_engine::AltitudeMode::Absolute => point.position.alt,
}
}
fn resolve_grid_surface_altitude(
grid: &GeoGrid,
coord: &rustial_math::GeoCoord,
state: &MapState,
) -> f64 {
let terrain = state.elevation_at(coord).unwrap_or(0.0);
match grid.altitude_mode {
rustial_engine::AltitudeMode::ClampToGround => terrain,
rustial_engine::AltitudeMode::RelativeToGround => terrain + grid.origin.alt,
rustial_engine::AltitudeMode::Absolute => grid.origin.alt,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustial_engine::GeoCoord;
use rustial_engine::{
ColumnInstance, ColumnInstanceSet, MapState, PointInstance, PointInstanceSet,
};
#[test]
fn grid_visibility_rejects_far_grid() {
let mut state = MapState::new();
state.set_viewport(1280, 720);
state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(1_000.0);
state.update_camera(1.0 / 60.0);
let grid = GeoGrid::new(GeoCoord::from_lat_lon(70.0, 120.0), 2, 2, 100.0, 100.0);
assert!(!grid_intersects_scene_viewport(
&grid,
&state,
state.scene_world_origin()
));
}
#[test]
fn columns_visibility_rejects_far_columns() {
let mut state = MapState::new();
state.set_viewport(1280, 720);
state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(1_000.0);
state.update_camera(1.0 / 60.0);
let columns = ColumnInstanceSet::new(vec![ColumnInstance::new(
GeoCoord::from_lat_lon(70.0, 120.0),
10.0,
5.0,
)]);
assert!(!columns_intersect_scene_viewport(
&columns,
&state,
state.scene_world_origin()
));
}
#[test]
fn points_visibility_rejects_far_points() {
let mut state = MapState::new();
state.set_viewport(1280, 720);
state.set_camera_target(GeoCoord::from_lat_lon(0.0, 0.0));
state.set_camera_distance(1_000.0);
state.update_camera(1.0 / 60.0);
let points = PointInstanceSet::new(vec![PointInstance::new(
GeoCoord::from_lat_lon(70.0, 120.0),
10.0,
)]);
assert!(!points_intersect_scene_viewport(
&points,
&state,
state.scene_world_origin()
));
}
}