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}
476