Skip to main content

codecraft/
views.rs

1//! Which parts of the frame the world is drawn into, and from where: a view is
2//! a rectangle plus a camera, and the renderer draws the world once per view.
3//!
4//! ```no_run
5//! # use codecraft::{Camera, Views};
6//! # fn demo(one: Camera, two: Camera) {
7//! // Two players at one screen, with the map they share over the join.
8//! let views = Views::split(one, two, 1920.0, 1080.0).with_minimap(Camera::default());
9//! # }
10//! ```
11use 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/// One view onto the world: where it lands on the frame, and the camera that draws it.
22#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct View {
24    /// In pixels, from the top left of the frame.
25    pub rect: Rect,
26    pub camera: Camera,
27    /// Whether this view is one of the tiles that cover the frame (what a resize divides up) or an overlay laid over them.
28    pub tiles: bool,
29}
30
31impl View {
32    /// A view that is one of the tiles covering the frame.
33    pub fn new(rect: Rect, camera: Camera) -> Self {
34        Self {
35            rect,
36            camera,
37            tiles: true,
38        }
39    }
40
41    /// A view drawn over the tiles, keeping what they left around it.
42    pub fn overlay(rect: Rect, camera: Camera) -> Self {
43        Self {
44            rect,
45            camera,
46            tiles: false,
47        }
48    }
49
50    /// How wide against how tall, for the projection.
51    pub fn aspect(&self) -> f32 {
52        self.rect.width / self.rect.height.max(1.0)
53    }
54
55    /// Where a point in the world lands on the frame in pixels through this view's camera.
56    /// `None` when behind the camera, past the far plane, or outside this view's rectangle.
57    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    /// [`View::project`] without the rectangle check: where a point in front of the camera
63    /// lands even when that is off the edge, so things half in view can still be placed.
64    pub fn project_unclipped(&self, world: Vec3) -> Option<(f32, f32)> {
65        let clip = self.camera.view_proj(self.aspect()) * world.extend(1.0);
66        // Perspective: behind is w <= 0. Orthographic: w is 1 everywhere, behind is z < 0.
67        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    /// Whether a screen point is within `margin` pixels of this view.
80    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/// Every view the frame is drawn from, in the order they are drawn. Never empty.
89#[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    /// The whole frame, from one camera; the rect is filled in by the renderer.
100    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    /// Two players at one screen, side by side.
105    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    /// Adds a shared map laid over the bottom centre of the frame.
114    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    /// The camera a click or a ray should be read against: the first view's.
157    pub fn camera(&self) -> Camera {
158        self.0
159            .first()
160            .map(|view| view.camera)
161            .unwrap_or_else(Camera::default)
162    }
163
164    /// Points the frame's one view from `camera`, and says whether it did; with several views it leaves them alone.
165    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    /// Which view a point on the frame is inside, latest (topmost) first.
176    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    /// Fills in any view that has no size yet and rescales the rest to the frame; called by the renderer every frame.
181    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    /// Two players looking at the origin, one from -X and one from +Z, on a 1920x1080 frame.
324    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        // Half the visible width at ten units out: the default lens is 45 degrees tall.
388        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}