1use glam::Vec3;
12
13use crate::ecs::Resource;
14use crate::sceneobjects::cameras::Camera;
15use crate::ui::Rect;
16
17const MINIMAP_HEIGHT: f32 = 0.25;
18
19const MINIMAP_MARGIN: f32 = 0.02;
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct View {
24 pub rect: Rect,
26 pub camera: Camera,
27 pub tiles: bool,
29}
30
31impl View {
32 pub fn new(rect: Rect, camera: Camera) -> Self {
34 Self {
35 rect,
36 camera,
37 tiles: true,
38 }
39 }
40
41 pub fn overlay(rect: Rect, camera: Camera) -> Self {
43 Self {
44 rect,
45 camera,
46 tiles: false,
47 }
48 }
49
50 pub fn aspect(&self) -> f32 {
52 self.rect.width / self.rect.height.max(1.0)
53 }
54
55 pub fn project(&self, world: Vec3) -> Option<(f32, f32)> {
58 let (x, y) = self.project_unclipped(world)?;
59 self.rect.contains(x, y).then_some((x, y))
60 }
61
62 pub fn project_unclipped(&self, world: Vec3) -> Option<(f32, f32)> {
65 let clip = self.camera.view_proj(self.aspect()) * world.extend(1.0);
66 if clip.w <= 0.0 || clip.z < 0.0 {
68 return None;
69 }
70 let ndc = clip.truncate() / clip.w;
71 if ndc.z > 1.0 {
72 return None;
73 }
74 let x = self.rect.x + (ndc.x + 1.0) * 0.5 * self.rect.width;
75 let y = self.rect.y + (1.0 - ndc.y) * 0.5 * self.rect.height;
76 Some((x, y))
77 }
78
79 pub fn is_near(&self, x: f32, y: f32, margin: f32) -> bool {
81 x >= self.rect.x - margin
82 && x <= self.rect.x + self.rect.width + margin
83 && y >= self.rect.y - margin
84 && y <= self.rect.y + self.rect.height + margin
85 }
86}
87
88#[derive(Resource, Clone, Debug, PartialEq)]
90pub struct Views(Vec<View>);
91
92impl Default for Views {
93 fn default() -> Self {
94 Self::one(Camera::default())
95 }
96}
97
98impl Views {
99 pub fn one(camera: Camera) -> Self {
101 Self(vec![View::new(Rect::new(0.0, 0.0, 0.0, 0.0), camera)])
102 }
103
104 pub fn split(left: Camera, right: Camera, width: f32, height: f32) -> Self {
106 let half = width * 0.5;
107 Self(vec![
108 View::new(Rect::new(0.0, 0.0, half, height), left),
109 View::new(Rect::new(half, 0.0, width - half, height), right),
110 ])
111 }
112
113 pub fn with_minimap(mut self, camera: Camera) -> Self {
115 let frame = self.frame();
116 let side = frame.height * MINIMAP_HEIGHT;
117 let margin = frame.height * MINIMAP_MARGIN;
118 self.0.push(View::overlay(
119 Rect::new(
120 frame.center_x() - side * 0.5,
121 frame.height - side - margin,
122 side,
123 side,
124 ),
125 camera,
126 ));
127 self
128 }
129
130 fn frame(&self) -> Rect {
131 self.0.iter().filter(|view| view.tiles).fold(
132 Rect::new(0.0, 0.0, 0.0, 0.0),
133 |frame, view| {
134 Rect::new(
135 0.0,
136 0.0,
137 frame.width.max(view.rect.x + view.rect.width),
138 frame.height.max(view.rect.y + view.rect.height),
139 )
140 },
141 )
142 }
143
144 pub fn iter(&self) -> impl Iterator<Item = &View> {
145 self.0.iter()
146 }
147
148 pub fn len(&self) -> usize {
149 self.0.len()
150 }
151
152 pub fn is_empty(&self) -> bool {
153 self.0.is_empty()
154 }
155
156 pub fn camera(&self) -> Camera {
158 self.0
159 .first()
160 .map(|view| view.camera)
161 .unwrap_or_else(Camera::default)
162 }
163
164 pub fn set_camera(&mut self, camera: Camera) -> bool {
166 match self.0.as_mut_slice() {
167 [only] => {
168 only.camera = camera;
169 true
170 }
171 _ => false,
172 }
173 }
174
175 pub fn at(&self, x: f32, y: f32) -> Option<&View> {
177 self.0.iter().rev().find(|view| view.rect.contains(x, y))
178 }
179
180 pub fn fit(&mut self, width: f32, height: f32) {
182 let frame = self.frame();
183 let (was_width, was_height) = (frame.width, frame.height);
184 for view in &mut self.0 {
185 if was_width <= 0.0 || was_height <= 0.0 {
186 view.rect = Rect::new(0.0, 0.0, width, height);
187 continue;
188 }
189 let (sx, sy) = (width / was_width, height / was_height);
190 view.rect = Rect::new(
191 view.rect.x * sx,
192 view.rect.y * sy,
193 view.rect.width * sx,
194 view.rect.height * sy,
195 );
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn camera(x: f32) -> Camera {
205 Camera::looking_at(glam::Vec3::new(x, 0.0, 0.0), glam::Vec3::ZERO)
206 }
207
208 #[test]
209 fn one_view_fills_the_frame() {
210 let mut views = Views::one(camera(0.0));
211 views.fit(1920.0, 1080.0);
212
213 assert_eq!(views.len(), 1);
214 let view = views.iter().next().unwrap();
215 assert_eq!(view.rect, Rect::new(0.0, 0.0, 1920.0, 1080.0));
216 assert!(view.tiles, "it is the frame");
217 }
218
219 #[test]
220 fn a_split_screen_covers_the_frame_exactly() {
221 let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
222 views.fit(1920.0, 1080.0);
223 let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();
224
225 assert_eq!(halves[0], Rect::new(0.0, 0.0, 960.0, 1080.0));
226 assert_eq!(halves[1], Rect::new(960.0, 0.0, 960.0, 1080.0));
227 assert_eq!(
228 halves[0].x + halves[0].width,
229 halves[1].x,
230 "they have to meet exactly",
231 );
232 assert_eq!(halves[1].x + halves[1].width, 1920.0, "and reach the edge");
233 }
234
235 #[test]
236 fn an_odd_width_still_covers_every_pixel() {
237 let mut views = Views::split(camera(-1.0), camera(1.0), 1921.0, 1080.0);
238 views.fit(1921.0, 1080.0);
239 let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();
240
241 assert_eq!(halves[0].x + halves[0].width, halves[1].x);
242 assert_eq!(halves[1].x + halves[1].width, 1921.0);
243 }
244
245 #[test]
246 fn each_half_knows_it_is_half_as_wide() {
247 let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
248 views.fit(1920.0, 1080.0);
249
250 for view in views.iter() {
251 assert!(
252 (view.aspect() - 960.0 / 1080.0).abs() < 1e-5,
253 "a half-width view is not the frame's shape: {}",
254 view.aspect(),
255 );
256 }
257 }
258
259 #[test]
260 fn the_minimap_sits_over_the_join_at_the_bottom() {
261 let mut views =
262 Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
263 views.fit(1920.0, 1080.0);
264
265 let map = views.iter().last().unwrap();
266 assert!(!map.tiles, "it is laid over the two, not cut into them");
267 assert!(
268 (map.rect.center_x() - 960.0).abs() < 1e-4,
269 "centred on the join: {:?}",
270 map.rect,
271 );
272 assert!(
273 map.rect.y + map.rect.height < 1080.0,
274 "floating off the bottom edge rather than glued to it",
275 );
276 assert_eq!(map.rect.width, map.rect.height, "square, like the ground");
277 }
278
279 #[test]
280 fn the_minimap_overlaps_both_players() {
281 let mut views =
282 Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
283 views.fit(1920.0, 1080.0);
284
285 let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
286 let (left, right, map) = (rects[0], rects[1], rects[2]);
287 assert!(map.x < left.x + left.width, "it reaches into the left half");
288 assert!(map.x + map.width > right.x, "and into the right one");
289 }
290
291 #[test]
292 fn a_resize_keeps_the_layout() {
293 let mut views =
294 Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
295 views.fit(1920.0, 1080.0);
296 views.fit(1280.0, 720.0);
297
298 let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
299 assert_eq!(rects[0], Rect::new(0.0, 0.0, 640.0, 720.0));
300 assert_eq!(rects[1], Rect::new(640.0, 0.0, 640.0, 720.0));
301 assert!(
302 (rects[2].center_x() - 640.0).abs() < 1e-4,
303 "the map stays over the join: {:?}",
304 rects[2],
305 );
306 }
307
308 #[test]
309 fn a_point_belongs_to_the_view_drawn_last() {
310 let mut views =
311 Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
312 views.fit(1920.0, 1080.0);
313 let map = views.iter().last().unwrap().rect;
314
315 let over_map = views.at(map.center_x(), map.center_y()).unwrap().rect;
316 assert_eq!(over_map, map, "the map wins where it covers the players");
317
318 let left = views.at(100.0, 100.0).unwrap().rect;
319 assert_eq!(left.x, 0.0, "and away from it the player view answers");
320 assert!(views.at(-1.0, 100.0).is_none(), "off the frame is nobody's");
321 }
322
323 fn facing() -> (View, View) {
325 let from_z = Camera::looking_at(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
326 let mut views = Views::split(camera(-10.0), from_z, 1920.0, 1080.0);
327 views.fit(1920.0, 1080.0);
328 let mut views = views.iter().copied();
329 (views.next().unwrap(), views.next().unwrap())
330 }
331
332 #[test]
333 fn what_a_camera_looks_at_lands_in_the_middle_of_its_own_view() {
334 let (left, right) = facing();
335 let (x, y) = left.project(Vec3::ZERO).unwrap();
336 assert!(
337 (x - 480.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
338 "({x}, {y})"
339 );
340 let (x, y) = right.project(Vec3::ZERO).unwrap();
341 assert!(
342 (x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
343 "({x}, {y})"
344 );
345 }
346
347 #[test]
348 fn up_in_the_world_is_up_the_screen() {
349 let (left, _) = facing();
350 let (x, y) = left.project(Vec3::new(0.0, 1.0, 0.0)).unwrap();
351 assert!(y < 540.0, "above the target came out at y {y}");
352 assert!((x - 480.0).abs() < 1e-2, "and dead ahead: x {x}");
353 }
354
355 #[test]
356 fn a_point_behind_the_camera_is_nowhere_on_screen() {
357 let (left, _) = facing();
358 assert_eq!(left.project(Vec3::new(-20.0, 0.0, 0.0)), None);
359 assert_eq!(left.project(Vec3::new(-10.0, 0.0, 0.0)), None);
360 }
361
362 #[test]
363 fn a_point_past_the_far_plane_is_nowhere_on_screen_either() {
364 let (left, _) = facing();
365 let eye = left.camera.eye.x;
366 let just_short = Vec3::new(eye + left.camera.far * 0.99, 0.0, 0.0);
367 let just_past = Vec3::new(eye + left.camera.far * 1.01, 0.0, 0.0);
368 assert!(left.project(just_short).is_some());
369 assert_eq!(left.project(just_past), None);
370 }
371
372 #[test]
373 fn a_point_in_the_other_players_view_is_none_through_this_one() {
374 let (left, right) = facing();
375 let ahead_of_right = Vec3::new(0.0, 0.0, 5.0);
376 let (x, y) = right.project(ahead_of_right).unwrap();
377 assert!(
378 (x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
379 "({x}, {y})"
380 );
381 assert_eq!(left.project(ahead_of_right), None);
382 }
383
384 #[test]
385 fn the_edge_a_projection_falls_off_is_the_views_own() {
386 let (left, _) = facing();
387 let half_width = (45f32.to_radians() * 0.5).tan() * left.aspect() * 10.0;
389 let inside = Vec3::new(0.0, 0.0, half_width * 0.99);
390 let outside = Vec3::new(0.0, 0.0, half_width * 1.01);
391 let (x, _) = left.project(inside).unwrap();
392 assert!(x > 940.0 && x <= 960.0, "{x}");
393 assert_eq!(left.project(outside), None);
394 }
395}