embedded_graphics/primitives/line/
mod.rs

1//! The line primitive
2
3use crate::{
4    geometry::{Dimensions, Point},
5    primitives::{
6        common::StrokeOffset,
7        line::thick_points::{ParallelLineType, ParallelsIterator},
8        PointsIter, Primitive, Rectangle,
9    },
10    transform::Transform,
11};
12use az::SaturatingAs;
13
14mod bresenham;
15pub(in crate::primitives) mod intersection_params;
16mod points;
17mod styled;
18mod thick_points;
19
20pub use points::Points;
21pub use styled::StyledPixelsIterator;
22
23/// Line primitive
24///
25/// # Examples
26///
27/// ## Create some lines with different styles
28///
29/// ```rust
30/// use embedded_graphics::{
31///     pixelcolor::Rgb565, prelude::*, primitives::{Line, PrimitiveStyle},
32/// };
33/// # use embedded_graphics::mock_display::MockDisplay;
34/// # let mut display = MockDisplay::default();
35///
36/// // Red 1 pixel wide line from (50, 20) to (60, 35)
37/// Line::new(Point::new(50, 20), Point::new(60, 35))
38///     .into_styled(PrimitiveStyle::with_stroke(Rgb565::RED, 1))
39///     .draw(&mut display)?;
40///
41/// // Green 10 pixel wide line with translation applied
42/// Line::new(Point::new(50, 20), Point::new(60, 35))
43///     .translate(Point::new(-30, 10))
44///     .into_styled(PrimitiveStyle::with_stroke(Rgb565::GREEN, 10))
45///     .draw(&mut display)?;
46/// # Ok::<(), core::convert::Infallible>(())
47/// ```
48#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
49#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
50pub struct Line {
51    /// Start point
52    pub start: Point,
53
54    /// End point
55    pub end: Point,
56}
57
58impl Primitive for Line {}
59
60impl PointsIter for Line {
61    type Iter = Points;
62
63    fn points(&self) -> Self::Iter {
64        Points::new(self)
65    }
66}
67
68impl Dimensions for Line {
69    fn bounding_box(&self) -> Rectangle {
70        Rectangle::with_corners(self.start, self.end)
71    }
72}
73
74impl Line {
75    /// Creates a line between two points.
76    pub const fn new(start: Point, end: Point) -> Self {
77        Self { start, end }
78    }
79
80    /// Creates a line with a start point and a delta vector.
81    ///
82    /// # Examples
83    /// ```
84    /// use embedded_graphics::{prelude::*, primitives::Line};
85    ///
86    /// let line = Line::with_delta(Point::new(10, 20), Point::new(20, -20));
87    /// # assert_eq!(line, Line::new(Point::new(10, 20), Point::new(30, 0)));
88    /// ```
89    pub const fn with_delta(start: Point, delta: Point) -> Self {
90        // Add coordinates manually because `start + delta` isn't const.
91        let end = Point::new(start.x + delta.x, start.y + delta.y);
92
93        Self { start, end }
94    }
95
96    /// Returns a perpendicular line.
97    ///
98    /// The returned line is rotated 90 degree counter clockwise and shares the start point with the
99    /// original line.
100    fn perpendicular(&self) -> Self {
101        let delta = self.end - self.start;
102        let delta = Point::new(delta.y, -delta.x);
103
104        Line::new(self.start, self.start + delta)
105    }
106
107    /// Get two lines representing the left and right edges of the thick line.
108    ///
109    /// If a thickness of `0` is given, the lines returned will lie on the same points as `self`.
110    pub(in crate::primitives) fn extents(
111        &self,
112        thickness: u32,
113        stroke_offset: StrokeOffset,
114    ) -> (Line, Line) {
115        let mut it = ParallelsIterator::new(self, thickness.saturating_as(), stroke_offset);
116        let reduce =
117            it.parallel_parameters.position_step.major + it.parallel_parameters.position_step.minor;
118
119        let mut left = (self.start, ParallelLineType::Normal);
120        let mut right = (self.start, ParallelLineType::Normal);
121
122        match stroke_offset {
123            #[allow(clippy::while_let_loop)]
124            StrokeOffset::None => loop {
125                if let Some((bresenham, reduce)) = it.next() {
126                    right = (bresenham.point, reduce);
127                } else {
128                    break;
129                }
130
131                if let Some((bresenham, reduce)) = it.next() {
132                    left = (bresenham.point, reduce);
133                } else {
134                    break;
135                }
136            },
137            StrokeOffset::Left => {
138                if let Some((bresenham, reduce)) = it.last() {
139                    left = (bresenham.point, reduce);
140                }
141            }
142            StrokeOffset::Right => {
143                if let Some((bresenham, reduce)) = it.last() {
144                    right = (bresenham.point, reduce);
145                }
146            }
147        };
148
149        let left_start = left.0;
150        let right_start = right.0;
151
152        let delta = self.end - self.start;
153
154        let left_line = Line::new(
155            left_start,
156            left_start + delta
157                - match left.1 {
158                    ParallelLineType::Normal => Point::zero(),
159                    ParallelLineType::Extra => reduce,
160                },
161        );
162
163        let right_line = Line::new(
164            right_start,
165            right_start + delta
166                - match right.1 {
167                    ParallelLineType::Normal => Point::zero(),
168                    ParallelLineType::Extra => reduce,
169                },
170        );
171        (left_line, right_line)
172    }
173
174    /// Compute the midpoint of the line.
175    pub fn midpoint(&self) -> Point {
176        self.start + (self.end - self.start) / 2
177    }
178
179    /// Compute the delta (`end - start`) of the line.
180    pub fn delta(&self) -> Point {
181        self.end - self.start
182    }
183}
184
185impl Transform for Line {
186    /// Translate the line from its current position to a new position by (x, y) pixels, returning
187    /// a new `Line`. For a mutating transform, see `translate_mut`.
188    ///
189    /// ```
190    /// # use embedded_graphics::primitives::Line;
191    /// # use embedded_graphics::prelude::*;
192    /// let line = Line::new(Point::new(5, 10), Point::new(15, 20));
193    /// let moved = line.translate(Point::new(10, 10));
194    ///
195    /// assert_eq!(moved.start, Point::new(15, 20));
196    /// assert_eq!(moved.end, Point::new(25, 30));
197    /// ```
198    fn translate(&self, by: Point) -> Self {
199        Self {
200            start: self.start + by,
201            end: self.end + by,
202        }
203    }
204
205    /// Translate the line from its current position to a new position by (x, y) pixels.
206    ///
207    /// ```
208    /// # use embedded_graphics::primitives::Line;
209    /// # use embedded_graphics::prelude::*;
210    /// let mut line = Line::new(Point::new(5, 10), Point::new(15, 20));
211    /// line.translate_mut(Point::new(10, 10));
212    ///
213    /// assert_eq!(line.start, Point::new(15, 20));
214    /// assert_eq!(line.end, Point::new(25, 30));
215    /// ```
216    fn translate_mut(&mut self, by: Point) -> &mut Self {
217        self.start += by;
218        self.end += by;
219
220        self
221    }
222}
223
224/// Pixel iterator for each pixel in the line
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::{
229        geometry::Size, mock_display::MockDisplay, pixelcolor::BinaryColor,
230        primitives::PrimitiveStyle, Drawable, Pixel,
231    };
232    use arrayvec::ArrayVec;
233
234    #[test]
235    fn bounding_box() {
236        let start = Point::new(10, 10);
237        let end = Point::new(19, 29);
238
239        let line: Line = Line::new(start, end);
240        let backwards_line: Line = Line::new(end, start);
241
242        assert_eq!(
243            line.bounding_box(),
244            Rectangle::new(start, Size::new(10, 20))
245        );
246        assert_eq!(
247            backwards_line.bounding_box(),
248            Rectangle::new(start, Size::new(10, 20))
249        );
250    }
251
252    #[test]
253    fn no_stroke_width_no_line() {
254        let start = Point::new(2, 3);
255        let end = Point::new(3, 2);
256
257        let line =
258            Line::new(start, end).into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 0));
259
260        assert!(line.pixels().eq(core::iter::empty()));
261    }
262
263    #[test]
264    fn thick_line_octant_1() {
265        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
266
267        Line::new(Point::new(2, 2), Point::new(20, 8))
268            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 5))
269            .draw(&mut display)
270            .unwrap();
271
272        display.assert_pattern(&[
273            "   #                   ",
274            "  #####                ",
275            "  ########             ",
276            "  ###########          ",
277            " ###############       ",
278            "    ###############    ",
279            "       ############### ",
280            "          ###########  ",
281            "             ########  ",
282            "                #####  ",
283            "                   #   ",
284        ]);
285    }
286
287    #[test]
288    fn thick_line_2px() {
289        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
290
291        // Horizontal line
292        Line::new(Point::new(2, 2), Point::new(10, 2))
293            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 2))
294            .draw(&mut display)
295            .unwrap();
296
297        // Vertical line
298        Line::new(Point::new(2, 5), Point::new(2, 10))
299            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::Off, 2))
300            .draw(&mut display)
301            .unwrap();
302
303        display.assert_pattern(&[
304            "            ",
305            "  ######### ",
306            "  ######### ",
307            "            ",
308            "            ",
309            "  ..        ",
310            "  ..        ",
311            "  ..        ",
312            "  ..        ",
313            "  ..        ",
314            "  ..        ",
315        ]);
316    }
317
318    // Check that 45 degree lines don't draw their right side 1px too long
319    #[test]
320    fn diagonal() {
321        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
322
323        Line::new(Point::new(3, 2), Point::new(10, 9))
324            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 7))
325            .draw(&mut display)
326            .unwrap();
327
328        display.assert_pattern(&[
329            "     #        ",
330            "    ###       ",
331            "   #####      ",
332            "  #######     ",
333            " #########    ",
334            "  #########   ",
335            "   #########  ",
336            "    ######### ",
337            "     #######  ",
338            "      #####   ",
339            "       ###    ",
340            "        #     ",
341        ]);
342    }
343
344    #[test]
345    fn thick_line_3px() {
346        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
347
348        // Horizontal line
349        Line::new(Point::new(2, 2), Point::new(10, 2))
350            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 3))
351            .draw(&mut display)
352            .unwrap();
353
354        // Vertical line
355        Line::new(Point::new(2, 5), Point::new(2, 10))
356            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::Off, 3))
357            .draw(&mut display)
358            .unwrap();
359
360        display.assert_pattern(&[
361            "            ",
362            "  ######### ",
363            "  ######### ",
364            "  ######### ",
365            "            ",
366            " ...        ",
367            " ...        ",
368            " ...        ",
369            " ...        ",
370            " ...        ",
371            " ...        ",
372        ]);
373    }
374
375    #[test]
376    fn thick_line_0px() {
377        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
378
379        Line::new(Point::new(2, 2), Point::new(2, 2))
380            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 3))
381            .draw(&mut display)
382            .unwrap();
383
384        display.assert_pattern(&[
385            "   ", //
386            "  #", //
387            "  #", //
388            "  #", //
389        ]);
390    }
391
392    #[test]
393    fn event_width_offset() {
394        let mut display: MockDisplay<BinaryColor> = MockDisplay::new();
395
396        // Horizontal line
397        Line::new(Point::new(2, 3), Point::new(10, 3))
398            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 4))
399            .draw(&mut display)
400            .unwrap();
401
402        // Vertical line
403        Line::new(Point::new(2, 9), Point::new(10, 8))
404            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 4))
405            .draw(&mut display)
406            .unwrap();
407
408        display.assert_pattern(&[
409            "            ",
410            "  ######### ",
411            "  ######### ",
412            "  ######### ",
413            "  ######### ",
414            "            ",
415            "       #### ",
416            "  ######### ",
417            "  ######### ",
418            "  ######### ",
419            "  #####     ",
420        ]);
421    }
422
423    #[test]
424    fn points_iter() {
425        let line = Line::new(Point::new(10, 10), Point::new(20, 30));
426
427        let styled_points: ArrayVec<_, 32> = line
428            .clone()
429            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
430            .pixels()
431            .map(|Pixel(p, _)| p)
432            .collect();
433
434        let points: ArrayVec<_, 32> = line.points().collect();
435
436        assert_eq!(points, styled_points);
437    }
438
439    #[test]
440    fn perpendicular() {
441        assert_eq!(
442            Line::new(Point::zero(), Point::new(10, 0)).perpendicular(),
443            Line::new(Point::zero(), Point::new(0, -10))
444        );
445
446        assert_eq!(
447            Line::new(Point::new(10, 20), Point::new(20, 10)).perpendicular(),
448            Line::new(Point::new(10, 20), Point::new(0, 10))
449        );
450
451        assert_eq!(
452            Line::new(Point::zero(), Point::new(0, -10)).perpendicular(),
453            Line::new(Point::zero(), Point::new(-10, 0))
454        );
455    }
456
457    #[test]
458    fn extents() {
459        let line = Line::new(Point::new(10, 50), Point::new(10, 0));
460        let (l, r) = line.extents(11, StrokeOffset::None);
461
462        assert_eq!(l, line.translate(Point::new(-5, 0)));
463        assert_eq!(r, line.translate(Point::new(5, 0)));
464    }
465
466    #[test]
467    fn extents_zero_thickness() {
468        let line = Line::new(Point::new(10, 20), Point::new(20, 10));
469
470        let (l, r) = line.extents(0, StrokeOffset::None);
471
472        assert_eq!(l, line);
473        assert_eq!(r, line);
474    }
475}