1use crate::canvas::{AxisTransform, Canvas, CanvasCore, Scale, Transform2D};
2use crate::color::CanvasColor;
3
4const X_PIXELS_PER_CHAR: usize = 1;
5const Y_PIXELS_PER_CHAR: usize = 2;
6const DOT_BITS: [[u8; Y_PIXELS_PER_CHAR]; X_PIXELS_PER_CHAR] = [[0b10, 0b01]];
7const DOT_DECODE: [char; 4] = [' ', '.', '\'', ':'];
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct DotCanvas {
12 core: CanvasCore<u8>,
13}
14
15impl DotCanvas {
16 #[must_use]
24 pub fn new(
25 char_width: usize,
26 char_height: usize,
27 origin_x: f64,
28 origin_y: f64,
29 plot_width: f64,
30 plot_height: f64,
31 ) -> Self {
32 let pixel_width = char_width.saturating_mul(X_PIXELS_PER_CHAR);
33 let pixel_height = char_height.saturating_mul(Y_PIXELS_PER_CHAR);
34 let x = AxisTransform::new(origin_x, plot_width, pixel_width, Scale::Identity, false)
35 .expect("DotCanvas::new requires finite x-origin, positive span, and non-zero width");
36 let y = AxisTransform::new(origin_y, plot_height, pixel_height, Scale::Identity, true)
37 .expect("DotCanvas::new requires finite y-origin, positive span, and non-zero height");
38
39 Self {
40 core: CanvasCore::new(
41 char_width,
42 char_height,
43 pixel_width,
44 pixel_height,
45 Transform2D::new(x, y),
46 ),
47 }
48 }
49
50 #[must_use]
51 #[cfg(test)]
52 pub(crate) fn print_row(&self, row: usize, color: bool) -> String {
53 if row >= self.char_height() {
54 return String::new();
55 }
56
57 let mut out = String::new();
58 for col in 0..self.char_width() {
59 super::write_colored_cell(
60 &mut out,
61 self.glyph_at(col, row),
62 self.color_at(col, row),
63 color,
64 );
65 }
66
67 out
68 }
69
70 #[must_use]
71 #[cfg(test)]
72 pub(crate) fn print(&self, color: bool) -> String {
73 let mut out = String::new();
74 for row in 0..self.char_height() {
75 for col in 0..self.char_width() {
76 super::write_colored_cell(
77 &mut out,
78 self.glyph_at(col, row),
79 self.color_at(col, row),
80 color,
81 );
82 }
83 if row + 1 < self.char_height() {
84 out.push('\n');
85 }
86 }
87
88 out
89 }
90
91 fn cell_index(&self, col: usize, row: usize) -> Option<usize> {
92 if col >= self.core.char_width || row >= self.core.char_height {
93 return None;
94 }
95
96 Some(row * self.core.char_width + col)
97 }
98}
99
100impl Canvas for DotCanvas {
101 fn pixel(&mut self, x: usize, y: usize, color: CanvasColor) {
102 if x > self.pixel_width() || y > self.pixel_height() {
103 return;
104 }
105
106 let clamped_x = if x < self.pixel_width() {
107 x
108 } else {
109 x.saturating_sub(1)
110 };
111 let clamped_y = if y < self.pixel_height() {
112 y
113 } else {
114 y.saturating_sub(1)
115 };
116
117 let col = clamped_x / X_PIXELS_PER_CHAR;
118 let row = clamped_y / Y_PIXELS_PER_CHAR;
119 let x_off = 0;
120 let y_off = clamped_y % Y_PIXELS_PER_CHAR;
121
122 let Some(index) = self.cell_index(col, row) else {
123 return;
124 };
125
126 self.core.grid[index] |= DOT_BITS[x_off][y_off];
127 self.core.colors[index] |= color.as_u8();
128 }
129
130 fn glyph_at(&self, col: usize, row: usize) -> char {
131 self.cell_index(col, row)
132 .and_then(|index| DOT_DECODE.get(usize::from(self.core.grid[index])).copied())
133 .unwrap_or(' ')
134 }
135
136 fn color_at(&self, col: usize, row: usize) -> CanvasColor {
137 self.cell_index(col, row)
138 .and_then(|index| CanvasColor::new(self.core.colors[index]))
139 .unwrap_or(CanvasColor::NORMAL)
140 }
141
142 fn char_width(&self) -> usize {
143 self.core.char_width
144 }
145
146 fn char_height(&self) -> usize {
147 self.core.char_height
148 }
149
150 fn pixel_width(&self) -> usize {
151 self.core.pixel_width
152 }
153
154 fn pixel_height(&self) -> usize {
155 self.core.pixel_height
156 }
157
158 fn transform(&self) -> &Transform2D {
159 &self.core.transform
160 }
161
162 fn transform_mut(&mut self) -> &mut Transform2D {
163 &mut self.core.transform
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::DotCanvas;
170 use crate::canvas::{Canvas, draw_reference_canvas_scene, render_canvas_show};
171 use crate::color::CanvasColor;
172 use crate::test_util::assert_fixture_eq;
173
174 fn fixture_canvas() -> DotCanvas {
175 let mut canvas = DotCanvas::new(40, 10, 0.0, 0.0, 1.0, 1.0);
176 draw_reference_canvas_scene(&mut canvas);
177 canvas
178 }
179
180 #[test]
181 fn print_row_matches_fixture() {
182 let canvas = fixture_canvas();
183 let actual = canvas.print_row(2, true);
184
185 assert_fixture_eq(&actual, "tests/fixtures/canvas/dot_printrow.txt");
186 }
187
188 #[test]
189 fn print_matches_fixture_with_color() {
190 let canvas = fixture_canvas();
191 let actual = canvas.print(true);
192
193 assert_fixture_eq(&actual, "tests/fixtures/canvas/dot_print.txt");
194 }
195
196 #[test]
197 fn print_matches_fixture_without_color() {
198 let canvas = fixture_canvas();
199 let actual = canvas.print(false);
200
201 assert_fixture_eq(&actual, "tests/fixtures/canvas/dot_print_nocolor.txt");
202 }
203
204 #[test]
205 fn show_matches_fixture_with_color() {
206 let canvas = fixture_canvas();
207 let actual = render_canvas_show(canvas, true);
208
209 assert_fixture_eq(&actual, "tests/fixtures/canvas/dot_show.txt");
210 }
211
212 #[test]
213 fn show_matches_fixture_without_color() {
214 let canvas = fixture_canvas();
215 let actual = render_canvas_show(canvas, false);
216
217 assert_fixture_eq(&actual, "tests/fixtures/canvas/dot_show_nocolor.txt");
218 }
219
220 #[test]
221 fn dot_pixel_encoding_matches_lookup_table() {
222 let mut top = DotCanvas::new(1, 1, 0.0, 0.0, 1.0, 1.0);
223 top.pixel(0, 0, CanvasColor::GREEN);
224 assert_eq!(top.glyph_at(0, 0), '\'');
225
226 let mut bottom = DotCanvas::new(1, 1, 0.0, 0.0, 1.0, 1.0);
227 bottom.pixel(0, 1, CanvasColor::GREEN);
228 assert_eq!(bottom.glyph_at(0, 0), '.');
229
230 let mut both = DotCanvas::new(1, 1, 0.0, 0.0, 1.0, 1.0);
231 both.pixel(0, 0, CanvasColor::GREEN);
232 both.pixel(0, 1, CanvasColor::GREEN);
233 assert_eq!(both.glyph_at(0, 0), ':');
234 }
235
236 #[test]
237 fn dot_pixel_ors_color_bits_on_overlaps() {
238 let mut canvas = DotCanvas::new(1, 1, 0.0, 0.0, 1.0, 1.0);
239 canvas.pixel(0, 0, CanvasColor::BLUE);
240 canvas.pixel(0, 0, CanvasColor::RED);
241
242 assert_eq!(canvas.color_at(0, 0), CanvasColor::MAGENTA);
243 }
244
245 #[test]
246 fn dot_pixel_clamps_upper_bounds_and_rejects_out_of_bounds() {
247 let mut canvas = DotCanvas::new(1, 1, 0.0, 0.0, 1.0, 1.0);
248 canvas.pixel(1, 2, CanvasColor::YELLOW);
249 assert_eq!(canvas.glyph_at(0, 0), '.');
250
251 canvas.pixel(2, 1, CanvasColor::WHITE);
252 assert_eq!(canvas.glyph_at(0, 0), '.');
253 }
254}