1use crate::debug::gizmo::{Channel, Gizmo, GizmoRole, Gizmos};
15use crate::frame::glyph_buffer::{PxRect, Rect};
16use crate::prelude::*;
17
18use super::layout::Grid;
19use super::scene::{MapScene, MapUnit};
20
21pub const GRID: Channel = Channel::new("grid", "Grid layout");
23
24pub fn layout_gizmos<U: MapUnit>(scene: &MapScene<U>, grid: &Grid, out: &mut Gizmos) {
41 if !out.channel_on(GRID) {
42 return;
43 }
44 let Some(view) = grid.bounds().intersect(&out.frame()) else {
45 return;
46 };
47 let (left, top) = (view.x, view.y);
48 let (right, bottom) = (view.x + view.w - 1, view.y + view.h - 1);
49 let mut push = |role, gizmo| out.push(GRID, role, gizmo);
50 let (across, down) = (grid.tiles.0.max(0), grid.tiles.1.max(0));
51
52 let line = |from, to| Gizmo::Line { from, to };
53 let columns = (0..across).map(|dx| grid.rect_at(dx, 0).x);
54 for x in columns.filter(|&x| x >= left && x < right).chain([right]) {
55 push(GizmoRole::Bounds, line((x, top), (x, bottom)));
56 }
57 let rows = (0..down).map(|dy| grid.rect_at(0, dy).y);
58 for y in rows.filter(|&y| y >= top && y < bottom).chain([bottom]) {
59 push(GizmoRole::Bounds, line((left, y), (right, y)));
60 }
61
62 let on_map = (0..down)
63 .flat_map(|dy| (0..across).map(move |dx| (dx, dy)))
64 .filter(|&(dx, dy)| scene.tile_at(dx, dy).is_some_and(|t| t.terrain.is_some()))
65 .map(|(dx, dy)| grid.rect_at(dx, dy))
66 .reduce(union);
67 if let Some(rect) = on_map.and_then(|map| map.intersect(&view)) {
68 push(GizmoRole::Bounds, Gizmo::Rect { rect });
69 }
70
71 let cell = |pos| grid.rect(pos).and_then(|r| r.intersect(&view));
72 if let Some(rect) = scene.cursor_tile().and_then(cell) {
73 push(GizmoRole::Note, Gizmo::Rect { rect });
74 }
75 for (i, unit) in scene.units.iter().enumerate() {
76 let Some(rect) = cell(unit.pos()) else {
77 continue;
78 };
79 push(GizmoRole::Note, Gizmo::Rect { rect });
80 let at = (rect.x + rect.w - 1, rect.y);
81 push(
82 GizmoRole::Note,
83 Gizmo::Label {
84 at,
85 text: i.to_string(),
86 },
87 );
88 }
89
90 let more = scene.more;
91 let edges = [
92 (more.left(), (left, top), (left, bottom)),
93 (more.right(), (right, top), (right, bottom)),
94 (more.up(), (left, top), (right, top)),
95 (more.down(), (left, bottom), (right, bottom)),
96 ];
97 for (_, from, to) in edges.into_iter().filter(|e| e.0) {
98 push(GizmoRole::Note, line(from, to));
99 }
100}
101
102fn union(a: PxRect, b: PxRect) -> PxRect {
104 let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
105 let (x1, y1) = ((a.x + a.w).max(b.x + b.w), (a.y + a.h).max(b.y + b.h));
106 Rect::new(x0, y0, x1 - x0, y1 - y0)
107}
108
109#[cfg(test)]
110mod tests {
111 use proptest::prelude::*;
112 use shuyi_core::Pos;
113
114 use super::*;
115 use crate::debug::gizmo::Drawn;
116 use crate::frame::console::ConsoleSpec;
117 use crate::grid::camera::Camera;
118 use crate::grid::scene::{CursorStyle, CursorView, MapEdges};
119 use crate::grid::skin::GridSkin;
120 use crate::toy::grid::{GLYPH, Piece};
121
122 const CONSOLE: ConsoleSpec = ConsoleSpec::text(40, 20);
124
125 fn scene(
129 map: (u16, u16),
130 area: Rect,
131 origin: Pos,
132 units: &[Pos],
133 cursor: Pos,
134 ) -> MapScene<Piece> {
135 let view = GridSkin::<Piece>::view_tiles(&GLYPH, area);
136 let half = Pos::new(origin.x + view.0 / 2, origin.y + view.1 / 2);
137 let camera = Camera::centred_on(half, map.0, map.1, view);
138 let mut scene = MapScene::new(camera.origin, view);
139 let (w, h) = (i32::from(map.0), i32::from(map.1));
140 scene.set_terrain(|p| {
141 ((0..w).contains(&p.x) && (0..h).contains(&p.y)).then_some(shuyi_core::TerrainId(0))
142 });
143 scene.more = MapEdges::of_view(camera.origin, view, (w, h));
144 for (i, &pos) in (0..).zip(units) {
145 scene.push_unit(Piece::new(i, 'u', pos));
146 }
147 scene.cursor = Some(CursorView {
148 pos: cursor,
149 brightness: 1.0,
150 style: CursorStyle::Corners,
151 });
152 scene
153 }
154
155 fn gizmos(scene: &MapScene<Piece>, area: Rect) -> Vec<Drawn> {
157 let grid = GridSkin::<Piece>::grid(&GLYPH, scene, area);
158 let frame = frame();
159 let mut out = Gizmos::with_on(["grid"]).within(frame);
160 layout_gizmos(scene, &grid, &mut out);
161 assert_eq!(out.offered(), [GRID]);
162 out.drawn().to_vec()
163 }
164
165 fn edges(drawn: &[Drawn]) -> Vec<&Gizmo> {
167 drawn
168 .iter()
169 .filter(|d| d.role == GizmoRole::Note && matches!(d.gizmo, Gizmo::Line { .. }))
170 .map(|d| &d.gizmo)
171 .collect()
172 }
173
174 const AREA: Rect = Rect::new(0, 0, 40, 20);
175
176 fn frame() -> Rect {
178 let (w, h) = CONSOLE.px();
179 Rect::new(0, 0, w, h)
180 }
181
182 #[test]
183 fn nothing_is_built_while_the_channel_is_off() {
184 let s = scene((10, 8), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
185 let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
186 let mut off = Gizmos::new();
187 layout_gizmos(&s, &grid, &mut off);
188 assert!(off.is_empty());
189 assert_eq!(off.offered(), [GRID]);
190 }
191
192 #[test]
193 fn a_map_that_fits_the_view_has_no_edges_and_its_bounds_inside_it() {
194 let s = scene(
196 (10, 8),
197 AREA,
198 Pos::new(2, 3),
199 &[Pos::new(2, 3), Pos::new(3, 4)],
200 Pos::new(2, 3),
201 );
202 assert_eq!(s.more, MapEdges::NONE);
203 let drawn = gizmos(&s, AREA);
204 assert_eq!(edges(&drawn), [] as [&Gizmo; 0]);
205 let roles: Vec<_> = drawn.iter().map(|d| d.role).collect();
206 assert_eq!(roles.len(), 21 + 21 + 1 + 1 + 4);
209 assert!(roles[..43].iter().all(|&r| r == GizmoRole::Bounds));
210 assert!(roles[43..].iter().all(|&r| r == GizmoRole::Note));
211 let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
213 let map = Rect::new(
214 grid.tile_px(Pos::new(0, 0)).0,
215 grid.tile_px(Pos::new(0, 0)).1,
216 160,
217 128,
218 );
219 assert_eq!(drawn[42].gizmo, Gizmo::Rect { rect: map });
220 let cursor = grid.rect(Pos::new(2, 3)).unwrap();
221 assert_eq!(drawn[43].gizmo, Gizmo::Rect { rect: cursor });
222 assert_eq!(drawn[44].gizmo, Gizmo::Rect { rect: cursor });
223 let label = Gizmo::Label {
224 at: (cursor.x + 15, cursor.y),
225 text: "0".to_owned(),
226 };
227 assert_eq!(drawn[45].gizmo, label);
228 assert!(matches!(&drawn[47].gizmo, Gizmo::Label { text, .. } if text == "1"));
229 }
230
231 #[test]
232 fn a_frame_that_cuts_the_view_on_a_cells_edge_draws_that_line_once() {
233 let s = scene((10, 8), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
234 let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
235 let mut out = Gizmos::with_on(["grid"]).within(Rect::new(0, 0, 33, 49));
238 layout_gizmos(&s, &grid, &mut out);
239 let lines = |across: bool| {
240 out.drawn()
241 .iter()
242 .filter_map(|d| match d.gizmo {
243 Gizmo::Line { from, to }
244 if d.role == GizmoRole::Bounds && (from.1 == to.1) == across =>
245 {
246 Some(if across { from.1 } else { from.0 })
247 }
248 _ => None,
249 })
250 .collect::<Vec<_>>()
251 };
252 assert_eq!(lines(false), [0, 16, 32]);
253 assert_eq!(lines(true), [0, 16, 32, 48]);
254 }
255
256 #[test]
257 fn the_union_holds_both_rectangles_whichever_comes_first() {
258 let (a, b) = (Rect::new(10, 20, 5, 6), Rect::new(-3, 2, 4, 30));
259 let both = Rect::new(-3, 2, 18, 30);
260 assert_eq!(union(a, b), both);
261 assert_eq!(union(b, a), both);
262 assert_eq!(union(a, a), a);
263 }
264
265 #[test]
266 fn edges_are_drawn_only_on_the_sides_the_map_goes_on_past() {
267 let s = scene((60, 50), AREA, Pos::new(0, 0), &[], Pos::new(0, 0));
269 assert_eq!(s.more.names(), ["right", "down"]);
270 let grid = GridSkin::<Piece>::grid(&GLYPH, &s, AREA);
271 let view = grid.bounds();
272 let (r, b) = (view.x + view.w - 1, view.y + view.h - 1);
273 let drawn = gizmos(&s, AREA);
274 let right = Gizmo::Line {
275 from: (r, view.y),
276 to: (r, b),
277 };
278 let down = Gizmo::Line {
279 from: (view.x, b),
280 to: (r, b),
281 };
282 assert_eq!(edges(&drawn), [&right, &down]);
283 let middle = scene((60, 50), AREA, Pos::new(30, 25), &[], Pos::new(30, 25));
285 assert_eq!(middle.more.names(), ["left", "right", "up", "down"]);
286 assert_eq!(edges(&gizmos(&middle, AREA)).len(), 4);
287 }
288
289 fn points(gizmo: &Gizmo) -> Vec<(i32, i32)> {
291 match gizmo {
292 Gizmo::Rect { rect } => {
293 vec![(rect.x, rect.y), (rect.x + rect.w - 1, rect.y + rect.h - 1)]
294 }
295 Gizmo::Line { from, to } => vec![*from, *to],
296 Gizmo::Label { at, .. } | Gizmo::Cross { at, .. } => vec![*at],
297 Gizmo::Circle { center, .. } => vec![*center],
298 }
299 }
300
301 proptest! {
302 #[test]
303 fn no_gizmo_leaves_the_frame(
304 map in (1_u16..80, 1_u16..80),
305 area in (-6..44i32, -4..24i32, 0..60i32, 0..30i32),
306 origin in (-20..90i32, -20..90i32),
307 units in prop::collection::vec((-5..85i32, -5..85i32), 0..5),
308 cursor in (-5..85i32, -5..85i32),
309 ) {
310 let area = Rect::new(area.0, area.1, area.2, area.3);
311 let units: Vec<_> = units.into_iter().map(|(x, y)| Pos::new(x, y)).collect();
312 let s = scene(map, area, Pos::new(origin.0, origin.1), &units, Pos::new(cursor.0, cursor.1));
313 let frame = frame();
314 for drawn in gizmos(&s, area) {
315 if let Gizmo::Rect { rect } = drawn.gizmo {
316 prop_assert!(!rect.is_empty());
317 }
318 for (x, y) in points(&drawn.gizmo) {
319 prop_assert!(frame.contains(x, y), "{:?} outside {frame:?}", drawn.gizmo);
320 }
321 }
322 }
323 }
324}