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