Skip to main content

shuyi_ui/grid/
path.rs

1//! The movement path arrow, as every grid skin draws it (game ADR-0018): a
2//! 3-px line through tile centres, under the glyphs, from the edge of the
3//! unit's tile, ending in a single arrowhead over the destination tile.
4//! Worked out from the skin's tile size: on the glyph skin's 16 px tiles
5//! the line is pixels 7..=9.
6
7use crate::prelude::*;
8use shuyi_core::Pos;
9
10use super::layout::Grid;
11use crate::frame::color::Rgb;
12use crate::frame::glyph_buffer::{Layer, Overlay, PxRect, Rect};
13
14/// The path line's thickness, in pixels.
15pub const LINE_W: i32 = 3;
16
17/// Pixels of the line on each side of its middle one.
18const LINE_HALF: i32 = 1;
19
20/// The arrowhead's columns (or rows), from its base (11 px across) to its
21/// tip (1 px).
22const ARROW_LEN: i32 = 6;
23
24/// The line's middle pixel from the edge of a tile `len` pixels long;
25/// arrowheads are centred on it.
26const fn line_mid(len: i32) -> i32 {
27    len / 2
28}
29
30/// Offset of the line from the edge of a tile `len` pixels long.
31const fn line_offset(len: i32) -> i32 {
32    line_mid(len) - LINE_HALF
33}
34
35/// Offset of the arrowhead's base from the edge behind it, on a tile `len`
36/// pixels long: the arrowhead is centred along the tile.
37const fn arrow_base(len: i32) -> i32 {
38    (len - ARROW_LEN) / 2
39}
40
41/// The line between the centres of adjacent tiles `a` and `b`; with
42/// `from_edge`, only the part outside `a`'s tile.
43fn segment(a: Pos, b: Pos, grid: &Grid, from_edge: bool) -> PxRect {
44    let ((ax, ay), (bx, by)) = (grid.tile_px(a), grid.tile_px(b));
45    let (w, h) = grid.tile;
46    let (ox, oy) = (line_offset(w), line_offset(h));
47    let (mut x0, mut y0) = (ax.min(bx) + ox, ay.min(by) + oy);
48    let (mut x1, mut y1) = (ax.max(bx) + ox + LINE_W, ay.max(by) + oy + LINE_W);
49    if from_edge {
50        match (b.x - a.x, b.y - a.y) {
51            (1, _) => x0 = ax + w,
52            (-1, _) => x1 = ax,
53            (_, 1) => y0 = ay + h,
54            _ => y1 = ay,
55        }
56    }
57    Rect::new(x0, y0, x1 - x0, y1 - y0)
58}
59
60/// The arrowhead on tile `to`, pointing away from the adjacent tile `from`:
61/// [`ARROW_LEN`] stacked 1-px rects, 11 px across at the base down to 1 at
62/// the tip, centred on the line.
63fn arrowhead(from: Pos, to: Pos, grid: &Grid) -> Vec<PxRect> {
64    let (px, py) = grid.tile_px(to);
65    let (w, h) = grid.tile;
66    let (mid_x, mid_y) = (line_mid(w), line_mid(h));
67    (0..ARROW_LEN)
68        .map(|i| {
69            let half = ARROW_LEN - 1 - i;
70            let across = 2 * half + 1;
71            let near = |len| arrow_base(len) + i;
72            let far = |len| len - 1 - arrow_base(len) - i;
73            match (to.x - from.x, to.y - from.y) {
74                (1, _) => Rect::new(px + near(w), py + mid_y - half, 1, across),
75                (-1, _) => Rect::new(px + far(w), py + mid_y - half, 1, across),
76                (_, 1) => Rect::new(px + mid_x - half, py + near(h), across, 1),
77                _ => Rect::new(px + mid_x - half, py + far(h), across, 1),
78            }
79        })
80        .collect()
81}
82
83/// The overlays drawing `path` (the unit's tile first) in `color`: the
84/// line `Under` the glyphs, then the arrowhead `Over` them; nothing for a
85/// path of one tile. Clipped to the tiles of `grid`. On a zoomed grid
86/// the line and the arrowhead are the art-sized tile's, every pixel a
87/// square of the zoom's.
88pub fn path_overlays(path: &[Pos], grid: &Grid, color: Rgb) -> Vec<Overlay> {
89    let view = grid.bounds();
90    let art = grid.at_art_size();
91    let mut out = Vec::new();
92    let mut add = |rect: PxRect, layer| {
93        if let Some(rect) = grid.zoomed(grid.corner, rect).intersect(&view) {
94            out.push(Overlay::new(rect, color, layer));
95        }
96    };
97    for (i, pair) in path.windows(2).enumerate() {
98        add(segment(pair[0], pair[1], &art, i == 0), Layer::Under);
99    }
100    if let [.., from, to] = path {
101        for rect in arrowhead(*from, *to, &art) {
102            add(rect, Layer::Over);
103        }
104    }
105    out
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    const RED: Rgb = Rgb::new(255, 0, 0);
113
114    fn p(x: i32, y: i32) -> Pos {
115        Pos::new(x, y)
116    }
117
118    /// The glyph skin's 16 px tiles in a 70 × 30-cell map area (560 ×
119    /// 480 px): 35 × 30 tiles from the map's corner.
120    fn view() -> Grid {
121        Grid::top_down(p(0, 0), (0, 0), (16, 16), (35, 30), 1)
122    }
123
124    /// The overlays of `path` with the camera at the origin, as
125    /// `(layer, x, y, w, h)`.
126    fn rects(path: &[Pos]) -> Vec<(Layer, i32, i32, i32, i32)> {
127        path_overlays(path, &view(), RED)
128            .iter()
129            .map(|o| (o.layer, o.rect.x, o.rect.y, o.rect.w, o.rect.h))
130            .collect()
131    }
132
133    #[test]
134    fn a_one_tile_path_draws_nothing() {
135        let found = rects(&[p(2, 2)]);
136        assert!(found.is_empty(), "{found:?}");
137    }
138
139    #[test]
140    fn the_line_starts_at_the_unit_tile_edge_and_ends_in_one_arrowhead() {
141        // Right from (1, 1) (pixels 16..32 × 16..32) to (2, 1), then down.
142        let r = rects(&[p(1, 1), p(2, 1), p(2, 2)]);
143        let under: Vec<_> = r.iter().filter(|o| o.0 == Layer::Under).collect();
144        assert_eq!(
145            under,
146            [
147                // From the tile's right edge (x 32) to (2, 1)'s centre band.
148                &(Layer::Under, 32, 23, 10, 3),
149                // (2, 1)'s centre band down to (2, 2)'s.
150                &(Layer::Under, 39, 23, 3, 19),
151            ]
152        );
153        // The arrowhead points down on (2, 2) (pixels 32..48 × 32..48):
154        // rows 37..=42, 11 px wide down to 1, centred on x 40.
155        let over: Vec<_> = r.iter().filter(|o| o.0 == Layer::Over).copied().collect();
156        let expect: Vec<_> = (0..6)
157            .map(|i| (Layer::Over, 40 - (5 - i), 37 + i, 2 * (5 - i) + 1, 1))
158            .collect();
159        assert_eq!(over, expect);
160    }
161
162    #[test]
163    fn arrowheads_point_the_way_of_the_last_step() {
164        let tips = |path: &[Pos]| {
165            let over: Vec<_> = rects(path)
166                .into_iter()
167                .filter(|o| o.0 == Layer::Over)
168                .collect();
169            // The 1-px tip is the last rect.
170            let (_, x, y, w, h) = over[over.len() - 1];
171            assert_eq!((w, h), (1, 1));
172            (x, y)
173        };
174        // Tile (1, 1) spans pixels 16..32 on both axes; its centre is 24.
175        assert_eq!(tips(&[p(0, 1), p(1, 1)]), (16 + 10, 24));
176        assert_eq!(tips(&[p(2, 1), p(1, 1)]), (16 + 5, 24));
177        assert_eq!(tips(&[p(1, 0), p(1, 1)]), (24, 16 + 10));
178        assert_eq!(tips(&[p(1, 2), p(1, 1)]), (24, 16 + 5));
179        // Each line starts at the unit tile's edge facing the move.
180        let first = |path: &[Pos]| rects(path)[0];
181        assert_eq!(first(&[p(1, 1), p(0, 1)]), (Layer::Under, 7, 23, 9, 3));
182        assert_eq!(first(&[p(1, 1), p(1, 0)]), (Layer::Under, 23, 7, 3, 9));
183        assert_eq!(first(&[p(1, 1), p(1, 2)]), (Layer::Under, 23, 32, 3, 10));
184        // Every arrowhead's 11-px base is centred on the line's middle
185        // pixel (24 on tile (1, 1)), across the direction of travel.
186        let base = |path: &[Pos]| {
187            let over: Vec<_> = rects(path)
188                .into_iter()
189                .filter(|o| o.0 == Layer::Over)
190                .collect();
191            over[0]
192        };
193        assert_eq!(base(&[p(0, 1), p(1, 1)]), (Layer::Over, 21, 19, 1, 11));
194        assert_eq!(base(&[p(2, 1), p(1, 1)]), (Layer::Over, 26, 19, 1, 11));
195        assert_eq!(base(&[p(1, 0), p(1, 1)]), (Layer::Over, 19, 21, 11, 1));
196        assert_eq!(base(&[p(1, 2), p(1, 1)]), (Layer::Over, 19, 26, 11, 1));
197        assert_eq!(2 * LINE_HALF + 1, LINE_W);
198        assert_eq!(line_offset(16) + LINE_HALF, line_mid(16));
199        assert_eq!((line_offset(16), line_mid(16), arrow_base(16)), (7, 8, 5));
200    }
201
202    #[test]
203    fn the_geometry_follows_the_tile_size() {
204        // 24 × 20 tiles: tile (1, 1) spans pixels 24..48 × 20..40.
205        let grid = Grid::top_down(p(0, 0), (0, 0), (24, 20), (10, 10), 1);
206        let r: Vec<_> = path_overlays(&[p(1, 1), p(2, 1), p(2, 2)], &grid, RED)
207            .iter()
208            .map(|o| (o.layer, o.rect.x, o.rect.y, o.rect.w, o.rect.h))
209            .collect();
210        // The line: from x 48 (the unit tile's right edge) along the centre
211        // band of (2, 1) (pixels 59..=61 across, 29..=31 down), then down
212        // into (2, 2)'s.
213        assert_eq!(r[0], (Layer::Under, 48, 29, 14, 3));
214        assert_eq!(r[1], (Layer::Under, 59, 29, 3, 23));
215        // The arrowhead points down on (2, 2) (pixels 48..72 × 40..60): its
216        // base 7 px into the tile, centred on x 60; its tip 5 rows on.
217        assert_eq!(r[2], (Layer::Over, 55, 47, 11, 1));
218        assert_eq!(r[7], (Layer::Over, 60, 52, 1, 1));
219        assert_eq!(r.len(), 8);
220        // Pointing left on a 24 px tile: columns 14 down to 9 of it.
221        let left = path_overlays(&[p(3, 1), p(2, 1)], &grid, RED);
222        let xs: Vec<i32> = left
223            .iter()
224            .filter(|o| o.layer == Layer::Over)
225            .map(|o| o.rect.x)
226            .collect();
227        assert_eq!(xs, [62, 61, 60, 59, 58, 57]);
228        assert_eq!((line_offset(24), line_mid(24), arrow_base(24)), (11, 12, 9));
229        assert_eq!(arrow_base(20), 7);
230        // The same tiles at twice the size, from (8, 16): every rectangle
231        // twice as far from the corner and twice as big.
232        let path = [p(1, 1), p(2, 1), p(2, 2)];
233        let zoomed = Grid {
234            corner: (8, 16),
235            tile: (48, 40),
236            scale: 2,
237            ..grid
238        };
239        let twice: Vec<_> = path_overlays(&path, &zoomed, RED)
240            .iter()
241            .map(|o| (o.layer, o.rect.x, o.rect.y, o.rect.w, o.rect.h))
242            .collect();
243        let doubled: Vec<_> = r
244            .iter()
245            .map(|&(layer, x, y, w, h)| (layer, 8 + 2 * x, 16 + 2 * y, 2 * w, 2 * h))
246            .collect();
247        assert_eq!(twice, doubled);
248        assert_eq!(twice[0], (Layer::Under, 104, 74, 28, 6));
249        // Clipped to the zoomed tiles: a path that leaves a two-tile view.
250        let two = Grid {
251            tiles: (2, 1),
252            ..zoomed
253        };
254        let cut = path_overlays(&[p(0, 0), p(1, 0), p(2, 0)], &two, RED);
255        let right = two.bounds().x + two.bounds().w;
256        assert!(cut.iter().all(|o| o.rect.x + o.rect.w <= right), "{cut:?}");
257        assert_eq!(cut.len(), 2);
258    }
259
260    #[test]
261    fn overlays_are_clipped_to_the_map_view() {
262        // The view is 35 tiles wide: tile 35 is past its edge.
263        let r = rects(&[p(33, 0), p(34, 0), p(35, 0)]);
264        let right = view().bounds().w;
265        assert_eq!(right, 560);
266        assert!(r.iter().all(|o| o.1 + o.3 <= right), "{r:?}");
267        assert_eq!(r.len(), 2, "no arrowhead off the view: {r:?}");
268    }
269}