1use crate::prelude::*;
66use alloc::collections::BTreeMap;
67use core::fmt::Write as _;
68
69use crate::frame::color::{Palette, Rgb};
70use crate::frame::gizmo::{GizmoItem, GizmoShape};
71use crate::frame::glyph_buffer::Cell;
72use crate::frame::glyph_buffer::{Backdrop, GlyphBuffer, Item, Paint, PxRect, Sprite};
73use crate::frame::painted::{PaintedSprite, PaintedTable};
74
75const SEE_THROUGH: &str = "see-through";
77
78const CONTROL_GLYPH: char = '\u{fffd}';
80
81impl GlyphBuffer {
82 pub fn to_snapshot(&self, palette: &Palette) -> String {
84 self.snapshot_with(palette, self.pictures())
85 }
86
87 fn snapshot_with(&self, palette: &Palette, pictures: &PaintedTable) -> String {
90 let (w, h) = (i32::from(self.width()), i32::from(self.height()));
91 let cells = || (0..h).flat_map(move |y| (0..w).filter_map(move |x| self.get(x, y)));
92 let pair = |c: &Cell| (c.fg, (!c.see_through).then_some(c.bg));
94 let mut keys: BTreeMap<(Rgb, Option<Rgb>), char> = BTreeMap::new();
95 let mut legend: Vec<(char, Rgb, Option<Rgb>)> = Vec::new();
96 for c in cells() {
97 let (fg, bg) = pair(c);
98 keys.entry((fg, bg)).or_insert_with(|| {
99 let k = key(legend.len());
100 legend.push((k, fg, bg));
101 k
102 });
103 }
104 let per_row = usize::from(self.width()).max(1);
105 let mut out = String::new();
106 for (i, c) in cells().enumerate() {
107 if c.is_continuation() {
108 } else if c.glyph.is_control() {
110 out.push(CONTROL_GLYPH);
111 } else {
112 out.push(c.glyph);
113 }
114 if (i + 1) % per_row == 0 {
115 out.push('\n');
116 }
117 }
118 out.push_str("--- colours ---\n");
119 for (i, c) in cells().enumerate() {
120 out.push(keys[&pair(c)]);
121 if (i + 1) % per_row == 0 {
122 out.push('\n');
123 }
124 }
125 out.push_str("--- legend ---\n");
126 let name = |rgb: Rgb| {
127 palette
128 .name_of(rgb)
129 .map_or_else(|| rgb.to_hex(), str::to_owned)
130 };
131 for (k, fg, bg) in legend {
132 let bg = bg.map_or_else(|| SEE_THROUGH.to_owned(), name);
133 let _ = writeln!(out, "{k} = fg:{} bg:{bg}", name(fg));
134 }
135 if !self.items().is_empty() {
136 out.push_str("--- overlays ---\n");
137 }
138 for item in self.items() {
139 let layer = item.layer().name();
140 match item {
141 Item::Rect(o) => {
142 let _ = writeln!(out, "{layer:<5} {} {}", px(o.rect), name(o.color));
143 }
144 Item::Sprite(s) => {
145 let _ = writeln!(out, "{layer:<5} {}", sprite_line(s, &name));
146 }
147 Item::Painted(p) => {
148 let _ = writeln!(out, "{layer:<5} {}", painted_line(p, pictures));
149 }
150 Item::Gizmo(g) => {
151 let _ = writeln!(out, "{layer:<5} {}", gizmo_line(g));
152 }
153 }
154 }
155 if let Some(backdrop) = self.backdrop() {
156 let _ = writeln!(out, "--- backdrop: {} ---", backdrop_line(backdrop));
157 out.push_str(&backdrop.scene().snapshot_with(palette, pictures));
158 }
159 out
160 }
161}
162
163fn backdrop_line(backdrop: &Backdrop) -> String {
165 let (x, y) = backdrop.origin_px();
166 format!(
167 "clip {} origin {},{} zoom {}",
168 px(backdrop.clip()),
169 tenths(x),
170 tenths(y),
171 backdrop.zoom()
172 )
173}
174
175fn tenths(v: f32) -> String {
177 let rounded = (v * 10.0).round() / 10.0;
178 format!("{:.1}", rounded + 0.0)
180}
181
182fn px(r: PxRect) -> String {
184 format!("{},{} {}x{}", r.x, r.y, r.w, r.h)
185}
186
187pub(crate) fn gizmo_line(g: &GizmoItem) -> String {
189 let point = |(x, y): (i32, i32)| format!("{x},{y}");
190 let shape = match g.shape {
191 GizmoShape::Rect { rect } => format!("rect {}", px(rect)),
192 GizmoShape::Line { from, to } => format!("line {} {}", point(from), point(to)),
193 GizmoShape::Cross { at, size } => format!("cross {} {size}", point(at)),
194 GizmoShape::Circle { center, radius } => format!("circle {} {radius}", point(center)),
195 GizmoShape::Label { at, text } => format!("label {} {:?}", point(at), text.as_str()),
196 };
197 let mut line = format!("gizmo {} {} {shape}", g.channel, g.role.name());
198 let label = matches!(g.shape, GizmoShape::Label { .. });
199 if !label && g.clip != g.shape.bounds() {
200 let _ = write!(line, " clip={}", px(g.clip));
201 }
202 line
203}
204
205fn sprite_line(s: &Sprite, name: &dyn Fn(Rgb) -> String) -> String {
207 let mut line = format!("sprite {} {} {}", px(s.dest), s.image.path(), px(s.src));
208 if s.flip_x {
209 line.push_str(" flip");
210 }
211 if s.opacity != u8::MAX {
212 let _ = write!(line, " opacity={}", s.opacity);
213 }
214 if s.brightness != u8::MAX {
215 let _ = write!(line, " bright={}", s.brightness);
216 }
217 match s.paint {
218 Paint::Image => {}
219 Paint::Solid(color) => {
220 let _ = write!(line, " solid={}", name(color));
221 }
222 Paint::Dimmed => line.push_str(" dimmed"),
223 Paint::Shaded(light) => {
224 let [r, g, b] = light.tint;
225 let _ = write!(line, " shade={}:{r},{g},{b}", light.grey);
226 }
227 }
228 if s.clip != s.dest {
229 let _ = write!(line, " clip={}", px(s.clip));
230 }
231 line
232}
233
234fn painted_line(p: &PaintedSprite, pictures: &PaintedTable) -> String {
237 let mut line = format!("painted {} {}", px(p.dest), p.picture);
238 let picture = pictures.get(p.picture);
239 if let Some(picture) = picture {
240 let (w, h) = picture.size();
241 let _ = write!(
242 line,
243 " {w}x{h} {} r{} {:016x}",
244 picture.kind().name(),
245 picture.revision(),
246 picture.fingerprint()
247 );
248 } else {
249 line.push_str(" missing");
250 }
251 let whole = picture.map(|picture| {
252 let (w, h) = picture.size();
253 PxRect::new(0, 0, w.into(), h.into())
254 });
255 if whole != Some(p.src) {
256 let _ = write!(line, " src={}", px(p.src));
257 }
258 if p.brightness != u8::MAX {
259 let _ = write!(line, " bright={}", p.brightness);
260 }
261 if p.clip != p.dest {
262 let _ = write!(line, " clip={}", px(p.clip));
263 }
264 line
265}
266
267fn key(n: usize) -> char {
269 const KEYS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
270 if let Some(&k) = KEYS.get(n) {
271 return char::from(k);
272 }
273 u32::try_from(n - KEYS.len())
277 .ok()
278 .and_then(|i| i.checked_add(0x4e00))
279 .filter(|&c| c <= 0x9fff)
280 .and_then(char::from_u32)
281 .unwrap_or('?')
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::frame::color::EngineColor;
288 use crate::frame::color::tests::engine_palette;
289 use crate::frame::glyph_buffer::{Cell, Rect};
290
291 #[test]
292 fn keys_sequence() {
293 assert_eq!(key(0), 'a');
294 assert_eq!(key(25), 'z');
295 assert_eq!(key(26), 'A');
296 assert_eq!(key(51), 'Z');
297 assert_eq!(key(52), '0');
298 assert_eq!(key(61), '9');
299 assert_eq!(key(62), '\u{4e00}');
300 assert_eq!(key(63), '\u{4e01}');
301 assert_eq!(key(62 + 0x9fff - 0x4e00), '\u{9fff}');
302 assert_eq!(key(62 + 0x9fff - 0x4e00 + 1), '?');
303 assert_eq!(key(usize::MAX), '?');
304 }
305
306 #[test]
307 fn exact_format_small() {
308 let p = engine_palette();
309 let text = p.get(EngineColor::Text);
310 let bg = p.get(EngineColor::Panel);
311 let odd = Rgb::new(1, 2, 3);
312 let mut b = GlyphBuffer::new(3, 2, Cell::new(' ', text, bg));
313 b.print(0, 0, "hi", odd, bg);
314 b.set(2, 1, Cell::new('\n', text, odd));
315 assert_eq!(
316 b.to_snapshot(&p),
317 "hi \n \u{fffd}\n\
318 --- colours ---\n\
319 aab\nbbc\n\
320 --- legend ---\n\
321 a = fg:#010203 bg:panel\n\
322 b = fg:text bg:panel\n\
323 c = fg:text bg:#010203\n"
324 );
325 }
326
327 #[test]
328 fn a_continuation_prints_as_nothing() {
329 let p = engine_palette();
330 let text = p.get(EngineColor::Text);
331 let widths = shuyi_assets::font::CellWidths::wide(['あ']);
332 let mut b = GlyphBuffer::new(4, 1, Cell::new(' ', text, text)).with_widths(widths);
333 b.print(0, 0, "あA", text, text);
334 assert_eq!(
335 b.to_snapshot(&p),
336 "あA \n--- colours ---\naaaa\n--- legend ---\na = fg:text bg:text\n"
337 );
338 }
339
340 #[test]
341 fn overlays_are_listed_after_the_legend() {
342 use crate::frame::glyph_buffer::{Layer, Overlay};
343 let p = engine_palette();
344 let text = p.get(EngineColor::Text);
345 let mut b = GlyphBuffer::new(3, 1, Cell::new(' ', text, text));
346 b.add_overlay(Overlay::new(
347 Rect::new(18, 14, 5, 2),
348 p.get(EngineColor::Warning),
349 Layer::Over,
350 ));
351 b.add_overlay(Overlay::new(
352 Rect::new(0, 7, 24, 3),
353 Rgb::new(1, 2, 3),
354 Layer::Under,
355 ));
356 assert_eq!(
357 b.to_snapshot(&p),
358 " \n\
359 --- colours ---\n\
360 aaa\n\
361 --- legend ---\n\
362 a = fg:text bg:text\n\
363 --- overlays ---\n\
364 over 18,14 5x2 warning\n\
365 under 0,7 24x3 #010203\n"
366 );
367 }
368
369 #[test]
370 fn sprites_are_listed_with_the_rectangles_in_drawing_order() {
371 use crate::frame::glyph_buffer::{Layer, Overlay};
372 let p = engine_palette();
373 let text = p.get(EngineColor::Text);
374 let images = shuyi_assets::image::ImageTable::new(
375 [(
376 "images/test_card.png",
377 shuyi_assets::image::ImageInfo::new(16, 16),
378 )]
379 .into(),
380 );
381 let card = images.id("images/test_card.png").unwrap();
382 let src = Rect::new(0, 0, 16, 16);
383 let mut b = GlyphBuffer::new(20, 6, Cell::new(' ', text, text));
384 let plain = Sprite::new(card, src, Rect::new(8, 8, 80, 80), Layer::Over);
385 b.add_sprite(plain);
386 b.add_overlay(Overlay::new(Rect::new(0, 7, 24, 3), text, Layer::Under));
387 b.add_sprite(Sprite {
388 flip_x: true,
389 ..Sprite::new(
390 card,
391 Rect::new(8, 0, 8, 4),
392 Rect::new(0, 0, 16, 8),
393 Layer::Under,
394 )
395 });
396 b.add_sprite(Sprite {
397 opacity: 128,
398 ..plain
399 });
400 b.add_sprite(Sprite {
401 clip: Rect::new(8, 8, 40, 80),
402 ..plain
403 });
404 b.add_sprite(Sprite {
405 flip_x: true,
406 opacity: 0,
407 clip: Rect::new(48, 40, 80, 80),
408 ..plain
409 });
410 b.add_sprite(plain.painted(Paint::Solid(p.get(EngineColor::Warning))));
411 b.add_sprite(plain.painted(Paint::Solid(Rgb::new(1, 2, 3))));
412 b.add_sprite(Sprite {
413 opacity: 128,
414 clip: Rect::new(8, 8, 40, 80),
415 ..plain.painted(Paint::Dimmed)
416 });
417 let dusk = shuyi_assets::lighting::Light::new(0.35, [0.62, 0.56, 0.46]);
418 b.add_sprite(plain.painted(Paint::Shaded(dusk)));
419 b.add_sprite(Sprite {
420 brightness: 102,
421 ..plain
422 });
423 assert_eq!(
424 b.to_snapshot(&p)
425 .split_once("--- overlays ---\n")
426 .unwrap()
427 .1,
428 "over sprite 8,8 80x80 images/test_card.png 0,0 16x16\n\
429 under 0,7 24x3 text\n\
430 under sprite 0,0 16x8 images/test_card.png 8,0 8x4 flip\n\
431 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 opacity=128\n\
432 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 clip=8,8 40x80\n\
433 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 flip opacity=0 clip=48,40 40x48\n\
434 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 solid=warning\n\
435 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 solid=#010203\n\
436 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 opacity=128 dimmed clip=8,8 40x80\n\
437 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 shade=350:620,560,460\n\
438 over sprite 8,8 80x80 images/test_card.png 0,0 16x16 bright=102\n"
439 );
440 }
441
442 #[test]
443 fn see_through_cells_get_their_own_key() {
444 let p = engine_palette();
445 let text = p.get(EngineColor::Text);
446 let black = p.get(EngineColor::Panel);
447 let mut b = GlyphBuffer::new(4, 1, Cell::new(' ', text, black));
448 b.set(1, 0, Cell::see_through(' ', text));
449 b.set(2, 0, Cell::see_through('x', text));
450 let mut dimmed = Cell::see_through(' ', text);
452 dimmed.bg = text;
453 b.set(3, 0, dimmed);
454 assert_eq!(
455 b.to_snapshot(&p),
456 " x \n\
457 --- colours ---\n\
458 abbb\n\
459 --- legend ---\n\
460 a = fg:text bg:panel\n\
461 b = fg:text bg:see-through\n"
462 );
463 }
464
465 #[test]
466 fn a_backdrop_is_a_header_line_and_the_scenes_own_snapshot() {
467 use alloc::rc::Rc;
468 let p = engine_palette();
469 let text = p.get(EngineColor::Text);
470 let black = p.get(EngineColor::Panel);
471 let mut scene = GlyphBuffer::new(2, 1, Cell::new('s', text, black));
472 scene.add_overlay(crate::frame::glyph_buffer::Overlay::new(
473 Rect::new(0, 0, 3, 2),
474 text,
475 crate::frame::glyph_buffer::Layer::Over,
476 ));
477 let scene = Rc::new(scene);
478 let mut b = GlyphBuffer::new(3, 2, Cell::new(' ', text, black));
479 b.set_backdrop(Rc::clone(&scene), Rect::new(1, 0, 5, 2), (12.34, -0.04), 3);
480 let snapshot = b.to_snapshot(&p);
481 let plain = GlyphBuffer::new(3, 2, Cell::new(' ', text, black)).to_snapshot(&p);
482 assert_eq!(
483 snapshot,
484 format!(
485 "{plain}--- backdrop: clip 1,0 2x2 origin 12.3,0.0 zoom 3 ---\n{}",
486 scene.to_snapshot(&p)
487 )
488 );
489 assert!(snapshot.ends_with("--- overlays ---\nover 0,0 3x2 text\n"));
490 assert!(!plain.contains("backdrop"));
491 }
492
493 #[test]
494 fn tenths_round_and_never_print_a_negative_zero() {
495 assert_eq!(tenths(0.0), "0.0");
496 assert_eq!(tenths(-0.0), "0.0");
497 assert_eq!(tenths(-0.04), "0.0");
498 assert_eq!(tenths(-0.06), "-0.1");
499 assert_eq!(tenths(12.34), "12.3");
500 assert_eq!(tenths(12.36), "12.4");
501 assert_eq!(tenths(-80.0), "-80.0");
502 }
503
504 #[test]
505 fn empty_buffers() {
506 let p = engine_palette();
507 let blank = Cell::new(' ', Rgb::new(0, 0, 0), Rgb::new(0, 0, 0));
508 let expected = "--- colours ---\n--- legend ---\n";
509 assert_eq!(GlyphBuffer::new(0, 0, blank).to_snapshot(&p), expected);
510 assert_eq!(GlyphBuffer::new(0, 3, blank).to_snapshot(&p), expected);
511 assert_eq!(GlyphBuffer::new(3, 0, blank).to_snapshot(&p), expected);
512 }
513
514 #[test]
515 fn many_pairs_get_distinct_keys() {
516 let p = engine_palette();
517 let mut b = GlyphBuffer::new(70, 1, Cell::new(' ', Rgb::new(0, 0, 0), Rgb::new(0, 0, 0)));
518 for x in 0..70u8 {
519 b.set(
520 i32::from(x),
521 0,
522 Cell::new('.', Rgb::new(x, 0, 7), Rgb::new(0, 0, 7)),
523 );
524 }
525 let snap = b.to_snapshot(&p);
526 let keys_row = snap.lines().nth(2).unwrap();
527 let keys: alloc::collections::BTreeSet<char> = keys_row.chars().collect();
528 assert_eq!(keys.len(), 70);
529 assert!(keys_row.starts_with("abc"));
530 assert_eq!(snap.lines().count(), 1 + 1 + 1 + 1 + 70);
531 }
532}