runmat_plot/core/
viewport.rs1use crate::core::{BoundingBox, Camera};
2use glam::Vec3;
3
4pub fn data_units_per_px(bounds: &BoundingBox, viewport_px: (u32, u32)) -> f32 {
18 let (w_px, h_px) = viewport_px;
19 let w_px = w_px.max(1) as f32;
20 let h_px = h_px.max(1) as f32;
21
22 let x_range = (bounds.max.x - bounds.min.x).abs().max(1e-6);
23 let y_range = (bounds.max.y - bounds.min.y).abs().max(1e-6);
24
25 (x_range / w_px).min(y_range / h_px)
26}
27
28pub fn data_units_per_px_3d(bounds: &BoundingBox, viewport_px: (u32, u32)) -> f32 {
35 let (w_px, h_px) = viewport_px;
36 let w_px = w_px.max(1) as f32;
37 let h_px = h_px.max(1) as f32;
38
39 let x_range = (bounds.max.x - bounds.min.x).abs().max(1e-6);
40 let y_range = (bounds.max.y - bounds.min.y).abs().max(1e-6);
41
42 (x_range / w_px).min(y_range / h_px)
43}
44
45pub fn data_units_per_px_3d_camera(
51 bounds: &BoundingBox,
52 viewport_px: (u32, u32),
53 view_angles_deg: Option<(f32, f32)>,
54) -> f32 {
55 let (w_px, h_px) = viewport_px;
56 let w = w_px.max(1) as f32;
57 let h = h_px.max(1) as f32;
58
59 let center = (bounds.min + bounds.max) * 0.5;
60 let mut camera = Camera::new();
61 camera.update_aspect_ratio((w / h).max(1e-6));
62 camera.fit_bounds(bounds.min, bounds.max);
63 if let Some((az, el)) = view_angles_deg {
64 camera.set_view_angles_deg(az, el);
65 }
66
67 let forward = (camera.target - camera.position).normalize_or_zero();
68 if forward.length_squared() <= 1e-8 {
69 return data_units_per_px_3d(bounds, viewport_px);
70 }
71 let right = forward.cross(camera.up).normalize_or_zero();
72 let up = right.cross(forward).normalize_or_zero();
73 if right.length_squared() <= 1e-8 || up.length_squared() <= 1e-8 {
74 return data_units_per_px_3d(bounds, viewport_px);
75 }
76
77 let unit = ((bounds.max - bounds.min).length() * 1e-3).max(1e-3);
78 let p0 = project_to_screen(&mut camera, center, w, h);
79 let px = project_to_screen(&mut camera, center + right * unit, w, h);
80 let py = project_to_screen(&mut camera, center + up * unit, w, h);
81 let (Some(p0), Some(px), Some(py)) = (p0, px, py) else {
82 return data_units_per_px_3d(bounds, viewport_px);
83 };
84
85 let px_per_unit_x = (px - p0).length() / unit;
86 let px_per_unit_y = (py - p0).length() / unit;
87 let px_per_unit = px_per_unit_x.min(px_per_unit_y).max(1e-6);
88 1.0 / px_per_unit
89}
90
91fn project_to_screen(camera: &mut Camera, pos: Vec3, w: f32, h: f32) -> Option<glam::Vec2> {
92 let vp = camera.view_proj_matrix();
93 let clip = vp * pos.extend(1.0);
94 if clip.w.abs() <= 1e-6 {
95 return None;
96 }
97 let ndc = clip.truncate() / clip.w;
98 if !ndc.is_finite() {
99 return None;
100 }
101 Some(glam::Vec2::new(
102 (ndc.x * 0.5 + 0.5) * w,
103 (1.0 - (ndc.y * 0.5 + 0.5)) * h,
104 ))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::{data_units_per_px, data_units_per_px_3d, data_units_per_px_3d_camera};
110 use crate::core::BoundingBox;
111 use glam::Vec3;
112
113 #[test]
114 fn data_units_per_px_uses_min_2d_scale() {
115 let bounds = BoundingBox {
116 min: Vec3::new(0.0, 0.0, 0.0),
117 max: Vec3::new(100.0, 50.0, 0.0),
118 };
119 let scale = data_units_per_px(&bounds, (1000, 500));
120 assert!((scale - 0.1).abs() < 1e-6);
121 }
122
123 #[test]
124 fn data_units_per_px_3d_uses_screen_axes_scale() {
125 let bounds = BoundingBox {
126 min: Vec3::new(0.0, 0.0, 0.0),
127 max: Vec3::new(100.0, 50.0, 10.0),
128 };
129 let scale = data_units_per_px_3d(&bounds, (1000, 500));
130 assert!((scale - 0.1).abs() < 1e-6);
132 }
133
134 #[test]
135 fn data_units_per_px_3d_camera_returns_positive_finite_scale() {
136 let bounds = BoundingBox {
137 min: Vec3::new(-1.0, -1.0, -1.0),
138 max: Vec3::new(1.0, 1.0, 1.0),
139 };
140 let scale = data_units_per_px_3d_camera(&bounds, (1280, 720), Some((30.0, 20.0)));
141 assert!(scale.is_finite());
142 assert!(scale > 0.0);
143 }
144}