1use egui::{Rect, Vec2};
2use glam::{DQuat, DVec3};
3use pfx_load::scene::{Camera, Projection};
4
5pub const DEGREES_PER_POINT: f64 = 0.25;
6pub const DOLLY_PER_POINT: f64 = 0.002;
7pub const NEAREST: f64 = 0.001;
8pub const FILL: f64 = 0.8;
9pub const STEP_POINTS: f64 = 60.0;
10const WORLD_UP: DVec3 = DVec3::Y;
11
12pub type Bounds = [[f64; 3]; 2];
13
14pub const EMPTY: Bounds = [[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]];
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Basis {
18 pub right: DVec3,
19 pub up: DVec3,
20 pub forward: DVec3,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
24pub struct Eye {
25 pub position: [f64; 3],
26 pub target: [f64; 3],
27 pub up: [f64; 3],
28 pub fov_y_deg: f64,
29 pub shift: [f64; 2],
30 pub ortho_height: Option<f64>,
31}
32
33pub fn wide(value: f32) -> f64 {
34 value.to_string().parse().unwrap_or(f64::from(value))
35}
36
37pub fn narrow(value: f64) -> f32 {
38 value as f32
39}
40
41pub fn passepartout(image: Rect, aspect: f32) -> Option<Rect> {
42 if !(aspect.is_finite() && aspect > 0.0) || image.width() < 1.0 || image.height() < 1.0 {
43 return None;
44 }
45 let size = if aspect >= image.width() / image.height() {
46 Vec2::new(image.width(), image.width() / aspect)
47 } else {
48 Vec2::new(image.height() * aspect, image.height())
49 };
50 Some(Rect::from_center_size(image.center(), size))
51}
52
53pub fn reach(image: Rect, frame: Rect) -> [f64; 2] {
54 [
55 f64::from(image.width()) / f64::from(frame.width().max(1e-3)),
56 f64::from(image.height()) / f64::from(frame.height().max(1e-3)),
57 ]
58}
59
60pub fn through(camera: &Camera, reach: [f64; 2]) -> Camera {
61 let eye = Eye::of(camera).widened(reach);
62 Camera {
63 projection: eye.projection(),
64 shift: eye.shift.map(narrow),
65 ..*camera
66 }
67}
68
69impl Eye {
70 pub fn of(camera: &Camera) -> Eye {
71 let (fov_y_deg, ortho_height) = match camera.projection {
72 Projection::Perspective { fov } => (wide(fov), None),
73 Projection::Orthographic { height } => (40.0, Some(wide(height))),
74 };
75 Eye {
76 position: camera.at.map(wide),
77 target: camera.look_at.map(wide),
78 up: camera.up.map(wide),
79 fov_y_deg,
80 shift: camera.shift.map(wide),
81 ortho_height,
82 }
83 }
84
85 pub fn basis(&self) -> Basis {
86 let position = DVec3::from_array(self.position);
87 let target = DVec3::from_array(self.target);
88 let forward = (target - position).try_normalize().unwrap_or(DVec3::NEG_Z);
89 let up = DVec3::from_array(self.up);
90 let right = forward
91 .cross(up)
92 .try_normalize()
93 .unwrap_or_else(|| forward.any_orthonormal_vector());
94 Basis {
95 right,
96 up: right.cross(forward),
97 forward,
98 }
99 }
100
101 pub fn distance(&self) -> f64 {
102 (DVec3::from_array(self.position) - DVec3::from_array(self.target)).length()
103 }
104
105 pub fn depth_range(&self, bounds: Bounds) -> (f64, f64) {
106 let forward = self.basis().forward;
107 let position = DVec3::from_array(self.position);
108 let mut nearest = f64::INFINITY;
109 let mut farthest = f64::NEG_INFINITY;
110 for x in [bounds[0][0], bounds[1][0]] {
111 for y in [bounds[0][1], bounds[1][1]] {
112 for z in [bounds[0][2], bounds[1][2]] {
113 let along = (DVec3::new(x, y, z) - position).dot(forward);
114 nearest = nearest.min(along);
115 farthest = farthest.max(along);
116 }
117 }
118 }
119 if !nearest.is_finite() || !farthest.is_finite() {
120 return (0.01, 100.0);
121 }
122 let span = (farthest - nearest).max(1e-3);
123 let far = farthest + span * 0.05 + 1e-3;
124 let near = (nearest - span * 0.05).max(far * 1e-4);
125 (near, far.max(near * 2.0))
126 }
127
128 pub fn projection(&self) -> Projection {
129 match self.ortho_height {
130 Some(height) => Projection::Orthographic {
131 height: narrow(height),
132 },
133 None => Projection::Perspective {
134 fov: narrow(self.fov_y_deg),
135 },
136 }
137 }
138
139 pub fn widened(&self, reach: [f64; 2]) -> Eye {
140 let [across, tall] = reach;
141 let half = (self.fov_y_deg.to_radians() * 0.5).tan() * tall;
142 Eye {
143 fov_y_deg: match self.ortho_height {
144 Some(_) => self.fov_y_deg,
145 None => (half.atan() * 2.0).to_degrees(),
146 },
147 ortho_height: self.ortho_height.map(|height| height * tall),
148 shift: [self.shift[0] / across, self.shift[1] / tall],
149 ..*self
150 }
151 }
152
153 pub fn camera(&self, base: &Camera, bounds: Bounds) -> Camera {
154 let (near, far) = self.depth_range(bounds);
155 let projection = self.projection();
156 let basis = self.basis();
157 Camera {
158 projection,
159 at: self.position.map(narrow),
160 look_at: self.target.map(narrow),
161 up: basis.up.to_array().map(narrow),
162 near: narrow(near),
163 far: narrow(far),
164 shift: self.shift.map(narrow),
165 depth_of_field: None,
166 ..*base
167 }
168 }
169
170 pub fn turn(&self, points: [f64; 2]) -> Eye {
171 let position = DVec3::from_array(self.position);
172 let target = DVec3::from_array(self.target);
173 let yaw = DQuat::from_axis_angle(WORLD_UP, -(points[0] * DEGREES_PER_POINT).to_radians());
174 let offset = yaw * (position - target);
175 let up = yaw * DVec3::from_array(self.up);
176 let turned = Eye {
177 position: (target + offset).to_array(),
178 up: up.to_array(),
179 ..*self
180 };
181 let frame = turned.basis();
182 let pitch =
183 DQuat::from_axis_angle(frame.right, -(points[1] * DEGREES_PER_POINT).to_radians());
184 Eye {
185 position: (target + pitch * offset).to_array(),
186 up: (pitch * frame.up).to_array(),
187 ..*self
188 }
189 }
190
191 pub fn span(&self, height_points: f64) -> f64 {
192 if height_points <= 0.0 {
193 return 0.0;
194 }
195 let tall = match self.ortho_height {
196 Some(height) => height,
197 None => 2.0 * self.distance() * (self.fov_y_deg.to_radians() / 2.0).tan(),
198 };
199 tall / height_points
200 }
201
202 pub fn pan(&self, points: [f64; 2], height_points: f64) -> Eye {
203 let frame = self.basis();
204 let shift = (-points[0] * frame.right + points[1] * frame.up) * self.span(height_points);
205 Eye {
206 position: (DVec3::from_array(self.position) + shift).to_array(),
207 target: (DVec3::from_array(self.target) + shift).to_array(),
208 ..*self
209 }
210 }
211
212 pub fn dolly(&self, points: f64) -> Eye {
213 let target = DVec3::from_array(self.target);
214 let offset = DVec3::from_array(self.position) - target;
215 let factor = (-points * DOLLY_PER_POINT).exp();
216 if let Some(height) = self.ortho_height {
217 return Eye {
218 ortho_height: Some((height * factor).max(NEAREST)),
219 ..*self
220 };
221 }
222 let Some(direction) = offset.try_normalize() else {
223 return *self;
224 };
225 let distance = offset.length();
226 let nearer = (distance * factor).max(NEAREST.min(distance));
227 Eye {
228 position: (target + direction * nearer).to_array(),
229 ..*self
230 }
231 }
232
233 pub fn framed(&self, bounds: Bounds, aspect: f64) -> Eye {
234 let low = DVec3::from_array(bounds[0]);
235 let high = DVec3::from_array(bounds[1]);
236 let centre = (low + high) * 0.5;
237 let radius = ((high - low).length() * 0.5).max(NEAREST);
238 let back = -self.basis().forward;
239 if self.ortho_height.is_some() {
240 let height = 2.0 * radius / FILL / aspect.clamp(1e-6, 1.0);
241 return Eye {
242 position: (centre + back * self.distance().max(radius * 2.0)).to_array(),
243 target: centre.to_array(),
244 ortho_height: Some(height),
245 shift: [0.0, 0.0],
246 ..*self
247 };
248 }
249 let half_y = (self.fov_y_deg.to_radians() * 0.5).max(1e-6);
250 let half_x = (half_y.tan() * aspect.max(1e-6)).atan();
251 let half = half_y.min(half_x);
252 let distance = radius / half.sin() / FILL;
253 Eye {
254 position: (centre + back * distance).to_array(),
255 target: centre.to_array(),
256 shift: [0.0, 0.0],
257 ..*self
258 }
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn eye() -> Eye {
267 Eye {
268 position: [0.0, 0.0, 2.0],
269 target: [0.0, 0.0, 0.0],
270 up: [0.0, 1.0, 0.0],
271 fov_y_deg: 40.0,
272 shift: [0.0, 0.0],
273 ortho_height: None,
274 }
275 }
276
277 fn close(a: [f64; 3], b: [f64; 3]) -> bool {
278 a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-9)
279 }
280
281 fn offset(eye: &Eye) -> DVec3 {
282 DVec3::from_array(eye.position) - DVec3::from_array(eye.target)
283 }
284
285 #[test]
286 fn a_sideways_drag_turns_around_the_target_by_its_angle() {
287 let start = eye();
288 let turned = start.turn([120.0, 0.0]);
289 let angle = offset(&start).angle_between(offset(&turned)).to_degrees();
290 assert!((angle - 120.0 * DEGREES_PER_POINT).abs() < 1e-9, "{angle}");
291 assert_eq!(turned.target, start.target);
292 assert!((offset(&turned).length() - 2.0).abs() < 1e-12);
293 assert!(turned.position[0] < 0.0, "{:?}", turned.position);
294 assert!(close(turned.up, start.up));
295 }
296
297 #[test]
298 fn a_drag_down_lifts_the_eye_by_its_angle() {
299 let start = eye();
300 let turned = start.turn([0.0, 80.0]);
301 let angle = offset(&start).angle_between(offset(&turned)).to_degrees();
302 assert!((angle - 80.0 * DEGREES_PER_POINT).abs() < 1e-9, "{angle}");
303 assert!(turned.position[1] > 0.0, "{:?}", turned.position);
304 let frame = turned.basis();
305 assert!(frame.up.dot(frame.forward).abs() < 1e-12);
306 }
307
308 #[test]
309 fn turning_back_returns_home() {
310 let start = Eye {
311 position: [0.3, 1.1, 2.5],
312 target: [0.1, 0.2, -0.4],
313 ..eye()
314 };
315 let back = start.turn([37.0, 0.0]).turn([-37.0, 0.0]);
316 assert!(close(back.position, start.position));
317 }
318
319 #[test]
320 fn a_straight_down_camera_still_turns() {
321 let plan = Eye {
322 position: [0.0, 10.0, 0.0],
323 up: [0.0, 0.0, -1.0],
324 ortho_height: Some(2.2),
325 ..eye()
326 };
327 let turned = plan.turn([0.0, 40.0]);
328 assert!(turned.position.iter().all(|value| value.is_finite()));
329 let angle = offset(&plan).angle_between(offset(&turned)).to_degrees();
330 assert!((angle - 10.0).abs() < 1e-9, "{angle}");
331 }
332
333 #[test]
334 fn dolly_never_crosses_the_target() {
335 let mut moved = eye();
336 for _ in 0..2000 {
337 moved = moved.dolly(400.0);
338 assert!(offset(&moved).dot(offset(&eye())) > 0.0);
339 }
340 assert!((offset(&moved).length() - NEAREST).abs() < 1e-12);
341 assert!(offset(&eye().dolly(-100.0)).length() > 2.0);
342 let ortho = Eye {
343 ortho_height: Some(2.0),
344 ..eye()
345 };
346 let zoomed = ortho.dolly(100.0);
347 assert_eq!(zoomed.position, ortho.position);
348 assert!(zoomed.ortho_height.unwrap() < 2.0);
349 }
350
351 #[test]
352 fn a_pan_moves_eye_and_target_together_by_the_view_span() {
353 let start = eye();
354 let moved = start.pan([100.0, 0.0], 500.0);
355 let tall = 2.0 * 2.0 * 20.0_f64.to_radians().tan();
356 let expected = -100.0 * tall / 500.0;
357 assert!((moved.position[0] - expected).abs() < 1e-12);
358 assert!((moved.target[0] - expected).abs() < 1e-12);
359 assert!(close(offset(&moved).to_array(), offset(&start).to_array()));
360 assert!(start.pan([0.0, 50.0], 500.0).target[1] > 0.0);
361 }
362
363 #[test]
364 fn framing_puts_the_box_in_the_middle_and_fits_it() {
365 let start = Eye {
366 position: [3.0, 2.0, 5.0],
367 target: [0.0, 0.0, 0.0],
368 ..eye()
369 };
370 let bounds = [[1.0, 0.0, -1.0], [2.0, 1.0, 0.0]];
371 let framed = start.framed(bounds, 1.6);
372 assert!(close(framed.target, [1.5, 0.5, -0.5]));
373 let radius = 3.0_f64.sqrt() * 0.5;
374 let expected = radius / 20.0_f64.to_radians().sin() / FILL;
375 assert!((framed.distance() - expected).abs() < 1e-9);
376 let before = offset(&start).normalize();
377 let after = offset(&framed).normalize();
378 assert!((before - after).length() < 1e-9);
379 }
380
381 #[test]
382 fn the_scene_camera_reads_and_writes_back() {
383 let camera = Camera {
384 at: [0.0, 1.2, 3.2],
385 look_at: [0.0, 0.35, 0.0],
386 projection: Projection::Perspective { fov: 40.0 },
387 ..Camera::default()
388 };
389 let eye = Eye::of(&camera);
390 assert_eq!(eye.position, [0.0, 1.2, 3.2]);
391 assert_eq!(eye.target, [0.0, 0.35, 0.0]);
392 assert_eq!(eye.fov_y_deg, 40.0);
393 let back = eye.camera(&camera, [[-2.0, -0.1, -2.0], [2.0, 1.0, 2.0]]);
394 assert_eq!(back.at, camera.at);
395 assert_eq!(back.look_at, camera.look_at);
396 assert_eq!(back.projection, camera.projection);
397 assert!(back.near > 0.0 && back.far > back.near);
398 let (_, right, up) = back.axes();
399 assert!((right[1]).abs() < 1e-6 && up[1] > 0.9);
400 }
401
402 #[test]
403 fn the_depth_range_holds_the_bounds() {
404 let (near, far) = eye().depth_range([[-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]]);
405 assert!(near > 0.0 && near < 1.0, "{near}");
406 assert!(far > 3.0 && far < 3.5, "{far}");
407 }
408
409 #[test]
410 fn the_passepartout_is_the_largest_centred_frame_of_the_aspect() {
411 let image = Rect::from_min_size(egui::Pos2::new(10.0, 20.0), Vec2::new(640.0, 480.0));
412 let letterbox = passepartout(image, 16.0 / 9.0).unwrap();
413 assert_eq!(letterbox.size(), Vec2::new(640.0, 360.0));
414 assert_eq!(letterbox.center(), image.center());
415 let pillarbox = passepartout(image, 1.0).unwrap();
416 assert_eq!(pillarbox.size(), Vec2::new(480.0, 480.0));
417 assert_eq!(pillarbox.center(), image.center());
418 assert_eq!(passepartout(image, 4.0 / 3.0), Some(image));
419 assert_eq!(passepartout(image, 0.0), None);
420 assert_eq!(passepartout(image, f32::NAN), None);
421 }
422
423 #[test]
424 fn a_widened_eye_over_the_panel_shows_the_frame_where_the_camera_put_it() {
425 let image = Rect::from_min_size(egui::Pos2::new(10.0, 20.0), Vec2::new(640.0, 480.0));
426 for aspect in [16.0 / 9.0, 0.75] {
427 let frame = passepartout(image, aspect).unwrap();
428 for ortho in [None, Some(3.0)] {
429 let own = Eye {
430 position: [0.4, 1.1, 2.9],
431 target: [0.1, 0.2, -0.3],
432 shift: [0.12, -0.3],
433 ortho_height: ortho,
434 ..eye()
435 };
436 let wide = own.widened(reach(image, frame));
437 let panel = crate::lens::Lens::new(&wide, image);
438 let framed = crate::lens::Lens::new(&own, frame);
439 for point in [[0.0, 0.0, 0.0], [0.6, -0.2, 0.4], [-0.9, 0.8, -1.0]] {
440 let point = DVec3::from_array(point);
441 let (a, b) = (
442 panel.project(point).unwrap(),
443 framed.project(point).unwrap(),
444 );
445 assert!((a - b).length() < 1e-3, "{aspect} {ortho:?} {a:?} {b:?}");
446 }
447 }
448 }
449 }
450
451 #[test]
452 fn the_engine_camera_through_the_frame_matches_the_widened_eye() {
453 let camera = Camera {
454 at: [0.0, 1.2, 3.2],
455 look_at: [0.0, 0.35, 0.0],
456 projection: Projection::Perspective { fov: 40.0 },
457 shift: [0.1, -0.2],
458 ..Camera::default()
459 };
460 let wide = through(&camera, [1.0, 4.0 / 3.0]);
461 let eye = Eye::of(&camera).widened([1.0, 4.0 / 3.0]);
462 assert_eq!(wide.projection, eye.projection());
463 assert_eq!(wide.shift, [0.1, -0.15]);
464 assert_eq!(
465 (wide.at, wide.near, wide.far),
466 (camera.at, camera.near, camera.far)
467 );
468 assert_eq!(through(&camera, [1.0, 1.0]).projection, camera.projection);
469 }
470}