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 std::convert::TryFrom;
21use std::io;
22
23/// Auxiliary function for the `draw_line` algorithm to handle slopes between 0 and -1.
24fn draw_line_low<R>(rasops: &mut R, x1: i32, y1: i32, x2: i32, y2: i32) -> io::Result<()>
25where
26    R: RasterOps,
27{
28    let dx = x2 - x1;
29    let mut dy = y2 - y1;
30
31    let mut yi = 1;
32    if dy < 0 {
33        yi = -1;
34        dy = -dy;
35    }
36    let mut d = (2 * dy) - dx;
37    let mut y = y1;
38
39    for x in x1..(x2 + 1) {
40        if cfg!(debug_assertions) {
41            rasops.draw_pixel(PixelsXY {
42                x: i16::try_from(x).expect("Coordinate must fit after computations"),
43                y: i16::try_from(y).expect("Coordinate must fit after computations"),
44            })?;
45        } else {
46            rasops.draw_pixel(PixelsXY { x: x as i16, y: y as i16 })?;
47        }
48        if d > 0 {
49            y += yi;
50            d += 2 * (dy - dx);
51        } else {
52            d += 2 * dy;
53        }
54    }
55
56    Ok(())
57}
58
59/// Auxiliary function for the `draw_line` algorithm to handle positive or negative slopes.
60fn draw_line_high<R>(rasops: &mut R, x1: i32, y1: i32, x2: i32, y2: i32) -> io::Result<()>
61where
62    R: RasterOps,
63{
64    let mut dx = x2 - x1;
65    let dy = y2 - y1;
66
67    let mut xi = 1;
68    if dx < 0 {
69        xi = -1;
70        dx = -dx;
71    }
72    let mut d = (2 * dx) - dy;
73    let mut x = x1;
74
75    for y in y1..(y2 + 1) {
76        if cfg!(debug_assertions) {
77            rasops.draw_pixel(PixelsXY {
78                x: i16::try_from(x).expect("Coordinate must fit after computations"),
79                y: i16::try_from(y).expect("Coordinate must fit after computations"),
80            })?;
81        } else {
82            rasops.draw_pixel(PixelsXY { x: x as i16, y: y as i16 })?;
83        }
84        if d > 0 {
85            x += xi;
86            d += 2 * (dx - dy);
87        } else {
88            d += 2 * dx;
89        }
90    }
91
92    Ok(())
93}
94
95/// Draws a line from `x1y1` to `x2y2` via `rasops`.
96///
97/// This implements the [Bresenham's line
98/// algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm).
99pub fn draw_line<R>(rasops: &mut R, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()>
100where
101    R: RasterOps,
102{
103    // Widen coordinates so we don't have to worry about overflows anywhere.
104    let x1 = i32::from(x1y1.x);
105    let y1 = i32::from(x1y1.y);
106    let x2 = i32::from(x2y2.x);
107    let y2 = i32::from(x2y2.y);
108
109    if (y2 - y1).abs() < (x2 - x1).abs() {
110        if x1y1.x > x2y2.x {
111            draw_line_low(rasops, x2, y2, x1, y1)
112        } else {
113            draw_line_low(rasops, x1, y1, x2, y2)
114        }
115    } else {
116        if x1y1.y > x2y2.y {
117            draw_line_high(rasops, x2, y2, x1, y1)
118        } else {
119            draw_line_high(rasops, x1, y1, x2, y2)
120        }
121    }
122}
123
124/// Draws a circle via `rasops` with `center` and `radius`.
125///
126/// This implements the [Midpoint circle
127/// algorithm](https://en.wikipedia.org/wiki/Midpoint_circle_algorithm).
128pub fn draw_circle<R>(rasops: &mut R, center: PixelsXY, radius: u16) -> io::Result<()>
129where
130    R: RasterOps,
131{
132    fn point<R: RasterOps>(rasops: &mut R, x: i16, y: i16) -> io::Result<()> {
133        rasops.draw_pixel(PixelsXY { x, y })
134    }
135
136    if radius == 0 {
137        return Ok(());
138    } else if radius == 1 {
139        return rasops.draw_pixel(center);
140    }
141
142    let (diameter, radius): (i16, i16) = match radius.checked_mul(2) {
143        Some(d) => match i16::try_from(d) {
144            Ok(d) => (d, radius as i16),
145            Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
146        },
147        None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
148    };
149
150    let mut x: i16 = radius - 1;
151    let mut y: i16 = 0;
152    let mut tx: i16 = 1;
153    let mut ty: i16 = 1;
154    let mut e: i16 = tx - diameter;
155
156    while x >= y {
157        point(rasops, center.x + x, center.y - y)?;
158        point(rasops, center.x + x, center.y + y)?;
159        point(rasops, center.x - x, center.y - y)?;
160        point(rasops, center.x - x, center.y + y)?;
161        point(rasops, center.x + y, center.y - x)?;
162        point(rasops, center.x + y, center.y + x)?;
163        point(rasops, center.x - y, center.y - x)?;
164        point(rasops, center.x - y, center.y + x)?;
165
166        if e <= 0 {
167            y += 1;
168            e += ty;
169            ty += 2;
170        }
171
172        if e > 0 {
173            x -= 1;
174            tx += 2;
175            e += tx - diameter;
176        }
177    }
178
179    Ok(())
180}
181
182/// Draws a circle via `rasops` with `center` and `radius`.
183///
184/// This implements the [Midpoint circle
185/// algorithm](https://en.wikipedia.org/wiki/Midpoint_circle_algorithm).
186pub fn draw_circle_filled<R>(rasops: &mut R, center: PixelsXY, radius: u16) -> io::Result<()>
187where
188    R: RasterOps,
189{
190    fn line<R: RasterOps>(rasops: &mut R, x1: i16, y1: i16, x2: i16, y2: i16) -> io::Result<()> {
191        rasops.draw_line(PixelsXY { x: x1, y: y1 }, PixelsXY { x: x2, y: y2 })
192    }
193
194    if radius == 0 {
195        return Ok(());
196    } else if radius == 1 {
197        return rasops.draw_pixel(center);
198    }
199
200    let (diameter, radius): (i16, i16) = match radius.checked_mul(2) {
201        Some(d) => match i16::try_from(d) {
202            Ok(d) => (d, radius as i16),
203
204            Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
205        },
206        None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "Radius is too big")),
207    };
208
209    let mut x: i16 = radius - 1;
210    let mut y: i16 = 0;
211    let mut tx: i16 = 1;
212    let mut ty: i16 = 1;
213    let mut e: i16 = tx - diameter;
214
215    while x >= y {
216        line(rasops, center.x + x, center.y - y, center.x + x, center.y + y)?;
217        line(rasops, center.x - x, center.y - y, center.x - x, center.y + y)?;
218        line(rasops, center.x + y, center.y - x, center.x + y, center.y + x)?;
219        line(rasops, center.x - y, center.y - x, center.x - y, center.y + x)?;
220
221        if e <= 0 {
222            y += 1;
223            e += ty;
224            ty += 2;
225        }
226
227        if e > 0 {
228            x -= 1;
229            tx += 2;
230            e += tx - diameter;
231        }
232    }
233
234    Ok(())
235}
236
237/// Draws a rectangle via `rasops` starting at `x1y1` with `size`.
238pub fn draw_rect<R>(rasops: &mut R, x1y1: PixelsXY, size: SizeInPixels) -> io::Result<()>
239where
240    R: RasterOps,
241{
242    let x2y2 = PixelsXY {
243        x: (i32::from(x1y1.x) + i32::from(size.width - 1)).clamped_into(),
244        y: (i32::from(x1y1.y) + i32::from(size.height - 1)).clamped_into(),
245    };
246    rasops.draw_line(PixelsXY { x: x1y1.x, y: x1y1.y }, PixelsXY { x: x2y2.x, y: x1y1.y })?;
247    rasops.draw_line(PixelsXY { x: x2y2.x, y: x1y1.y }, PixelsXY { x: x2y2.x, y: x2y2.y })?;
248    rasops.draw_line(PixelsXY { x: x2y2.x, y: x2y2.y }, PixelsXY { x: x1y1.x, y: x2y2.y })?;
249    rasops.draw_line(PixelsXY { x: x1y1.x, y: x2y2.y }, PixelsXY { x: x1y1.x, y: x1y1.y })?;
250    Ok(())
251}
252
253#[cfg(test)]
254mod testutils {
255    use super::*;
256    use crate::console::graphics::RasterInfo;
257    use crate::console::{SizeInPixels, RGB};
258
259    /// Representation of captured raster operations.
260    #[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
261    pub(crate) enum CapturedRasop {
262        DrawLine(i16, i16, i16, i16),
263        DrawPixel(i16, i16),
264    }
265
266    /// An implementation of `RasterOps` that captures calls for later validation.
267    #[derive(Default)]
268    pub(crate) struct RecordingRasops {
269        pub(crate) ops: Vec<CapturedRasop>,
270    }
271
272    impl RasterOps for RecordingRasops {
273        type ID = u8;
274
275        fn get_info(&self) -> RasterInfo {
276            unimplemented!();
277        }
278
279        fn set_draw_color(&mut self, _color: RGB) {
280            unimplemented!();
281        }
282
283        fn clear(&mut self) -> io::Result<()> {
284            unimplemented!();
285        }
286
287        fn set_sync(&mut self, _enabled: bool) {
288            unimplemented!();
289        }
290
291        fn present_canvas(&mut self) -> io::Result<()> {
292            unimplemented!();
293        }
294
295        fn read_pixels(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<Self::ID> {
296            unimplemented!();
297        }
298
299        fn put_pixels(&mut self, _xy: PixelsXY, _data: &Self::ID) -> io::Result<()> {
300            unimplemented!();
301        }
302
303        fn move_pixels(
304            &mut self,
305            _x1y1: PixelsXY,
306            _x2y2: PixelsXY,
307            _size: SizeInPixels,
308        ) -> io::Result<()> {
309            unimplemented!();
310        }
311
312        fn write_text(&mut self, _xy: PixelsXY, _text: &str) -> io::Result<()> {
313            unimplemented!();
314        }
315
316        fn draw_circle(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
317            unimplemented!();
318        }
319
320        fn draw_circle_filled(&mut self, _center: PixelsXY, _radius: u16) -> io::Result<()> {
321            unimplemented!();
322        }
323
324        fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
325            self.ops.push(CapturedRasop::DrawLine(x1y1.x, x1y1.y, x2y2.x, x2y2.y));
326            Ok(())
327        }
328
329        fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
330            self.ops.push(CapturedRasop::DrawPixel(xy.x, xy.y));
331            Ok(())
332        }
333
334        fn draw_rect(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<()> {
335            unimplemented!();
336        }
337
338        fn draw_rect_filled(&mut self, _xy: PixelsXY, _size: SizeInPixels) -> io::Result<()> {
339            unimplemented!();
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::testutils::*;
347    use super::*;
348
349    #[test]
350    fn test_draw_circle_zero() {
351        let mut rasops = RecordingRasops::default();
352        draw_circle(&mut rasops, PixelsXY::new(10, 20), 0).unwrap();
353        assert!(rasops.ops.is_empty());
354    }
355
356    #[test]
357    fn test_draw_circle_dot() {
358        let mut rasops = RecordingRasops::default();
359        draw_circle(&mut rasops, PixelsXY::new(10, 20), 1).unwrap();
360        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
361    }
362
363    #[test]
364    fn test_draw_circle_larger() {
365        let mut rasops = RecordingRasops::default();
366        draw_circle(&mut rasops, PixelsXY::new(10, 20), 4).unwrap();
367        rasops.ops.sort();
368        assert_eq!(
369            [
370                CapturedRasop::DrawPixel(7, 18),
371                CapturedRasop::DrawPixel(7, 19),
372                CapturedRasop::DrawPixel(7, 20),
373                CapturedRasop::DrawPixel(7, 20),
374                CapturedRasop::DrawPixel(7, 21),
375                CapturedRasop::DrawPixel(7, 22),
376                CapturedRasop::DrawPixel(8, 17),
377                CapturedRasop::DrawPixel(8, 23),
378                CapturedRasop::DrawPixel(9, 17),
379                CapturedRasop::DrawPixel(9, 23),
380                CapturedRasop::DrawPixel(10, 17),
381                CapturedRasop::DrawPixel(10, 17),
382                CapturedRasop::DrawPixel(10, 23),
383                CapturedRasop::DrawPixel(10, 23),
384                CapturedRasop::DrawPixel(11, 17),
385                CapturedRasop::DrawPixel(11, 23),
386                CapturedRasop::DrawPixel(12, 17),
387                CapturedRasop::DrawPixel(12, 23),
388                CapturedRasop::DrawPixel(13, 18),
389                CapturedRasop::DrawPixel(13, 19),
390                CapturedRasop::DrawPixel(13, 20),
391                CapturedRasop::DrawPixel(13, 20),
392                CapturedRasop::DrawPixel(13, 21),
393                CapturedRasop::DrawPixel(13, 22),
394            ],
395            rasops.ops.as_slice()
396        );
397    }
398
399    #[test]
400    fn test_draw_circle_corners() {
401        for corner in
402            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
403        {
404            let mut rasops = RecordingRasops::default();
405            draw_circle(&mut rasops, corner, 1).unwrap();
406            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
407        }
408    }
409
410    #[test]
411    fn test_draw_circle_filled_zero() {
412        let mut rasops = RecordingRasops::default();
413        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 0).unwrap();
414        assert!(rasops.ops.is_empty());
415    }
416
417    #[test]
418    fn test_draw_circle_filled_dot() {
419        let mut rasops = RecordingRasops::default();
420        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 1).unwrap();
421        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
422    }
423
424    #[test]
425    fn test_draw_circle_filled_larger() {
426        let mut rasops = RecordingRasops::default();
427        draw_circle_filled(&mut rasops, PixelsXY::new(10, 20), 4).unwrap();
428        rasops.ops.sort();
429        assert_eq!(
430            [
431                CapturedRasop::DrawLine(7, 18, 7, 22),
432                CapturedRasop::DrawLine(7, 19, 7, 21),
433                CapturedRasop::DrawLine(7, 20, 7, 20),
434                CapturedRasop::DrawLine(8, 17, 8, 23),
435                CapturedRasop::DrawLine(9, 17, 9, 23),
436                CapturedRasop::DrawLine(10, 17, 10, 23),
437                CapturedRasop::DrawLine(10, 17, 10, 23),
438                CapturedRasop::DrawLine(11, 17, 11, 23),
439                CapturedRasop::DrawLine(12, 17, 12, 23),
440                CapturedRasop::DrawLine(13, 18, 13, 22),
441                CapturedRasop::DrawLine(13, 19, 13, 21),
442                CapturedRasop::DrawLine(13, 20, 13, 20),
443            ],
444            rasops.ops.as_slice()
445        );
446    }
447
448    #[test]
449    fn test_draw_circle_filled_corners() {
450        for corner in
451            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
452        {
453            let mut rasops = RecordingRasops::default();
454            draw_circle_filled(&mut rasops, corner, 1).unwrap();
455            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
456        }
457    }
458
459    #[test]
460    fn test_draw_line_dot() {
461        let mut rasops = RecordingRasops::default();
462        draw_line(&mut rasops, PixelsXY::new(10, 20), PixelsXY::new(10, 20)).unwrap();
463        assert_eq!([CapturedRasop::DrawPixel(10, 20)], rasops.ops.as_slice());
464    }
465
466    #[test]
467    fn test_draw_line_horizontal_right() {
468        let mut rasops = RecordingRasops::default();
469        draw_line(&mut rasops, PixelsXY::new(100, 0), PixelsXY::new(105, 0)).unwrap();
470        assert_eq!(
471            [
472                CapturedRasop::DrawPixel(100, 0),
473                CapturedRasop::DrawPixel(101, 0),
474                CapturedRasop::DrawPixel(102, 0),
475                CapturedRasop::DrawPixel(103, 0),
476                CapturedRasop::DrawPixel(104, 0),
477                CapturedRasop::DrawPixel(105, 0),
478            ],
479            rasops.ops.as_slice()
480        );
481    }
482
483    #[test]
484    fn test_draw_line_horizontal_left() {
485        let mut rasops = RecordingRasops::default();
486        draw_line(&mut rasops, PixelsXY::new(105, 0), PixelsXY::new(100, 0)).unwrap();
487        assert_eq!(
488            [
489                CapturedRasop::DrawPixel(100, 0),
490                CapturedRasop::DrawPixel(101, 0),
491                CapturedRasop::DrawPixel(102, 0),
492                CapturedRasop::DrawPixel(103, 0),
493                CapturedRasop::DrawPixel(104, 0),
494                CapturedRasop::DrawPixel(105, 0),
495            ],
496            rasops.ops.as_slice()
497        );
498    }
499
500    #[test]
501    fn test_draw_line_vertical_down() {
502        let mut rasops = RecordingRasops::default();
503        draw_line(&mut rasops, PixelsXY::new(0, 100), PixelsXY::new(0, 105)).unwrap();
504        assert_eq!(
505            [
506                CapturedRasop::DrawPixel(0, 100),
507                CapturedRasop::DrawPixel(0, 101),
508                CapturedRasop::DrawPixel(0, 102),
509                CapturedRasop::DrawPixel(0, 103),
510                CapturedRasop::DrawPixel(0, 104),
511                CapturedRasop::DrawPixel(0, 105),
512            ],
513            rasops.ops.as_slice()
514        );
515    }
516
517    #[test]
518    fn test_draw_line_vertical_up() {
519        let mut rasops = RecordingRasops::default();
520        draw_line(&mut rasops, PixelsXY::new(0, 105), PixelsXY::new(0, 100)).unwrap();
521        assert_eq!(
522            [
523                CapturedRasop::DrawPixel(0, 100),
524                CapturedRasop::DrawPixel(0, 101),
525                CapturedRasop::DrawPixel(0, 102),
526                CapturedRasop::DrawPixel(0, 103),
527                CapturedRasop::DrawPixel(0, 104),
528                CapturedRasop::DrawPixel(0, 105),
529            ],
530            rasops.ops.as_slice()
531        );
532    }
533
534    #[test]
535    fn test_draw_line_horizontal_slope_right() {
536        let mut rasops = RecordingRasops::default();
537        draw_line(&mut rasops, PixelsXY::new(100, 0), PixelsXY::new(105, 3)).unwrap();
538        assert_eq!(
539            [
540                CapturedRasop::DrawPixel(100, 0),
541                CapturedRasop::DrawPixel(101, 1),
542                CapturedRasop::DrawPixel(102, 1),
543                CapturedRasop::DrawPixel(103, 2),
544                CapturedRasop::DrawPixel(104, 2),
545                CapturedRasop::DrawPixel(105, 3),
546            ],
547            rasops.ops.as_slice()
548        );
549    }
550
551    #[test]
552    fn test_draw_line_horizontal_slope_left() {
553        let mut rasops = RecordingRasops::default();
554        draw_line(&mut rasops, PixelsXY::new(105, 0), PixelsXY::new(100, 3)).unwrap();
555        assert_eq!(
556            [
557                CapturedRasop::DrawPixel(100, 3),
558                CapturedRasop::DrawPixel(101, 2),
559                CapturedRasop::DrawPixel(102, 2),
560                CapturedRasop::DrawPixel(103, 1),
561                CapturedRasop::DrawPixel(104, 1),
562                CapturedRasop::DrawPixel(105, 0),
563            ],
564            rasops.ops.as_slice()
565        );
566    }
567
568    #[test]
569    fn test_draw_line_vertical_slope_up() {
570        let mut rasops = RecordingRasops::default();
571        draw_line(&mut rasops, PixelsXY::new(0, 100), PixelsXY::new(3, 105)).unwrap();
572        assert_eq!(
573            [
574                CapturedRasop::DrawPixel(0, 100),
575                CapturedRasop::DrawPixel(1, 101),
576                CapturedRasop::DrawPixel(1, 102),
577                CapturedRasop::DrawPixel(2, 103),
578                CapturedRasop::DrawPixel(2, 104),
579                CapturedRasop::DrawPixel(3, 105),
580            ],
581            rasops.ops.as_slice()
582        );
583    }
584
585    #[test]
586    fn test_draw_line_vertical_slope_down() {
587        let mut rasops = RecordingRasops::default();
588        draw_line(&mut rasops, PixelsXY::new(0, 105), PixelsXY::new(3, 100)).unwrap();
589        assert_eq!(
590            [
591                CapturedRasop::DrawPixel(3, 100),
592                CapturedRasop::DrawPixel(2, 101),
593                CapturedRasop::DrawPixel(2, 102),
594                CapturedRasop::DrawPixel(1, 103),
595                CapturedRasop::DrawPixel(1, 104),
596                CapturedRasop::DrawPixel(0, 105),
597            ],
598            rasops.ops.as_slice()
599        );
600    }
601
602    #[test]
603    fn test_draw_line_corners() {
604        for corner in
605            [PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT, PixelsXY::BOTTOM_RIGHT]
606        {
607            let mut rasops = RecordingRasops::default();
608            draw_line(&mut rasops, corner, corner).unwrap();
609            assert_eq!([CapturedRasop::DrawPixel(corner.x, corner.y)], rasops.ops.as_slice());
610        }
611    }
612
613    #[test]
614    fn test_draw_line_perimeter() {
615        for corners in [
616            (PixelsXY::TOP_LEFT, PixelsXY::TOP_RIGHT),
617            (PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_RIGHT),
618            (PixelsXY::BOTTOM_RIGHT, PixelsXY::BOTTOM_LEFT),
619            (PixelsXY::BOTTOM_LEFT, PixelsXY::TOP_LEFT),
620        ] {
621            let mut rasops = RecordingRasops::default();
622            draw_line(&mut rasops, corners.0, corners.1).unwrap();
623            assert_eq!(usize::from(u16::MAX) + 1, rasops.ops.len());
624        }
625    }
626
627    #[test]
628    fn test_draw_line_diagonals() {
629        for corners in [
630            (PixelsXY::TOP_LEFT, PixelsXY::BOTTOM_RIGHT),
631            (PixelsXY::BOTTOM_RIGHT, PixelsXY::TOP_LEFT),
632            (PixelsXY::TOP_RIGHT, PixelsXY::BOTTOM_LEFT),
633            (PixelsXY::BOTTOM_LEFT, PixelsXY::TOP_RIGHT),
634        ] {
635            let mut rasops = RecordingRasops::default();
636            draw_line(&mut rasops, corners.0, corners.1).unwrap();
637            assert_eq!(usize::from(u16::MAX) + 1, rasops.ops.len());
638        }
639    }
640
641    #[test]
642    fn test_draw_rect_dot() {
643        let mut rasops = RecordingRasops::default();
644        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(1, 1)).unwrap();
645        assert_eq!(
646            [
647                CapturedRasop::DrawLine(10, 20, 10, 20),
648                CapturedRasop::DrawLine(10, 20, 10, 20),
649                CapturedRasop::DrawLine(10, 20, 10, 20),
650                CapturedRasop::DrawLine(10, 20, 10, 20),
651            ],
652            rasops.ops.as_slice()
653        );
654    }
655
656    #[test]
657    fn test_draw_rect_horizontal_line() {
658        let mut rasops = RecordingRasops::default();
659        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(6, 1)).unwrap();
660        assert_eq!(
661            [
662                CapturedRasop::DrawLine(10, 20, 15, 20),
663                CapturedRasop::DrawLine(15, 20, 15, 20),
664                CapturedRasop::DrawLine(15, 20, 10, 20),
665                CapturedRasop::DrawLine(10, 20, 10, 20),
666            ],
667            rasops.ops.as_slice()
668        );
669    }
670
671    #[test]
672    fn test_draw_rect_vertical_line() {
673        let mut rasops = RecordingRasops::default();
674        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(1, 6)).unwrap();
675        assert_eq!(
676            [
677                CapturedRasop::DrawLine(10, 20, 10, 20),
678                CapturedRasop::DrawLine(10, 20, 10, 25),
679                CapturedRasop::DrawLine(10, 25, 10, 25),
680                CapturedRasop::DrawLine(10, 25, 10, 20),
681            ],
682            rasops.ops.as_slice()
683        );
684    }
685
686    #[test]
687    fn test_draw_rect_2d() {
688        let mut rasops = RecordingRasops::default();
689        draw_rect(&mut rasops, PixelsXY::new(10, 20), SizeInPixels::new(6, 7)).unwrap();
690        assert_eq!(
691            [
692                CapturedRasop::DrawLine(10, 20, 15, 20),
693                CapturedRasop::DrawLine(15, 20, 15, 26),
694                CapturedRasop::DrawLine(15, 26, 10, 26),
695                CapturedRasop::DrawLine(10, 26, 10, 20),
696            ],
697            rasops.ops.as_slice()
698        );
699    }
700
701    #[test]
702    fn test_draw_rect_corners() {
703        let mut rasops = RecordingRasops::default();
704        draw_rect(&mut rasops, PixelsXY::TOP_LEFT, SizeInPixels::MAX).unwrap();
705        assert_eq!(
706            [
707                CapturedRasop::DrawLine(i16::MIN, i16::MIN, i16::MAX - 1, i16::MIN),
708                CapturedRasop::DrawLine(i16::MAX - 1, i16::MIN, i16::MAX - 1, i16::MAX - 1),
709                CapturedRasop::DrawLine(i16::MAX - 1, i16::MAX - 1, i16::MIN, i16::MAX - 1),
710                CapturedRasop::DrawLine(i16::MIN, i16::MAX - 1, i16::MIN, i16::MIN),
711            ],
712            rasops.ops.as_slice()
713        );
714    }
715}