Skip to main content

shuyi_ui/grid/
layout.rs

1//! Where a scene's tiles go on the console, in pixels: what every grid
2//! skin works out from its own tile size, and the shared drawing (the path,
3//! the threat arrows) works from. The tiles are laid out by a
4//! [`Projection`]: today top-down squares only (ADR-0003 §6; the
5//! isometric one is ticket 1126).
6
7use shuyi_core::Pos;
8
9use crate::frame::console::ConsoleSpec;
10use crate::frame::glyph_buffer::{PxRect, Rect};
11
12/// A console cell in pixels, across × down. The grid skins lay a map out
13/// in a text console's cells ([`ConsoleSpec::text`]): 8 × 16 pixels.
14pub const CELL_PX: (i32, i32) = (
15    ConsoleSpec::text(0, 0).cell_w as i32,
16    ConsoleSpec::text(0, 0).cell_h as i32,
17);
18
19/// `cells` in console pixels.
20pub fn px_rect(cells: Rect) -> PxRect {
21    let (cw, ch) = CELL_PX;
22    Rect::new(cells.x * cw, cells.y * ch, cells.w * cw, cells.h * ch)
23}
24
25/// How map tiles are laid on the screen: where a tile goes, from where it
26/// is in the view. Every skin and every drawing across tiles asks it,
27/// rather than assume squares in rows (ADR-0003 §6).
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum Projection {
30    /// Seen from above: tiles in rows and columns, each the tile's size
31    /// right of and below the one before.
32    #[default]
33    TopDown,
34}
35
36impl Projection {
37    /// The top-left pixel of the tile `dx` right of and `dy` below the
38    /// view's first, whose top-left pixel is `corner`, on tiles of `tile`
39    /// pixels.
40    pub const fn place(self, corner: (i32, i32), tile: (i32, i32), dx: i32, dy: i32) -> (i32, i32) {
41        match self {
42            Projection::TopDown => (corner.0 + tile.0 * dx, corner.1 + tile.1 * dy),
43        }
44    }
45}
46
47/// The tiles of a scene laid out on the console.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Grid {
50    /// The map tile in the top-left corner.
51    pub origin: Pos,
52    /// The top-left pixel of that tile.
53    pub corner: (i32, i32),
54    /// A tile's size in pixels, across × down.
55    pub tile: (i32, i32),
56    /// Tiles drawn, across × down.
57    pub tiles: (i32, i32),
58    /// Console pixels per pixel of the art (the skin's zoom): 1 unless
59    /// everything on the map is drawn bigger. `tile` already counts it.
60    pub scale: i32,
61    /// How the tiles are laid.
62    pub projection: Projection,
63}
64
65impl Grid {
66    /// `tiles` tiles of `tile` pixels from the map tile `origin`, whose
67    /// top-left pixel is `corner`, laid top-down at the zoom `scale`.
68    pub const fn top_down(
69        origin: Pos,
70        corner: (i32, i32),
71        tile: (i32, i32),
72        tiles: (i32, i32),
73        scale: i32,
74    ) -> Self {
75        Self {
76            origin,
77            corner,
78            tile,
79            tiles,
80            scale,
81            projection: Projection::TopDown,
82        }
83    }
84
85    /// Where `tile` is, as tiles right of and below the origin, if drawn.
86    pub fn offset(&self, tile: Pos) -> Option<(i32, i32)> {
87        let dx = tile.x.checked_sub(self.origin.x)?;
88        let dy = tile.y.checked_sub(self.origin.y)?;
89        let inside = (0..self.tiles.0).contains(&dx) && (0..self.tiles.1).contains(&dy);
90        inside.then_some((dx, dy))
91    }
92
93    /// The top-left pixel of `tile`, drawn or not (it may lie outside the
94    /// grid).
95    pub fn tile_px(&self, tile: Pos) -> (i32, i32) {
96        let (dx, dy) = (tile.x - self.origin.x, tile.y - self.origin.y);
97        self.projection.place(self.corner, self.tile, dx, dy)
98    }
99
100    /// The pixels of the tile `dx` right of and `dy` below the origin.
101    pub fn rect_at(&self, dx: i32, dy: i32) -> PxRect {
102        let (w, h) = self.tile;
103        let (x, y) = self.projection.place(self.corner, self.tile, dx, dy);
104        Rect::new(x, y, w, h)
105    }
106
107    /// The pixels of `tile`, or `None` if it isn't drawn.
108    pub fn rect(&self, tile: Pos) -> Option<PxRect> {
109        let (dx, dy) = self.offset(tile)?;
110        Some(self.rect_at(dx, dy))
111    }
112
113    /// The skin's zoom, at least 1.
114    pub fn zoom(&self) -> i32 {
115        self.scale.max(1)
116    }
117
118    /// A tile at the art's own size, its corner at (0, 0): what a mark's
119    /// place on a tile is worked out on, whatever the zoom
120    /// ([`zoomed`](Self::zoomed)).
121    pub fn art_tile(&self) -> PxRect {
122        let zoom = self.zoom();
123        Rect::new(0, 0, self.tile.0 / zoom, self.tile.1 / zoom)
124    }
125
126    /// The same tiles at the art's own size, laid from (0, 0): what a
127    /// drawing across tiles (the path) is worked out on, whatever the
128    /// zoom, before [`zoomed`](Self::zoomed) from this grid's corner
129    /// places it.
130    #[must_use]
131    pub fn at_art_size(&self) -> Self {
132        let tile = self.art_tile();
133        Self {
134            corner: (0, 0),
135            tile: (tile.w, tile.h),
136            scale: 1,
137            ..*self
138        }
139    }
140
141    /// `art`, pixels worked out on [`art_tile`](Self::art_tile), on the
142    /// tile whose top-left pixel is `corner`: every art pixel a square of
143    /// the zoom's console pixels.
144    pub fn zoomed(&self, corner: (i32, i32), art: PxRect) -> PxRect {
145        let zoom = self.zoom();
146        Rect::new(
147            corner.0 + art.x * zoom,
148            corner.1 + art.y * zoom,
149            art.w * zoom,
150            art.h * zoom,
151        )
152    }
153
154    /// The pixels every drawn tile covers.
155    pub fn bounds(&self) -> PxRect {
156        let (w, h) = self.tile;
157        let (x, y) = self.corner;
158        Rect::new(x, y, w * self.tiles.0, h * self.tiles.1)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn p(x: i32, y: i32) -> Pos {
167        Pos::new(x, y)
168    }
169
170    #[test]
171    fn tiles_are_laid_from_the_corner_in_their_size() {
172        let g = Grid::top_down(p(5, -2), (4, 8), (24, 20), (3, 2), 1);
173        assert_eq!(g.projection, Projection::TopDown);
174        assert_eq!(g.offset(p(5, -2)), Some((0, 0)));
175        assert_eq!(g.offset(p(7, -1)), Some((2, 1)));
176        for out in [
177            p(8, -2),
178            p(4, -2),
179            p(5, 0),
180            p(5, -3),
181            p(i32::MIN, 0),
182            p(0, i32::MIN),
183        ] {
184            assert_eq!(g.offset(out), None, "{out:?}");
185            assert_eq!(g.rect(out), None, "{out:?}");
186        }
187        assert_eq!(g.tile_px(p(5, -2)), (4, 8));
188        assert_eq!(g.tile_px(p(7, -1)), (52, 28));
189        // Outside the grid too.
190        assert_eq!(g.tile_px(p(4, -3)), (-20, -12));
191        assert_eq!(g.rect(p(6, -1)), Some(Rect::new(28, 28, 24, 20)));
192        assert_eq!(g.rect_at(2, 0), Rect::new(52, 8, 24, 20));
193        assert_eq!(g.bounds(), Rect::new(4, 8, 72, 40));
194        // At the art's own size, a mark is where it was worked out.
195        assert_eq!(g.art_tile(), Rect::new(0, 0, 24, 20));
196        let mark = Rect::new(3, -2, 5, 7);
197        assert_eq!(g.zoomed((28, 28), mark), Rect::new(31, 26, 5, 7));
198    }
199
200    #[test]
201    fn a_cell_is_a_text_consoles_and_cells_scale_to_pixels() {
202        assert_eq!(CELL_PX, (8, 16));
203        assert_eq!(px_rect(Rect::new(3, 2, 5, 4)), Rect::new(24, 32, 40, 64));
204        assert_eq!(px_rect(Rect::new(-1, -2, 0, 1)), Rect::new(-8, -32, 0, 16));
205    }
206
207    #[test]
208    fn top_down_tiles_go_in_rows_and_columns() {
209        let top = Projection::default();
210        assert_eq!(top, Projection::TopDown);
211        assert_eq!(top.place((4, 8), (24, 20), 0, 0), (4, 8));
212        assert_eq!(top.place((4, 8), (24, 20), 2, 1), (52, 28));
213        assert_eq!(top.place((4, 8), (24, 20), -1, -3), (-20, -52));
214    }
215
216    #[test]
217    fn a_zoomed_grid_draws_every_art_pixel_as_a_square() {
218        let g = Grid::top_down(p(0, 0), (8, 0), (48, 40), (3, 2), 2);
219        assert_eq!(g.art_tile(), Rect::new(0, 0, 24, 20));
220        // From the tile's corner: twice as far, twice as big.
221        let mark = Rect::new(3, -2, 5, 7);
222        assert_eq!(g.zoomed((56, 40), mark), Rect::new(62, 36, 10, 14));
223        assert_eq!(g.zoomed((56, 40), g.art_tile()), g.rect_at(1, 1));
224        // The same tiles unzoomed, from (0, 0); already so at 1×.
225        let art = Grid {
226            corner: (0, 0),
227            tile: (24, 20),
228            scale: 1,
229            ..g
230        };
231        assert_eq!(g.at_art_size(), art);
232        assert_eq!(art.at_art_size(), art);
233        // A scale below 1 counts as 1.
234        let flat = Grid { scale: 0, ..g };
235        assert_eq!(flat.art_tile(), Rect::new(0, 0, 48, 40));
236        assert_eq!(flat.zoomed((1, 2), mark), Rect::new(4, 0, 5, 7));
237    }
238}