Skip to main content

endbasic_std/console/
drawing.rs

1// EndBASIC
2// Copyright 2024 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Drawing algorithms for consoles that don't provide native rendering primitives.
17
18use crate::console::graphics::{ClampedInto, RasterOps};
19use crate::console::{PixelsXY, SizeInPixels};
20use crate::gfx::lcd::fonts::Font;
21use std::convert::TryFrom;
22use std::io;
23
24/// Auxiliary function for the `draw_line` algorithm to handle slopes between 0 and -1.
25fn draw_line_low<R>(rasops: &mut R, x1: i32, y1: i32, x2: i32, y2: i32) -> io::Result<()>
26where
27    R: RasterOps,
28{
29    let dx = x2 - x1;
30    let mut dy = y2 - y1;
31
32    let mut yi = 1;
33    if dy < 0 {
34        yi = -1;
35        dy = -dy;
36    }
37    let mut d = (2 * dy) - dx;
38    let mut y = y1;
39
40    for x in x1..(x2 + 1) {
41        if cfg!(debug_assertions) {
42            rasops.draw_pixel(PixelsXY {
43                x: i16::try_from(x).expect("Coordinate must fit after computations"),
44                y: i16::try_from(y).expect("Coordinate must fit after computations"),
45            })?;
46        } else {
47            rasops.draw_pixel(PixelsXY { x: x as i16, y: y as i16 })?;
48        }
49        if d > 0 {
50            y += yi;
51            d += 2 * (dy - dx);
52        } else {
53            d += 2 * dy;
54        }
55    }
56
57    Ok(())
58}
59
60/// Auxiliary function for the `draw_line` algorithm to handle positive or negative slopes.
61fn draw_line_high<R>(rasops: &mut R, x1: i32, y1: i32, x2: i32, y2: i32) -> io::Result<()>
62where
63    R: RasterOps,
64{
65    let mut dx = x2 - x1;
66    let dy = y2 - y1;
67
68    let mut xi = 1;
69    if dx < 0 {
70        xi = -1;
71        dx = -dx;
72    }
73    let mut d = (2 * dx) - dy;
74    let mut x = x1;
75
76    for y in y1..(y2 + 1) {
77        if cfg!(debug_assertions) {
78            rasops.draw_pixel(PixelsXY {
79                x: i16::try_from(x).expect("Coordinate must fit after computations"),
80                y: i16::try_from(y).expect("Coordinate must fit after computations"),
81            })?;
82        } else {
83            rasops.draw_pixel(PixelsXY { x: x as i16, y: y as i16 })?;
84        }
85        if d > 0 {
86            x += xi;
87            d += 2 * (dx - dy);
88        } else {
89            d += 2 * dx;
90        }
91    }
92
93    Ok(())
94}
95
96/// Draws a line from `x1y1` to `x2y2` via `rasops`.
97///
98/// This implements the [Bresenham's line
99/// algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm).
100pub fn draw_line<R>(rasops: &mut R, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()>
101where
102    R: RasterOps,
103{
104    // Widen coordinates so we don't have to worry about overflows anywhere.
105    let x1 = i32::from(x1y1.x);
106    let y1 = i32::from(x1y1.y);
107    let x2 = i32::from(x2y2.x);
108    let y2 = i32::from(x2y2.y);
109
110    if (y2 - y1).abs() < (x2 - x1).abs() {
111        if x1y1.x > x2y2.x {
112            draw_line_low(rasops, x2, y2, x1, y1)
113        } else {
114            draw_line_low(rasops, x1, y1, x2, y2)
115        }
116    } else {
117        if x1y1.y > x2y2.y {
118            draw_line_high(rasops, x2, y2, x1, y1)
119        } else {
120            draw_line_high(rasops, x1, y1, x2, y2)
121        }
122    }
123}
124
125/// Fills the 4-connected region around `xy` via `rasops`.
126pub fn bucket_fill<R>(rasops: &mut R, xy: PixelsXY, fill_color: u8) -> io::Result<()>
127where
128    R: RasterOps,
129{
130    let seed_color = match rasops.peek_pixel(xy)? {
131        Some(color) => color,
132        None => return Ok(()),
133    };
134    if seed_color == fill_color {
135        return Ok(());
136    }
137
138    let mut pending = vec![xy];
139    while let Some(xy) = pending.pop() {
140        if rasops.peek_pixel(xy)? != Some(seed_color) {
141            continue;
142        }
143
144        rasops.draw_pixel(xy)?;
145
146        if xy.x > i16::MIN {
147            pending.push(PixelsXY::new(xy.x - 1, xy.y));
148        }
149        if xy.x < i16::MAX {
150            pending.push(PixelsXY::new(xy.x + 1, xy.y));
151        }
152        if xy.y > i16::MIN {
153            pending.push(PixelsXY::new(xy.x, xy.y - 1));
154        }
155        if xy.y < i16::MAX {
156            pending.push(PixelsXY::new(xy.x, xy.y + 1));
157        }
158    }
159
160    Ok(())
161}
162
163/// Draws a circle via `rasops` with `center` and `radius`.
164///
165/// This implements the [Midpoint circle
166/// algorithm](https://en.wikipedia.org/wiki/Midpoint_circle_algorithm).
167pub fn draw_circle<R>(rasops: &mut R, center: PixelsXY, radius: u16) -> io::Result<()>
168where
169    R: RasterOps,
170{
171    fn point<R: RasterOps>(rasops: &mut R, x: i16, y: i16) -> io::Result<()> {
172        rasops.draw_pixel(PixelsXY { x, y })
173    }
174
175    if radius == 0 {
176        return Ok(());
177    } else if radius == 1 {
178        return rasops.draw_pixel(center);
179    }
180
181    let (diameter, radius): (i16, i16) = match radius.checked_mul(2) {
182        Some(d) => match i16::try_from(d) {
183            Ok(d) => (d, radius as i16),
184            Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
185        },
186        None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
187    };
188
189    let mut x: i16 = radius - 1;
190    let mut y: i16 = 0;
191    let mut tx: i16 = 1;
192    let mut ty: i16 = 1;
193    let mut e: i16 = tx - diameter;
194
195    while x >= y {
196        point(rasops, center.x + x, center.y - y)?;
197        point(rasops, center.x + x, center.y + y)?;
198        point(rasops, center.x - x, center.y - y)?;
199        point(rasops, center.x - x, center.y + y)?;
200        point(rasops, center.x + y, center.y - x)?;
201        point(rasops, center.x + y, center.y + x)?;
202        point(rasops, center.x - y, center.y - x)?;
203        point(rasops, center.x - y, center.y + x)?;
204
205        if e <= 0 {
206            y += 1;
207            e += ty;
208            ty += 2;
209        }
210
211        if e > 0 {
212            x -= 1;
213            tx += 2;
214            e += tx - diameter;
215        }
216    }
217
218    Ok(())
219}
220
221/// Draws a circle via `rasops` with `center` and `radius`.
222///
223/// This implements the [Midpoint circle
224/// algorithm](https://en.wikipedia.org/wiki/Midpoint_circle_algorithm).
225pub fn draw_circle_filled<R>(rasops: &mut R, center: PixelsXY, radius: u16) -> io::Result<()>
226where
227    R: RasterOps,
228{
229    fn line<R: RasterOps>(rasops: &mut R, x1: i16, y1: i16, x2: i16, y2: i16) -> io::Result<()> {
230        rasops.draw_line(PixelsXY { x: x1, y: y1 }, PixelsXY { x: x2, y: y2 })
231    }
232
233    if radius == 0 {
234        return Ok(());
235    } else if radius == 1 {
236        return rasops.draw_pixel(center);
237    }
238
239    let (diameter, radius): (i16, i16) = match radius.checked_mul(2) {
240        Some(d) => match i16::try_from(d) {
241            Ok(d) => (d, radius as i16),
242
243            Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
244        },
245        None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
246    };
247
248    let mut x: i16 = radius - 1;
249    let mut y: i16 = 0;
250    let mut tx: i16 = 1;
251    let mut ty: i16 = 1;
252    let mut e: i16 = tx - diameter;
253
254    while x >= y {
255        line(rasops, center.x + x, center.y - y, center.x + x, center.y + y)?;
256        line(rasops, center.x - x, center.y - y, center.x - x, center.y + y)?;
257        line(rasops, center.x + y, center.y - x, center.x + y, center.y + x)?;
258        line(rasops, center.x - y, center.y - x, center.x - y, center.y + x)?;
259
260        if e <= 0 {
261            y += 1;
262            e += ty;
263            ty += 2;
264        }
265
266        if e > 0 {
267            x -= 1;
268            tx += 2;
269            e += tx - diameter;
270        }
271    }
272
273    Ok(())
274}
275
276/// Draws a polygon via `rasops`.
277pub fn draw_poly<R>(rasops: &mut R, points: &[PixelsXY]) -> io::Result<()>
278where
279    R: RasterOps + ?Sized,
280{
281    if points.len() < 2 {
282        return Ok(());
283    }
284
285    for i in 1..points.len() {
286        rasops.draw_line(points[i - 1], points[i])?;
287    }
288    rasops.draw_line(*points.last().expect("At least 2 points are present"), points[0])?;
289    Ok(())
290}
291
292/// Calculates the x-coordinate intersection of a horizontal scanline with a line segment.
293///
294/// This function uses the linear interpolation formula to find the point where the
295/// scanline at height `y` crosses the edge defined by points `p1` and `p2`.
296///
297/// Returns the x-coordinate of the intersection if the scanline passes through the edge
298/// (specifically within the half-open interval `[ymin, ymax)`, or `None` if the edge is
299/// horizontal (to avoid division by zero) or if the scanline does not intersect the
300/// vertical span of the edge.
301fn edge_intersection(y: i32, p1: PixelsXY, p2: PixelsXY) -> Option<i32> {
302    let y1 = i32::from(p1.y);
303    let y2 = i32::from(p2.y);
304    if y1 == y2 {
305        return None;
306    }
307
308    let ymin = y1.min(y2);
309    let ymax = y1.max(y2);
310    // The check `y >= ymax` is to return `None` to prevent double-counting vertices
311    // in polygon filling algorithms (the "top-left" rule).
312    if y < ymin || y >= ymax {
313        return None;
314    }
315
316    let x1 = i32::from(p1.x);
317    let x2 = i32::from(p2.x);
318    Some(x1 + (y - y1) * (x2 - x1) / (y2 - y1))
319}
320
321/// Fills the poligon defined by `points` using a scanline intersection algorithm.
322fn fill_polygon<R>(rasops: &mut R, points: &[PixelsXY]) -> io::Result<()>
323where
324    R: RasterOps + ?Sized,
325{
326    if points.is_empty() {
327        return Ok(());
328    }
329
330    let min_y = points.iter().map(|p| i32::from(p.y)).min().expect("Points are not empty");
331    let max_y = points.iter().map(|p| i32::from(p.y)).max().expect("Points are not empty");
332
333    for y in min_y..=max_y {
334        let mut xs = vec![];
335        for i in 0..points.len() {
336            let next = (i + 1) % points.len();
337            if let Some(x) = edge_intersection(y, points[i], points[next]) {
338                xs.push(x);
339            }
340        }
341
342        if xs.len() < 2 {
343            continue;
344        }
345
346        xs.sort_unstable();
347        for pair in xs.chunks_exact(2) {
348            rasops.draw_line(
349                PixelsXY::new(pair[0].clamped_into(), y.clamped_into()),
350                PixelsXY::new(pair[1].clamped_into(), y.clamped_into()),
351            )?;
352        }
353    }
354
355    Ok(())
356}
357
358/// Draws a filled polygon via `rasops`.
359pub fn draw_poly_filled<R>(rasops: &mut R, points: &[PixelsXY]) -> io::Result<()>
360where
361    R: RasterOps + ?Sized,
362{
363    fill_polygon(rasops, points)?;
364    draw_poly(rasops, points)
365}
366
367/// Draws a rectangle via `rasops` starting at `x1y1` with `size`.
368pub fn draw_rect<R>(rasops: &mut R, x1y1: PixelsXY, size: SizeInPixels) -> io::Result<()>
369where
370    R: RasterOps,
371{
372    let x2y2 = PixelsXY {
373        x: (i32::from(x1y1.x) + i32::from(size.width - 1)).clamped_into(),
374        y: (i32::from(x1y1.y) + i32::from(size.height - 1)).clamped_into(),
375    };
376    rasops.draw_line(PixelsXY { x: x1y1.x, y: x1y1.y }, PixelsXY { x: x2y2.x, y: x1y1.y })?;
377    rasops.draw_line(PixelsXY { x: x2y2.x, y: x1y1.y }, PixelsXY { x: x2y2.x, y: x2y2.y })?;
378    rasops.draw_line(PixelsXY { x: x2y2.x, y: x2y2.y }, PixelsXY { x: x1y1.x, y: x2y2.y })?;
379    rasops.draw_line(PixelsXY { x: x1y1.x, y: x2y2.y }, PixelsXY { x: x1y1.x, y: x1y1.y })?;
380    Ok(())
381}
382
383/// Writes a single character `ch` at `pos`.
384fn draw_char<R>(rasops: &mut R, font: &Font, pos: PixelsXY, ch: char) -> io::Result<()>
385where
386    R: RasterOps,
387{
388    let glyph = font.glyph(ch);
389    for j in 0..font.glyph_size.height {
390        for k in 0..font.stride {
391            let row = glyph[j * font.stride + k];
392            let mut mask = 0x80;
393            for i in 0..font.glyph_size.width {
394                let bit = row & mask;
395                if bit != 0 {
396                    // TODO(jmmv): The "as i16" conversions below are code smells.  We should
397                    // change the font glyph size representation to be u8s to allow widening
398                    // the integers instead of having to truncate them.
399                    let x = pos.x + i as i16 + k as i16 * 8;
400                    let y = pos.y + j as i16;
401                    rasops.draw_pixel(PixelsXY { x, y })?;
402                }
403                mask >>= 1;
404            }
405        }
406    }
407    Ok(())
408}
409
410/// Writes a single character `ch` at `pos`.
411pub fn draw_text<R>(rasops: &mut R, font: &Font, xy: PixelsXY, text: &str) -> io::Result<()>
412where
413    R: RasterOps,
414{
415    let mut pos = xy;
416    for ch in text.chars() {
417        draw_char(rasops, font, pos, ch)?;
418        pos.x += i16::try_from(font.glyph_size.width).expect("Must fit");
419    }
420    Ok(())
421}
422
423/// Draws a triangle via `rasops`.
424pub fn draw_tri<R>(rasops: &mut R, x1y1: PixelsXY, x2y2: PixelsXY, x3y3: PixelsXY) -> io::Result<()>
425where
426    R: RasterOps,
427{
428    rasops.draw_line(x1y1, x2y2)?;
429    rasops.draw_line(x2y2, x3y3)?;
430    rasops.draw_line(x3y3, x1y1)?;
431    Ok(())
432}
433
434/// Draws a filled triangle via `rasops`.
435pub fn draw_tri_filled<R>(
436    rasops: &mut R,
437    x1y1: PixelsXY,
438    x2y2: PixelsXY,
439    x3y3: PixelsXY,
440) -> io::Result<()>
441where
442    R: RasterOps,
443{
444    fill_polygon(rasops, &[x1y1, x2y2, x3y3])?;
445    draw_tri(rasops, x1y1, x2y2, x3y3)
446}
447
448#[cfg(test)]
449mod testutils {
450    use super::*;
451    use crate::console::graphics::RasterInfo;
452    use crate::console::{RGB, SizeInPixels, rgb_to_ansi_color};
453    use std::collections::HashMap;
454
455    /// Representation of captured raster operations.
456    #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
457    pub(crate) enum CapturedRasop {
458        DrawLine(i16, i16, i16, i16),
459        DrawPixel(i16, i16),
460    }
461
462    /// An implementation of `RasterOps` that captures calls for later validation.
463    ///
464    /// This also captures the minimum set of side-effects required by tests (like the active color
465    /// or some pixels directly drawn).
466    #[derive(Default)]
467    pub(crate) struct RecordingRasops {
468        pub(crate) ops: Vec<CapturedRasop>,
469        pub(crate) draw_color: Option<u8>,
470        pub(crate) pixels: HashMap<PixelsXY, Option<u8>>,
471    }
472
473    impl RecordingRasops {
474        pub(crate) fn set_pixel(&mut self, xy: PixelsXY, color: Option<u8>) {
475            self.pixels.insert(xy, color);
476        }
477    }
478
479    impl RasterOps for RecordingRasops {
480        type ID = u8;
481
482        fn get_info(&self) -> RasterInfo {
483            unimplemented!();
484        }
485
486        fn set_draw_color(&mut self, color: RGB) {
487            self.draw_color = rgb_to_ansi_color(color);
488        }
489
490        fn clear(&mut self) -> io::Result<()> {
491            unimplemented!();
492        }
493
494        fn set_sync(&mut self, _enabled: bool) {
495            unimplemented!();
496        }
497
498        fn present_canvas(&mut self) -> io::Result<()> {
499            unimplemented!();
500        }
501
502        fn peek_pixel(&self, xy: PixelsXY) -> io::Result<Option<u8>> {
503            Ok(self.pixels.get(&xy).copied().unwrap_or(None))
504        }
505
506        fn read_pixels(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<Self::ID> {
507            unimplemented!();
508        }
509
510        fn put_pixels(&mut self, _xy: PixelsXY, _data: &Self::ID) -> io::Result<()> {
511            unimplemented!();
512        }
513
514        fn move_pixels(
515            &mut self,
516            _x1y1: PixelsXY,
517            _x2y2: PixelsXY,
518            _size: SizeInPixels,
519        ) -> io::Result<()> {
520            unimplemented!();
521        }
522
523        fn write_text(&mut self, _xy: PixelsXY, _text: &str) -> io::Result<()> {
524            unimplemented!();
525        }
526
527        fn draw_circle(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
528            unimplemented!();
529        }
530
531        fn draw_circle_filled(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
532            unimplemented!();
533        }
534
535        fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
536            self.ops.push(CapturedRasop::DrawLine(x1y1.x, x1y1.y, x2y2.x, x2y2.y));
537            Ok(())
538        }
539
540        fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
541            self.ops.push(CapturedRasop::DrawPixel(xy.x, xy.y));
542            self.set_pixel(xy, self.draw_color);
543            Ok(())
544        }
545
546        fn draw_poly(&mut self, points: &[PixelsXY]) -> io::Result<()> {
547            draw_poly(self, points)
548        }
549
550        fn draw_poly_filled(&mut self, points: &[PixelsXY]) -> io::Result<()> {
551            draw_poly_filled(self, points)
552        }
553
554        fn draw_rect(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<()> {
555            unimplemented!();
556        }
557
558        fn draw_rect_filled(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<()> {
559            unimplemented!();
560        }
561
562        fn draw_tri(
563            &mut self,
564            _x1y1: PixelsXY,
565            _x2y2: PixelsXY,
566            _x3y3: PixelsXY,
567        ) -> io::Result<()> {
568            unimplemented!();
569        }
570
571        fn draw_tri_filled(
572            &mut self,
573            _x1y1: PixelsXY,
574            _x2y2: PixelsXY,
575            _x3y3: PixelsXY,
576        ) -> io::Result<()> {
577            unimplemented!();
578        }
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::testutils::*;
585    use super::*;
586    use crate::console::ansi_color_to_rgb;
587
588    #[test]
589    fn test_bucket_fill_connected_region() {
590        let mut rasops = RecordingRasops::default();
591        rasops.set_draw_color(ansi_color_to_rgb(7));
592        for xy in [
593            PixelsXY::new(0, 0),
594            PixelsXY::new(1, 0),
595            PixelsXY::new(0, 1),
596            PixelsXY::new(2, 1),
597            PixelsXY::new(1, 2),
598        ] {
599            rasops.set_pixel(xy, Some(2));
600        }
601
602        bucket_fill(&mut rasops, PixelsXY::new(0, 0), 7).unwrap();
603
604        rasops.ops.sort();
605        assert_eq!(
606            [
607                CapturedRasop::DrawPixel(0, 0),
608                CapturedRasop::DrawPixel(0, 1),
609                CapturedRasop::DrawPixel(1, 0),
610            ],
611            rasops.ops.as_slice()
612        );
613    }
614
615    #[test]
616    fn test_bucket_fill_diagonal_is_not_connected() {
617        let mut rasops = RecordingRasops::default();
618        rasops.set_draw_color(ansi_color_to_rgb(7));
619        rasops.set_pixel(PixelsXY::new(0, 0), Some(2));
620        rasops.set_pixel(PixelsXY::new(1, 1), Some(2));
621
622        bucket_fill(&mut rasops, PixelsXY::new(0, 0), 7).unwrap();
623
624        assert_eq!([CapturedRasop::DrawPixel(0, 0)], rasops.ops.as_slice());
625        assert_eq!(Some(2), rasops.peek_pixel(PixelsXY::new(1, 1)).unwrap());
626    }
627
628    #[test]
629    fn test_bucket_fill_out_of_bounds_is_noop() {
630        let mut rasops = RecordingRasops::default();
631        rasops.set_draw_color(ansi_color_to_rgb(7));
632        bucket_fill(&mut rasops, PixelsXY::new(10, 20), 7).unwrap();
633        assert!(rasops.ops.is_empty());
634    }
635
636    #[test]
637    fn test_bucket_fill_same_color_is_noop() {
638        let mut rasops = RecordingRasops::default();
639        rasops.set_draw_color(ansi_color_to_rgb(7));
640        rasops.set_pixel(PixelsXY::new(10, 20), Some(7));
641
642        bucket_fill(&mut rasops, PixelsXY::new(10, 20), 7).unwrap();
643
644        assert!(rasops.ops.is_empty());
645    }
646
647    #[test]
648    fn test_draw_circle_zero() {
649        let mut rasops = RecordingRasops::default();
650        draw_circle(&mut rasops, PixelsXY::new(10, 20), 0).unwrap();
651        assert!(rasops.ops.is_empty());
652    }
653
654    #[test]
655    fn test_draw_circle_dot() {
656        let mut rasops = RecordingRasops::default();
657        draw_circle(&mut rasops, PixelsXY::new(10, 20), 1).unwrap();
658        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
659    }
660
661    #[test]
662    fn test_draw_circle_larger() {
663        let mut rasops = RecordingRasops::default();
664        draw_circle(&mut rasops, PixelsXY::new(10, 20), 4).unwrap();
665        rasops.ops.sort();
666        assert_eq!(
667            [
668                CapturedRasop::DrawPixel(7, 18),
669                CapturedRasop::DrawPixel(7, 19),
670                CapturedRasop::DrawPixel(7, 20),
671                CapturedRasop::DrawPixel(7, 20),
672                CapturedRasop::DrawPixel(7, 21),
673                CapturedRasop::DrawPixel(7, 22),
674                CapturedRasop::DrawPixel(8, 17),
675                CapturedRasop::DrawPixel(8, 23),
676                CapturedRasop::DrawPixel(9, 17),
677                CapturedRasop::DrawPixel(9, 23),
678                CapturedRasop::DrawPixel(10, 17),
679                CapturedRasop::DrawPixel(10, 17),
680                CapturedRasop::DrawPixel(10, 23),
681                CapturedRasop::DrawPixel(10, 23),
682                CapturedRasop::DrawPixel(11, 17),
683                CapturedRasop::DrawPixel(11, 23),
684                CapturedRasop::DrawPixel(12, 17),
685                CapturedRasop::DrawPixel(12, 23),
686                CapturedRasop::DrawPixel(13, 18),
687                CapturedRasop::DrawPixel(13, 19),
688                CapturedRasop::DrawPixel(13, 20),
689                CapturedRasop::DrawPixel(13, 20),
690                CapturedRasop::DrawPixel(13, 21),
691                CapturedRasop::DrawPixel(13, 22),
692            ],
693            rasops.ops.as_slice()
694        );
695    }
696
697    #[test]
698    fn test_draw_circle_corners() {
699        for corner in
700            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
701        {
702            let mut rasops = RecordingRasops::default();
703            draw_circle(&mut rasops, corner, 1).unwrap();
704            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
705        }
706    }
707
708    #[test]
709    fn test_draw_circle_filled_zero() {
710        let mut rasops = RecordingRasops::default();
711        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 0).unwrap();
712        assert!(rasops.ops.is_empty());
713    }
714
715    #[test]
716    fn test_draw_circle_filled_dot() {
717        let mut rasops = RecordingRasops::default();
718        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 1).unwrap();
719        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
720    }
721
722    #[test]
723    fn test_draw_circle_filled_larger() {
724        let mut rasops = RecordingRasops::default();
725        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 4).unwrap();
726        rasops.ops.sort();
727        assert_eq!(
728            [
729                CapturedRasop::DrawLine(7, 18, 7, 22),
730                CapturedRasop::DrawLine(7, 19, 7, 21),
731                CapturedRasop::DrawLine(7, 20, 7, 20),
732                CapturedRasop::DrawLine(8, 17, 8, 23),
733                CapturedRasop::DrawLine(9, 17, 9, 23),
734                CapturedRasop::DrawLine(10, 17, 10, 23),
735                CapturedRasop::DrawLine(10, 17, 10, 23),
736                CapturedRasop::DrawLine(11, 17, 11, 23),
737                CapturedRasop::DrawLine(12, 17, 12, 23),
738                CapturedRasop::DrawLine(13, 18, 13, 22),
739                CapturedRasop::DrawLine(13, 19, 13, 21),
740                CapturedRasop::DrawLine(13, 20, 13, 20),
741            ],
742            rasops.ops.as_slice()
743        );
744    }
745
746    #[test]
747    fn test_draw_circle_filled_corners() {
748        for corner in
749            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
750        {
751            let mut rasops = RecordingRasops::default();
752            draw_circle_filled(&mut rasops, corner, 1).unwrap();
753            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
754        }
755    }
756
757    #[test]
758    fn test_draw_line_dot() {
759        let mut rasops = RecordingRasops::default();
760        draw_line(&mut rasops, PixelsXY::new(10, 20), PixelsXY::new(10, 20)).unwrap();
761        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
762    }
763
764    #[test]
765    fn test_draw_line_horizontal_right() {
766        let mut rasops = RecordingRasops::default();
767        draw_line(&mut rasops, PixelsXY::new(100, 0), PixelsXY::new(105, 0)).unwrap();
768        assert_eq!(
769            [
770                CapturedRasop::DrawPixel(100, 0),
771                CapturedRasop::DrawPixel(101, 0),
772                CapturedRasop::DrawPixel(102, 0),
773                CapturedRasop::DrawPixel(103, 0),
774                CapturedRasop::DrawPixel(104, 0),
775                CapturedRasop::DrawPixel(105, 0),
776            ],
777            rasops.ops.as_slice()
778        );
779    }
780
781    #[test]
782    fn test_draw_line_horizontal_left() {
783        let mut rasops = RecordingRasops::default();
784        draw_line(&mut rasops, PixelsXY::new(105, 0), PixelsXY::new(100, 0)).unwrap();
785        assert_eq!(
786            [
787                CapturedRasop::DrawPixel(100, 0),
788                CapturedRasop::DrawPixel(101, 0),
789                CapturedRasop::DrawPixel(102, 0),
790                CapturedRasop::DrawPixel(103, 0),
791                CapturedRasop::DrawPixel(104, 0),
792                CapturedRasop::DrawPixel(105, 0),
793            ],
794            rasops.ops.as_slice()
795        );
796    }
797
798    #[test]
799    fn test_draw_line_vertical_down() {
800        let mut rasops = RecordingRasops::default();
801        draw_line(&mut rasops, PixelsXY::new(0, 100), PixelsXY::new(0, 105)).unwrap();
802        assert_eq!(
803            [
804                CapturedRasop::DrawPixel(0, 100),
805                CapturedRasop::DrawPixel(0, 101),
806                CapturedRasop::DrawPixel(0, 102),
807                CapturedRasop::DrawPixel(0, 103),
808                CapturedRasop::DrawPixel(0, 104),
809                CapturedRasop::DrawPixel(0, 105),
810            ],
811            rasops.ops.as_slice()
812        );
813    }
814
815    #[test]
816    fn test_draw_line_vertical_up() {
817        let mut rasops = RecordingRasops::default();
818        draw_line(&mut rasops, PixelsXY::new(0, 105), PixelsXY::new(0, 100)).unwrap();
819        assert_eq!(
820            [
821                CapturedRasop::DrawPixel(0, 100),
822                CapturedRasop::DrawPixel(0, 101),
823                CapturedRasop::DrawPixel(0, 102),
824                CapturedRasop::DrawPixel(0, 103),
825                CapturedRasop::DrawPixel(0, 104),
826                CapturedRasop::DrawPixel(0, 105),
827            ],
828            rasops.ops.as_slice()
829        );
830    }
831
832    #[test]
833    fn test_draw_line_horizontal_slope_right() {
834        let mut rasops = RecordingRasops::default();
835        draw_line(&mut rasops, PixelsXY::new(100, 0), PixelsXY::new(105, 3)).unwrap();
836        assert_eq!(
837            [
838                CapturedRasop::DrawPixel(100, 0),
839                CapturedRasop::DrawPixel(101, 1),
840                CapturedRasop::DrawPixel(102, 1),
841                CapturedRasop::DrawPixel(103, 2),
842                CapturedRasop::DrawPixel(104, 2),
843                CapturedRasop::DrawPixel(105, 3),
844            ],
845            rasops.ops.as_slice()
846        );
847    }
848
849    #[test]
850    fn test_draw_line_horizontal_slope_left() {
851        let mut rasops = RecordingRasops::default();
852        draw_line(&mut rasops, PixelsXY::new(105, 0), PixelsXY::new(100, 3)).unwrap();
853        assert_eq!(
854            [
855                CapturedRasop::DrawPixel(100, 3),
856                CapturedRasop::DrawPixel(101, 2),
857                CapturedRasop::DrawPixel(102, 2),
858                CapturedRasop::DrawPixel(103, 1),
859                CapturedRasop::DrawPixel(104, 1),
860                CapturedRasop::DrawPixel(105, 0),
861            ],
862            rasops.ops.as_slice()
863        );
864    }
865
866    #[test]
867    fn test_draw_line_vertical_slope_up() {
868        let mut rasops = RecordingRasops::default();
869        draw_line(&mut rasops, PixelsXY::new(0, 100), PixelsXY::new(3, 105)).unwrap();
870        assert_eq!(
871            [
872                CapturedRasop::DrawPixel(0, 100),
873                CapturedRasop::DrawPixel(1, 101),
874                CapturedRasop::DrawPixel(1, 102),
875                CapturedRasop::DrawPixel(2, 103),
876                CapturedRasop::DrawPixel(2, 104),
877                CapturedRasop::DrawPixel(3, 105),
878            ],
879            rasops.ops.as_slice()
880        );
881    }
882
883    #[test]
884    fn test_draw_line_vertical_slope_down() {
885        let mut rasops = RecordingRasops::default();
886        draw_line(&mut rasops, PixelsXY::new(0, 105), PixelsXY::new(3, 100)).unwrap();
887        assert_eq!(
888            [
889                CapturedRasop::DrawPixel(3, 100),
890                CapturedRasop::DrawPixel(2, 101),
891                CapturedRasop::DrawPixel(2, 102),
892                CapturedRasop::DrawPixel(1, 103),
893                CapturedRasop::DrawPixel(1, 104),
894                CapturedRasop::DrawPixel(0, 105),
895            ],
896            rasops.ops.as_slice()
897        );
898    }
899
900    #[test]
901    fn test_draw_line_corners() {
902        for corner in
903            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
904        {
905            let mut rasops = RecordingRasops::default();
906            draw_line(&mut rasops, corner, corner).unwrap();
907            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
908        }
909    }
910
911    #[test]
912    fn test_draw_line_perimeter() {
913        for corners in [
914            (PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT),
915            (PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_RIGHT),
916            (PixelsXY::BOTTOM_RIGHT, PixelsXY::BOTTOM_LEFT),
917            (PixelsXY::BOTTOM_LEFT, PixelsXY::TOP_LEFT),
918        ] {
919            let mut rasops = RecordingRasops::default();
920            draw_line(&mut rasops, corners.0, corners.1).unwrap();
921            assert_eq!(usize::from(u16::MAX) + 1, rasops.ops.len());
922        }
923    }
924
925    #[test]
926    fn test_draw_line_diagonals() {
927        for corners in [
928            (PixelsXY::TOP_LEFT, PixelsXY::BOTTOM_RIGHT),
929            (PixelsXY::BOTTOM_RIGHT, PixelsXY::TOP_LEFT),
930            (PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT),
931            (PixelsXY::BOTTOM_LEFT, PixelsXY::TOP_RIGHT),
932        ] {
933            let mut rasops = RecordingRasops::default();
934            draw_line(&mut rasops, corners.0, corners.1).unwrap();
935            assert_eq!(usize::from(u16::MAX) + 1, rasops.ops.len());
936        }
937    }
938
939    #[test]
940    fn test_draw_poly() {
941        let mut rasops = RecordingRasops::default();
942        draw_poly(
943            &mut rasops,
944            &[
945                PixelsXY::new(10, 20),
946                PixelsXY::new(20, 20),
947                PixelsXY::new(15, 30),
948                PixelsXY::new(8, 24),
949            ],
950        )
951        .unwrap();
952        assert_eq!(
953            [
954                CapturedRasop::DrawLine(10, 20, 20, 20),
955                CapturedRasop::DrawLine(20, 20, 15, 30),
956                CapturedRasop::DrawLine(15, 30, 8, 24),
957                CapturedRasop::DrawLine(8, 24, 10, 20),
958            ],
959            rasops.ops.as_slice()
960        );
961    }
962
963    #[test]
964    fn test_draw_poly_filled() {
965        let mut rasops = RecordingRasops::default();
966        draw_poly_filled(
967            &mut rasops,
968            &[
969                PixelsXY::new(10, 20),
970                PixelsXY::new(20, 20),
971                PixelsXY::new(18, 24),
972                PixelsXY::new(12, 24),
973            ],
974        )
975        .unwrap();
976        assert_eq!(
977            [
978                CapturedRasop::DrawLine(10, 20, 20, 20),
979                CapturedRasop::DrawLine(11, 21, 20, 21),
980                CapturedRasop::DrawLine(11, 22, 19, 22),
981                CapturedRasop::DrawLine(12, 23, 19, 23),
982                CapturedRasop::DrawLine(10, 20, 20, 20),
983                CapturedRasop::DrawLine(20, 20, 18, 24),
984                CapturedRasop::DrawLine(18, 24, 12, 24),
985                CapturedRasop::DrawLine(12, 24, 10, 20),
986            ],
987            rasops.ops.as_slice()
988        );
989    }
990
991    #[test]
992    fn test_draw_poly_filled_concave() {
993        let mut rasops = RecordingRasops::default();
994        draw_poly_filled(
995            &mut rasops,
996            &[
997                PixelsXY::new(10, 20),
998                PixelsXY::new(20, 20),
999                PixelsXY::new(20, 24),
1000                PixelsXY::new(15, 22),
1001                PixelsXY::new(10, 24),
1002            ],
1003        )
1004        .unwrap();
1005        assert_eq!(
1006            [
1007                CapturedRasop::DrawLine(10, 20, 20, 20),
1008                CapturedRasop::DrawLine(10, 21, 20, 21),
1009                CapturedRasop::DrawLine(10, 22, 15, 22),
1010                CapturedRasop::DrawLine(15, 22, 20, 22),
1011                CapturedRasop::DrawLine(10, 23, 13, 23),
1012                CapturedRasop::DrawLine(18, 23, 20, 23),
1013                CapturedRasop::DrawLine(10, 20, 20, 20),
1014                CapturedRasop::DrawLine(20, 20, 20, 24),
1015                CapturedRasop::DrawLine(20, 24, 15, 22),
1016                CapturedRasop::DrawLine(15, 22, 10, 24),
1017                CapturedRasop::DrawLine(10, 24, 10, 20),
1018            ],
1019            rasops.ops.as_slice()
1020        );
1021    }
1022
1023    #[test]
1024    fn test_draw_rect_dot() {
1025        let mut rasops = RecordingRasops::default();
1026        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(1, 1)).unwrap();
1027        assert_eq!(
1028            [
1029                CapturedRasop::DrawLine(10, 20, 10, 20),
1030                CapturedRasop::DrawLine(10, 20, 10, 20),
1031                CapturedRasop::DrawLine(10, 20, 10, 20),
1032                CapturedRasop::DrawLine(10, 20, 10, 20),
1033            ],
1034            rasops.ops.as_slice()
1035        );
1036    }
1037
1038    #[test]
1039    fn test_draw_rect_horizontal_line() {
1040        let mut rasops = RecordingRasops::default();
1041        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(6, 1)).unwrap();
1042        assert_eq!(
1043            [
1044                CapturedRasop::DrawLine(10, 20, 15, 20),
1045                CapturedRasop::DrawLine(15, 20, 15, 20),
1046                CapturedRasop::DrawLine(15, 20, 10, 20),
1047                CapturedRasop::DrawLine(10, 20, 10, 20),
1048            ],
1049            rasops.ops.as_slice()
1050        );
1051    }
1052
1053    #[test]
1054    fn test_draw_rect_vertical_line() {
1055        let mut rasops = RecordingRasops::default();
1056        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(1, 6)).unwrap();
1057        assert_eq!(
1058            [
1059                CapturedRasop::DrawLine(10, 20, 10, 20),
1060                CapturedRasop::DrawLine(10, 20, 10, 25),
1061                CapturedRasop::DrawLine(10, 25, 10, 25),
1062                CapturedRasop::DrawLine(10, 25, 10, 20),
1063            ],
1064            rasops.ops.as_slice()
1065        );
1066    }
1067
1068    #[test]
1069    fn test_draw_rect_2d() {
1070        let mut rasops = RecordingRasops::default();
1071        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(6, 7)).unwrap();
1072        assert_eq!(
1073            [
1074                CapturedRasop::DrawLine(10, 20, 15, 20),
1075                CapturedRasop::DrawLine(15, 20, 15, 26),
1076                CapturedRasop::DrawLine(15, 26, 10, 26),
1077                CapturedRasop::DrawLine(10, 26, 10, 20),
1078            ],
1079            rasops.ops.as_slice()
1080        );
1081    }
1082
1083    #[test]
1084    fn test_draw_rect_corners() {
1085        let mut rasops = RecordingRasops::default();
1086        draw_rect(&mut rasops, PixelsXY::TOP_LEFT, SizeInPixels::MAX).unwrap();
1087        assert_eq!(
1088            [
1089                CapturedRasop::DrawLine(i16::MIN, i16::MIN, i16::MAX - 1, i16::MIN),
1090                CapturedRasop::DrawLine(i16::MAX - 1, i16::MIN, i16::MAX - 1, i16::MAX - 1),
1091                CapturedRasop::DrawLine(i16::MAX - 1, i16::MAX - 1, i16::MIN, i16::MAX - 1),
1092                CapturedRasop::DrawLine(i16::MIN, i16::MAX - 1, i16::MIN, i16::MIN),
1093            ],
1094            rasops.ops.as_slice()
1095        );
1096    }
1097
1098    #[test]
1099    fn test_draw_text_empty() {
1100        let mut rasops = RecordingRasops::default();
1101        draw_text(&mut rasops, &crate::gfx::lcd::fonts::FONT_5X8, PixelsXY::new(10, 20), "")
1102            .unwrap();
1103        assert!(rasops.ops.is_empty());
1104    }
1105
1106    #[test]
1107    fn test_draw_text_one_char() {
1108        let mut rasops = RecordingRasops::default();
1109        draw_text(&mut rasops, &crate::gfx::lcd::fonts::FONT_5X8, PixelsXY::new(10, 20), "!")
1110            .unwrap();
1111        assert_eq!(
1112            [
1113                CapturedRasop::DrawPixel(12, 20),
1114                CapturedRasop::DrawPixel(12, 21),
1115                CapturedRasop::DrawPixel(12, 22),
1116                CapturedRasop::DrawPixel(12, 23),
1117                CapturedRasop::DrawPixel(12, 25),
1118            ],
1119            rasops.ops.as_slice()
1120        );
1121    }
1122
1123    #[test]
1124    fn test_draw_text_two_chars() {
1125        let mut rasops = RecordingRasops::default();
1126        draw_text(&mut rasops, &crate::gfx::lcd::fonts::FONT_5X8, PixelsXY::new(10, 20), "!!")
1127            .unwrap();
1128        assert_eq!(
1129            [
1130                CapturedRasop::DrawPixel(12, 20),
1131                CapturedRasop::DrawPixel(12, 21),
1132                CapturedRasop::DrawPixel(12, 22),
1133                CapturedRasop::DrawPixel(12, 23),
1134                CapturedRasop::DrawPixel(12, 25),
1135                CapturedRasop::DrawPixel(17, 20),
1136                CapturedRasop::DrawPixel(17, 21),
1137                CapturedRasop::DrawPixel(17, 22),
1138                CapturedRasop::DrawPixel(17, 23),
1139                CapturedRasop::DrawPixel(17, 25),
1140            ],
1141            rasops.ops.as_slice()
1142        );
1143    }
1144
1145    #[test]
1146    fn test_draw_tri() {
1147        let mut rasops = RecordingRasops::default();
1148        draw_tri(&mut rasops, PixelsXY::new(10, 20), PixelsXY::new(20, 20), PixelsXY::new(15, 30))
1149            .unwrap();
1150        assert_eq!(
1151            [
1152                CapturedRasop::DrawLine(10, 20, 20, 20),
1153                CapturedRasop::DrawLine(20, 20, 15, 30),
1154                CapturedRasop::DrawLine(15, 30, 10, 20),
1155            ],
1156            rasops.ops.as_slice()
1157        );
1158    }
1159
1160    #[test]
1161    fn test_draw_tri_filled() {
1162        let mut rasops = RecordingRasops::default();
1163        draw_tri_filled(
1164            &mut rasops,
1165            PixelsXY::new(10, 20),
1166            PixelsXY::new(20, 20),
1167            PixelsXY::new(15, 24),
1168        )
1169        .unwrap();
1170        assert_eq!(
1171            [
1172                CapturedRasop::DrawLine(10, 20, 20, 20),
1173                CapturedRasop::DrawLine(12, 21, 19, 21),
1174                CapturedRasop::DrawLine(13, 22, 18, 22),
1175                CapturedRasop::DrawLine(14, 23, 17, 23),
1176                CapturedRasop::DrawLine(10, 20, 20, 20),
1177                CapturedRasop::DrawLine(20, 20, 15, 24),
1178                CapturedRasop::DrawLine(15, 24, 10, 20),
1179            ],
1180            rasops.ops.as_slice()
1181        );
1182    }
1183}