1use std::{
2 f64,
3 ops::{Deref, DerefMut},
4};
5
6use cgmath::{prelude::*, *};
7
8use crate::{
9 coords::{ViewRegion, WorldCoords, Zoom, ZoomLevel},
10 render::camera::{
11 Camera, EdgeInsets, InvertedViewProjection, Perspective, ViewProjection, FLIP_Y,
12 OPENGL_TO_WGPU_MATRIX,
13 },
14 util::{
15 math::{bounds_from_points, Aabb2, Aabb3, Plane},
16 ChangeObserver,
17 },
18 window::{LogicalSize, PhysicalSize},
19};
20
21const VIEW_REGION_PADDING: i32 = 1;
22const MAX_N_TILES: usize = 512;
23
24pub enum ViewStatePadding {
25 Loose,
27 Tight,
29}
30
31#[derive(Clone)] pub struct ViewState {
33 zoom: ChangeObserver<Zoom>,
34 camera: ChangeObserver<Camera>,
35 perspective: Perspective,
36
37 width: f64,
38 height: f64,
39 edge_insets: EdgeInsets,
40}
41
42impl ViewState {
43 pub fn new<F: Into<Rad<f64>>, P: Into<Deg<f64>>>(
44 window_size: PhysicalSize,
45 position: WorldCoords,
46 zoom: Zoom,
47 pitch: P,
48 fovy: F,
49 ) -> Self {
50 let camera = Camera::new((position.x, position.y), Deg(0.0), pitch.into());
51
52 let perspective = Perspective::new(fovy);
53
54 Self {
55 zoom: ChangeObserver::new(zoom),
56 camera: ChangeObserver::new(camera),
57 perspective,
58 width: window_size.width() as f64,
59 height: window_size.height() as f64,
60 edge_insets: EdgeInsets {
61 top: 0.0,
62 bottom: 0.0,
63 left: 0.0,
64 right: 0.0,
65 },
66 }
67 }
68 pub fn set_edge_insets(&mut self, edge_insets: EdgeInsets) {
69 self.edge_insets = edge_insets;
70 }
71
72 pub fn edge_insets(&self) -> &EdgeInsets {
73 &self.edge_insets
74 }
75
76 pub fn resize(&mut self, size: LogicalSize) {
77 self.width = size.width() as f64;
78 self.height = size.height() as f64;
79 }
80
81 pub fn create_view_region(
82 &self,
83 visible_level: ZoomLevel,
84 padding: ViewStatePadding,
85 ) -> Option<ViewRegion> {
86 self.view_region_bounding_box(&self.view_projection().invert())
87 .map(|bounding_box| {
88 ViewRegion::new(
89 bounding_box,
90 match padding {
91 ViewStatePadding::Loose => VIEW_REGION_PADDING,
92 ViewStatePadding::Tight => 0,
93 },
94 MAX_N_TILES,
95 *self.zoom,
96 visible_level,
97 )
98 })
99 }
100
101 fn get_intersection_time(
102 ray_origin: Vector3<f64>,
103 ray_direction: Vector3<f64>,
104 plane_origin: Vector3<f64>,
105 plane_normal: Vector3<f64>,
106 ) -> f64 {
107 let m = plane_origin - ray_origin;
108 let distance = (m).dot(plane_normal);
109
110 let approach_speed = ray_direction.dot(plane_normal);
111
112 return distance / approach_speed;
117
118 }
122
123 fn furthest_distance(&self, camera_height: f64, center_offset: Point2<f64>) -> f64 {
124 let perspective = &self.perspective;
125 let width = self.width;
126 let height = self.height;
127 let camera = self.camera.position();
128
129 let y = perspective.y_tan();
130 let x = perspective.x_tan(width, height);
131 let offset_x = perspective.offset_x(center_offset, width);
132 let offset_y = perspective.offset_y(center_offset, height);
133
134 let rotation = Matrix4::from_angle_x(self.camera.get_pitch())
135 * Matrix4::from_angle_y(self.camera.get_yaw())
136 * Matrix4::from_angle_z(self.camera.get_roll());
137
138 let rays = [
139 Vector3::new(x * (1.0 - offset_x), y * (1.0 - offset_y), 1.0),
140 Vector3::new(x * (-1.0 - offset_x), y * (1.0 - offset_y), 1.0),
141 Vector3::new(x * (1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
142 Vector3::new(x * (-1.0 - offset_x), y * (-1.0 - offset_y), 1.0),
143 ];
144 let ray_origin = Vector3::new(-camera.x, -camera.y, -camera_height);
145
146 let plane_origin = Vector3::new(-camera.x, -camera.y, 0.0);
147 let plane_normal = (rotation * Vector4::new(0.0, 0.0, 1.0, 1.0)).truncate();
148
149 rays.iter()
150 .map(|ray| Self::get_intersection_time(ray_origin, *ray, plane_origin, plane_normal))
151 .fold(0. / 0., f64::max)
152 }
153
154 pub fn camera_to_center_distance(&self) -> f64 {
155 let height = self.height;
156
157 let fovy = self.perspective.fovy();
158 let half_fovy = fovy / 2.0;
159
160 let camera_to_center_distance = (height / 2.0) / (half_fovy.tan()); camera_to_center_distance
163 }
164
165 #[tracing::instrument(skip_all)]
168 pub fn view_projection(&self) -> ViewProjection {
169 let width = self.width;
170 let height = self.height;
171
172 let center = self.edge_insets.center(width, height);
173 let center_offset = center - Vector2::new(width, height) / 2.0;
175
176 let camera_to_center_distance = self.camera_to_center_distance();
177
178 let camera_matrix = self.camera.calc_matrix(camera_to_center_distance);
179
180 let furthest = self.furthest_distance(camera_to_center_distance, center_offset);
182 let far_z = furthest * 1.01;
183
184 let near_z = height / 50.0;
185
186 let perspective =
187 self.perspective
188 .calc_matrix_with_center(width, height, near_z, far_z, center_offset);
189
190 let view_projection = perspective * camera_matrix;
192
193 ViewProjection(FLIP_Y * OPENGL_TO_WGPU_MATRIX * view_projection)
194 }
195
196 pub fn zoom(&self) -> Zoom {
197 *self.zoom
198 }
199
200 pub fn did_zoom_change(&self) -> bool {
201 self.zoom.did_change(0.05)
202 }
203
204 pub fn update_zoom(&mut self, new_zoom: Zoom) {
205 *self.zoom = new_zoom;
206 log::info!("zoom: {new_zoom}");
207 }
208
209 pub fn camera(&self) -> &Camera {
210 self.camera.deref()
211 }
212
213 pub fn camera_mut(&mut self) -> &mut Camera {
214 self.camera.deref_mut()
215 }
216
217 pub fn did_camera_change(&self) -> bool {
218 self.camera.did_change(0.05)
219 }
220
221 pub fn update_references(&mut self) {
222 self.camera.update_reference();
223 self.zoom.update_reference();
224 }
225
226 pub(crate) fn clip_to_window_transform(&self) -> Matrix4<f64> {
229 let min_depth = 0.0;
230 let max_depth = 1.0;
231 let x = 0.0;
232 let y = 0.0;
233 let ox = x + self.width / 2.0;
234 let oy = y + self.height / 2.0;
235 let oz = min_depth;
236 let pz = max_depth - min_depth;
237 Matrix4::from_cols(
238 Vector4::new(self.width / 2.0, 0.0, 0.0, 0.0),
239 Vector4::new(0.0, -self.height / 2.0, 0.0, 0.0),
240 Vector4::new(0.0, 0.0, pz, 0.0),
241 Vector4::new(ox, oy, oz, 1.0),
242 )
243 }
244
245 pub(crate) fn clip_to_window(&self, clip: &Vector4<f64>) -> Vector4<f64> {
249 #[rustfmt::skip]
250 let ndc = Vector4::new(
251 clip.x / clip.w,
252 clip.y / clip.w,
253 clip.z / clip.w,
254 1.0
255 );
256
257 self.clip_to_window_transform() * ndc
258 }
259
260 pub(crate) fn clip_to_window_maplibre(&self, clip: &Vector4<f64>) -> Vector4<f64> {
262 assert_eq!(clip.z, 0.0);
263 return Vector4::new(
264 ((clip.x / clip.w + 1.) / 2.) * self.width,
265 ((-clip.y / clip.w + 1.) / 2.) * self.height,
266 0.0,
267 1.0,
268 );
269 }
270
271 fn clip_to_window_vulkan(&self, clip: &Vector4<f64>) -> Vector3<f64> {
277 #[rustfmt::skip]
278 let ndc = Vector4::new(
279 clip.x / clip.w,
280 clip.y / clip.w,
281 clip.z / clip.w,
282 1.0
283 );
284
285 let min_depth = 0.0;
286 let max_depth = 1.0;
287
288 let x = 0.0;
289 let y = 0.0;
290 let ox = x + self.width / 2.0;
291 let oy = y + self.height / 2.0;
292 let oz = min_depth;
293 let px = self.width;
294 let py = self.height;
295 let pz = max_depth - min_depth;
296 let xd = ndc.x;
297 let yd = ndc.y;
298 let zd = ndc.z;
299 Vector3::new(px / 2.0 * xd + ox, py / 2.0 * yd + oy, pz * zd + oz)
300 }
301
302 fn window_to_world(
307 &self,
308 window: &Vector3<f64>,
309 inverted_view_proj: &InvertedViewProjection,
310 ) -> Vector3<f64> {
311 #[rustfmt::skip]
312 let fixed_window = Vector4::new(
313 window.x,
314 window.y,
315 window.z,
316 1.0
317 );
318
319 let ndc = self.clip_to_window_transform().invert().unwrap() * fixed_window;
320 let unprojected = inverted_view_proj.project(ndc);
321
322 Vector3::new(
323 unprojected.x / unprojected.w,
324 unprojected.y / unprojected.w,
325 unprojected.z / unprojected.w,
326 )
327 }
328
329 fn window_to_world_nalgebra(
333 window: &Vector3<f64>,
334 inverted_view_proj: &InvertedViewProjection,
335 width: f64,
336 height: f64,
337 ) -> Vector3<f64> {
338 let pt = Vector4::new(
339 2.0 * (window.x - 0.0) / width - 1.0,
340 2.0 * (height - window.y - 0.0) / height - 1.0,
341 window.z,
342 1.0,
343 );
344 let unprojected = inverted_view_proj.project(pt);
345
346 Vector3::new(
347 unprojected.x / unprojected.w,
348 unprojected.y / unprojected.w,
349 unprojected.z / unprojected.w,
350 )
351 }
352
353 pub fn window_to_world_at_ground(
355 &self,
356 window: &Vector2<f64>,
357 inverted_view_proj: &InvertedViewProjection,
358 bound: bool,
359 ) -> Option<Vector2<f64>> {
360 let near_world =
361 self.window_to_world(&Vector3::new(window.x, window.y, 0.0), inverted_view_proj);
362
363 let far_world =
364 self.window_to_world(&Vector3::new(window.x, window.y, 1.0), inverted_view_proj);
365
366 let u = -near_world.z / (far_world.z - near_world.z);
369 if !bound || (0.0..=1.01).contains(&u) {
370 let result = near_world + u * (far_world - near_world);
371 Some(Vector2::new(result.x, result.y))
372 } else {
373 None
374 }
375 }
376
377 pub fn view_region_bounding_box(
388 &self,
389 inverted_view_proj: &InvertedViewProjection,
390 ) -> Option<Aabb2<f64>> {
391 let screen_bounding_box = [
392 Vector2::new(0.0, 0.0),
393 Vector2::new(self.width, 0.0),
394 Vector2::new(self.width, self.height),
395 Vector2::new(0.0, self.height),
396 ]
397 .map(|point| self.window_to_world_at_ground(&point, inverted_view_proj, false));
398
399 let (min, max) = bounds_from_points(
400 screen_bounding_box
401 .into_iter()
402 .flatten()
403 .map(|point| [point.x, point.y]),
404 )?;
405
406 Some(Aabb2::new(Point2::from(min), Point2::from(max)))
407 }
408 pub fn view_region_bounding_box_ndc(&self) -> Option<Aabb2<f64>> {
414 let view_proj = self.view_projection();
415 let a = view_proj.project(Vector4::new(0.0, 0.0, 0.0, 1.0));
416 let b = view_proj.project(Vector4::new(1.0, 0.0, 0.0, 1.0));
417 let c = view_proj.project(Vector4::new(1.0, 1.0, 0.0, 1.0));
418
419 let a_ndc = self.clip_to_window(&a).truncate();
420 let b_ndc = self.clip_to_window(&b).truncate();
421 let c_ndc = self.clip_to_window(&c).truncate();
422 let to_ndc = Vector3::new(1.0 / self.width, 1.0 / self.height, 1.0);
423 let plane: Plane<f64> = Plane::from_points(
424 Point3::from_vec(a_ndc.mul_element_wise(to_ndc)),
425 Point3::from_vec(b_ndc.mul_element_wise(to_ndc)),
426 Point3::from_vec(c_ndc.mul_element_wise(to_ndc)),
427 )?;
428
429 let points = plane.intersection_points_aabb3(&Aabb3::new(
430 Point3::new(0.0, 0.0, 0.0),
431 Point3::new(1.0, 1.0, 1.0),
432 ));
433
434 let inverted_view_proj = view_proj.invert();
435
436 let from_ndc = Vector3::new(self.width, self.height, 1.0);
437 let vec = points
438 .iter()
439 .map(|point| {
440 self.window_to_world(&point.mul_element_wise(from_ndc), &inverted_view_proj)
441 })
442 .collect::<Vec<_>>();
443
444 let min_x = vec
445 .iter()
446 .map(|point| point.x)
447 .min_by(|a, b| a.partial_cmp(b).unwrap())?;
448 let min_y = vec
449 .iter()
450 .map(|point| point.y)
451 .min_by(|a, b| a.partial_cmp(b).unwrap())?;
452 let max_x = vec
453 .iter()
454 .map(|point| point.x)
455 .max_by(|a, b| a.partial_cmp(b).unwrap())?;
456 let max_y = vec
457 .iter()
458 .map(|point| point.y)
459 .max_by(|a, b| a.partial_cmp(b).unwrap())?;
460 Some(Aabb2::new(
461 Point2::new(min_x, min_y),
462 Point2::new(max_x, max_y),
463 ))
464 }
465 pub fn height(&self) -> f64 {
466 self.height
467 }
468 pub fn width(&self) -> f64 {
469 self.width
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use cgmath::{Deg, Matrix4, Vector2, Vector4};
476
477 use crate::{
478 coords::{WorldCoords, Zoom},
479 render::view_state::ViewState,
480 window::PhysicalSize,
481 };
482
483 #[test]
484 fn conform_transformation() {
485 let fov = Deg(60.0);
486 let mut state = ViewState::new(
487 PhysicalSize::new(800, 600).unwrap(),
488 WorldCoords::at_ground(0.0, 0.0),
489 Zoom::new(10.0),
490 Deg(0.0),
491 fov,
492 );
493
494 let projection = state.view_projection().invert();
497
498 let bottom_left = state
499 .window_to_world_at_ground(&Vector2::new(0.0, 0.0), &projection, true)
500 .unwrap();
501 println!("bottom left on ground {:?}", bottom_left);
502 let top_right = state
503 .window_to_world_at_ground(&Vector2::new(state.width, state.height), &projection, true)
504 .unwrap();
505 println!("top right on ground {:?}", top_right);
506
507 let mut rotated = Matrix4::from_angle_x(Deg(-30.0))
508 * Vector4::new(bottom_left.x, bottom_left.y, 0.0, 0.0);
509
510 println!("bottom left rotated around x axis {:?}", rotated);
511
512 rotated = Matrix4::from_angle_y(Deg(-30.0)) * rotated;
513
514 println!("bottom left rotated around x and y axis {:?}", rotated);
515
516 state.camera.set_pitch(Deg(30.0));
517 }
521}