1 | //! The line primitive |
2 | |
3 | use 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 | }; |
12 | use az::SaturatingAs; |
13 | |
14 | mod bresenham; |
15 | pub(in crate::primitives) mod intersection_params; |
16 | mod points; |
17 | mod styled; |
18 | mod thick_points; |
19 | |
20 | pub use points::Points; |
21 | pub 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))] |
50 | pub struct Line { |
51 | /// Start point |
52 | pub start: Point, |
53 | |
54 | /// End point |
55 | pub end: Point, |
56 | } |
57 | |
58 | impl Primitive for Line {} |
59 | |
60 | impl PointsIter for Line { |
61 | type Iter = Points; |
62 | |
63 | fn points(&self) -> Self::Iter { |
64 | Points::new(self) |
65 | } |
66 | } |
67 | |
68 | impl Dimensions for Line { |
69 | fn bounding_box(&self) -> Rectangle { |
70 | Rectangle::with_corners(self.start, self.end) |
71 | } |
72 | } |
73 | |
74 | impl 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 | |
185 | impl 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)] |
226 | mod 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 | |