Skip to main content

shuyi_ui/grid/
picture.rs

1//! A whole grid map painted once into a buffer just big enough, to pan
2//! over (ticket 1154): a cinematic's backdrop ([`Pan`](crate::cinematic::Pan))
3//! or the scene camera's ([`debug::camera`](crate::debug::camera)).
4
5use alloc::rc::Rc;
6
7use super::scene::MapScene;
8use super::skin::{GridSkin, Looks, UnitPainter};
9use crate::frame::console::ConsoleSpec;
10use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Rect};
11
12/// An area surely big enough for one tile of any skin, to ask its size.
13pub const ROOMY: Rect = Rect::new(0, 0, 64, 64);
14
15/// A whole map as a skin painted it, to pan over.
16#[derive(Debug, Clone, PartialEq)]
17pub struct MapPicture {
18    /// The picture: just big enough for the map.
19    pub scene: Rc<GlyphBuffer>,
20    /// One tile's size in the picture, in pixels.
21    pub tile_px: (i32, i32),
22}
23
24impl MapPicture {
25    /// All of `scene` (a view of a whole map, from its first tile) painted
26    /// by `skin`, its units by `units`, into a buffer of `console`'s cells
27    /// just big enough, cleared to `blank` first. `None` if the skin can't
28    /// place the scene's first tile (a scene of no tiles).
29    pub fn paint<U>(
30        looks: &Looks,
31        skin: &dyn GridSkin<U>,
32        units: &dyn UnitPainter<U>,
33        scene: &MapScene<U>,
34        console: ConsoleSpec,
35        blank: Cell,
36    ) -> Option<Self> {
37        let tile = skin.tile_px(scene, ROOMY, scene.origin)?;
38        let paint = |buf: &mut GlyphBuffer| skin.paint(looks, scene, buf.bounds(), units, buf);
39        Some(Self::painted(
40            (tile.w, tile.h),
41            scene.size,
42            console,
43            blank,
44            paint,
45        ))
46    }
47
48    /// A picture of `tiles` tiles (across × down) of `tile_px` pixels each,
49    /// painted by `paint` into a buffer of `console`'s cells just big
50    /// enough (rounded up to whole cells), cleared to `blank` first: for a
51    /// painter that isn't a [`GridSkin`].
52    pub fn painted(
53        tile_px: (i32, i32),
54        tiles: (i32, i32),
55        console: ConsoleSpec,
56        blank: Cell,
57        paint: impl FnOnce(&mut GlyphBuffer),
58    ) -> Self {
59        let cells = |tiles: i32, tile_px: i32, cell_px: u16| {
60            let cell_px = i32::from(cell_px);
61            u16::try_from((tiles * tile_px + cell_px - 1) / cell_px).unwrap_or(0)
62        };
63        let (w, h) = (
64            cells(tiles.0, tile_px.0, console.cell_w),
65            cells(tiles.1, tile_px.1, console.cell_h),
66        );
67        let mut buf = GlyphBuffer::with_spec(console.sized(w, h), blank);
68        paint(&mut buf);
69        Self {
70            scene: Rc::new(buf),
71            tile_px,
72        }
73    }
74
75    /// The picture's size in its own pixels.
76    pub fn size_px(&self) -> (u32, u32) {
77        pixel_size(&self.scene)
78    }
79
80    /// The picture's pixel at the map point `tiles` (a whole number is the
81    /// middle of that tile).
82    #[allow(clippy::cast_precision_loss)] // a tile is a few pixels
83    pub fn point_px(&self, tiles: (f32, f32)) -> (f32, f32) {
84        (
85            (tiles.0 + 0.5) * self.tile_px.0 as f32,
86            (tiles.1 + 0.5) * self.tile_px.1 as f32,
87        )
88    }
89}
90
91/// `buf`'s size in its own pixels.
92pub fn pixel_size(buf: &GlyphBuffer) -> (u32, u32) {
93    let px = buf.pixel_bounds();
94    (
95        u32::try_from(px.w).unwrap_or(0),
96        u32::try_from(px.h).unwrap_or(0),
97    )
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::frame::color::{EngineColor, Rgb};
104    use crate::grid::NoUnits;
105    use crate::toy::grid::{Cards, Field, GLYPH, Kit, Letters, Piece, sprite, tiles};
106    use shuyi_core::Pos;
107
108    const CONSOLE: ConsoleSpec = ConsoleSpec::text(40, 20);
109
110    fn blank(kit: &Kit) -> Cell {
111        let c = |e| kit.palette.engine(e);
112        Cell::new(' ', c(EngineColor::Text), c(EngineColor::Background))
113    }
114
115    /// The toy's whole field, painted by `skin` and `units`.
116    fn picture(
117        kit: &Kit,
118        skin: &dyn GridSkin<Piece>,
119        units: &dyn UnitPainter<Piece>,
120        scene: &MapScene<Piece>,
121    ) -> Option<MapPicture> {
122        MapPicture::paint(&kit.looks(), skin, units, scene, CONSOLE, blank(kit))
123    }
124
125    #[test]
126    fn a_glyph_picture_is_the_whole_field_with_its_pieces() {
127        let kit = Kit::new();
128        let scene = Field::glyphs().whole();
129        // 10 × 8 tiles of two cells by one, 16 px each way.
130        let shown = picture(&kit, &GLYPH, &Letters, &scene).unwrap();
131        assert_eq!((shown.scene.width(), shown.scene.height()), (20, 8));
132        assert_eq!(shown.tile_px, (16, 16));
133        assert_eq!(shown.size_px(), (160, 128));
134        // A whole number is a tile's middle.
135        assert_eq!(shown.point_px((0.0, 0.0)), (8.0, 8.0));
136        assert_eq!(shown.point_px((5.75, 3.5)), (100.0, 64.0));
137        assert_eq!(shown.point_px((-0.5, 7.5)), (0.0, 128.0));
138        // The pieces are on it: your piece's tile isn't bare grass.
139        let bare = picture(&kit, &GLYPH, &NoUnits, &scene).unwrap();
140        assert_eq!(shown.scene.get(4, 3).map(|c| c.glyph), Some('y'));
141        assert_ne!(shown.scene.get(4, 3), bare.scene.get(4, 3));
142        assert_eq!(shown.scene.get(0, 0), bare.scene.get(0, 0));
143        assert_ne!(shown, bare);
144        assert!(shown.scene.backdrop().is_none());
145        let found = shown.scene.sprites();
146        assert!(found.is_empty(), "{found:?}");
147    }
148
149    #[test]
150    fn a_sprite_picture_is_in_whole_cells_of_the_tiles_pixels() {
151        let kit = Kit::new();
152        let skin = sprite(Some(tiles(kit.tiles_image())));
153        let cards = Cards { card: kit.card() };
154        let scene = Field::glyphs().whole();
155        // The toy's tiles are 24 px: 240 × 192, in whole cells.
156        let shown = picture(&kit, &skin, &cards, &scene).unwrap();
157        assert_eq!((shown.scene.width(), shown.scene.height()), (30, 12));
158        assert_eq!(shown.tile_px, (24, 24));
159        assert_eq!(shown.size_px(), (240, 192));
160        assert_eq!(shown.point_px((1.0, 2.0)), (36.0, 60.0));
161        // The ground's tiles and the two cards.
162        assert_eq!(shown.scene.sprites().len(), 10 * 8 + 2);
163        // A size that isn't whole cells is rounded up to them.
164        let odd = MapScene::<Piece>::new(Pos::new(0, 0), (1, 1));
165        let shown = picture(&kit, &skin, &cards, &odd).unwrap();
166        assert_eq!((shown.scene.width(), shown.scene.height()), (3, 2));
167        assert_eq!(shown.size_px(), (24, 32));
168        // Nothing to paint, in either skin.
169        let none = MapScene::<Piece>::new(Pos::new(0, 0), (0, 0));
170        assert!(picture(&kit, &skin, &cards, &none).is_none());
171        assert!(picture(&kit, &GLYPH, &Letters, &none).is_none());
172    }
173
174    #[test]
175    fn a_picture_painted_by_hand_is_cleared_first_and_sized_in_cells() {
176        let (ink, paper) = (Rgb::new(1, 2, 3), Rgb::new(4, 5, 6));
177        let blank = Cell::new('.', ink, paper);
178        let mut seen = None;
179        let shown = MapPicture::painted((24, 24), (3, 2), CONSOLE, blank, |buf| {
180            seen = buf.get(8, 2).copied();
181            buf.set(0, 0, Cell::new('#', paper, ink));
182        });
183        assert_eq!(seen, Some(blank));
184        // 72 × 48 px: 9 cells by 3.
185        assert_eq!((shown.scene.width(), shown.scene.height()), (9, 3));
186        assert_eq!(shown.scene.get(0, 0), Some(&Cell::new('#', paper, ink)));
187        assert_eq!(pixel_size(&shown.scene), (72, 48));
188    }
189}