1use shuyi_core::Pos;
8
9use crate::frame::console::ConsoleSpec;
10use crate::frame::glyph_buffer::{PxRect, Rect};
11
12pub 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
19pub 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#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum Projection {
30 #[default]
33 TopDown,
34}
35
36impl Projection {
37 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct Grid {
50 pub origin: Pos,
52 pub corner: (i32, i32),
54 pub tile: (i32, i32),
56 pub tiles: (i32, i32),
58 pub scale: i32,
61 pub projection: Projection,
63}
64
65impl Grid {
66 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 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 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 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 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 pub fn zoom(&self) -> i32 {
115 self.scale.max(1)
116 }
117
118 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 #[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 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 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 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 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 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 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 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}