use crate::core::{BoundingBox, Camera};
use glam::Vec3;
pub fn data_units_per_px(bounds: &BoundingBox, viewport_px: (u32, u32)) -> f32 {
let (w_px, h_px) = viewport_px;
let w_px = w_px.max(1) as f32;
let h_px = h_px.max(1) as f32;
let x_range = (bounds.max.x - bounds.min.x).abs().max(1e-6);
let y_range = (bounds.max.y - bounds.min.y).abs().max(1e-6);
(x_range / w_px).min(y_range / h_px)
}
pub fn data_units_per_px_3d(bounds: &BoundingBox, viewport_px: (u32, u32)) -> f32 {
let (w_px, h_px) = viewport_px;
let w_px = w_px.max(1) as f32;
let h_px = h_px.max(1) as f32;
let x_range = (bounds.max.x - bounds.min.x).abs().max(1e-6);
let y_range = (bounds.max.y - bounds.min.y).abs().max(1e-6);
(x_range / w_px).min(y_range / h_px)
}
pub fn data_units_per_px_3d_camera(
bounds: &BoundingBox,
viewport_px: (u32, u32),
view_angles_deg: Option<(f32, f32)>,
) -> f32 {
let (w_px, h_px) = viewport_px;
let w = w_px.max(1) as f32;
let h = h_px.max(1) as f32;
let center = (bounds.min + bounds.max) * 0.5;
let mut camera = Camera::new();
camera.update_aspect_ratio((w / h).max(1e-6));
camera.fit_bounds(bounds.min, bounds.max);
if let Some((az, el)) = view_angles_deg {
camera.set_view_angles_deg(az, el);
}
let forward = (camera.target - camera.position).normalize_or_zero();
if forward.length_squared() <= 1e-8 {
return data_units_per_px_3d(bounds, viewport_px);
}
let right = forward.cross(camera.up).normalize_or_zero();
let up = right.cross(forward).normalize_or_zero();
if right.length_squared() <= 1e-8 || up.length_squared() <= 1e-8 {
return data_units_per_px_3d(bounds, viewport_px);
}
let unit = ((bounds.max - bounds.min).length() * 1e-3).max(1e-3);
let p0 = project_to_screen(&mut camera, center, w, h);
let px = project_to_screen(&mut camera, center + right * unit, w, h);
let py = project_to_screen(&mut camera, center + up * unit, w, h);
let (Some(p0), Some(px), Some(py)) = (p0, px, py) else {
return data_units_per_px_3d(bounds, viewport_px);
};
let px_per_unit_x = (px - p0).length() / unit;
let px_per_unit_y = (py - p0).length() / unit;
let px_per_unit = px_per_unit_x.min(px_per_unit_y).max(1e-6);
1.0 / px_per_unit
}
fn project_to_screen(camera: &mut Camera, pos: Vec3, w: f32, h: f32) -> Option<glam::Vec2> {
let vp = camera.view_proj_matrix();
let clip = vp * pos.extend(1.0);
if clip.w.abs() <= 1e-6 {
return None;
}
let ndc = clip.truncate() / clip.w;
if !ndc.is_finite() {
return None;
}
Some(glam::Vec2::new(
(ndc.x * 0.5 + 0.5) * w,
(1.0 - (ndc.y * 0.5 + 0.5)) * h,
))
}
#[cfg(test)]
mod tests {
use super::{data_units_per_px, data_units_per_px_3d, data_units_per_px_3d_camera};
use crate::core::BoundingBox;
use glam::Vec3;
#[test]
fn data_units_per_px_uses_min_2d_scale() {
let bounds = BoundingBox {
min: Vec3::new(0.0, 0.0, 0.0),
max: Vec3::new(100.0, 50.0, 0.0),
};
let scale = data_units_per_px(&bounds, (1000, 500));
assert!((scale - 0.1).abs() < 1e-6);
}
#[test]
fn data_units_per_px_3d_uses_screen_axes_scale() {
let bounds = BoundingBox {
min: Vec3::new(0.0, 0.0, 0.0),
max: Vec3::new(100.0, 50.0, 10.0),
};
let scale = data_units_per_px_3d(&bounds, (1000, 500));
assert!((scale - 0.1).abs() < 1e-6);
}
#[test]
fn data_units_per_px_3d_camera_returns_positive_finite_scale() {
let bounds = BoundingBox {
min: Vec3::new(-1.0, -1.0, -1.0),
max: Vec3::new(1.0, 1.0, 1.0),
};
let scale = data_units_per_px_3d_camera(&bounds, (1280, 720), Some((30.0, 20.0)));
assert!(scale.is_finite());
assert!(scale > 0.0);
}
}