| 1 | use crate::{ |
| 2 | draw_target::DrawTarget, |
| 3 | geometry::{Dimensions, Point, PointExt}, |
| 4 | pixelcolor::PixelColor, |
| 5 | primitives::{ |
| 6 | circle::{points::Scanlines, Circle}, |
| 7 | common::{Scanline, StyledScanline}, |
| 8 | rectangle::Rectangle, |
| 9 | styled::{StyledDimensions, StyledDrawable, StyledPixels}, |
| 10 | PrimitiveStyle, |
| 11 | }, |
| 12 | Pixel, |
| 13 | }; |
| 14 | use az::SaturatingAs; |
| 15 | |
| 16 | /// Pixel iterator for each pixel in the circle border |
| 17 | #[derive (Clone, Eq, PartialEq, Hash, Debug)] |
| 18 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
| 19 | pub struct StyledPixelsIterator<C> { |
| 20 | styled_scanlines: StyledScanlines, |
| 21 | |
| 22 | stroke_left: Scanline, |
| 23 | fill: Scanline, |
| 24 | stroke_right: Scanline, |
| 25 | |
| 26 | stroke_color: Option<C>, |
| 27 | fill_color: Option<C>, |
| 28 | } |
| 29 | |
| 30 | impl<C: PixelColor> StyledPixelsIterator<C> { |
| 31 | pub(in crate::primitives) fn new(primitive: &Circle, style: &PrimitiveStyle<C>) -> Self { |
| 32 | let stroke_area: Circle = style.stroke_area(primitive); |
| 33 | let fill_area: Circle = style.fill_area(primitive); |
| 34 | |
| 35 | Self { |
| 36 | styled_scanlines: StyledScanlines::new(&stroke_area, &fill_area), |
| 37 | stroke_left: Scanline::new_empty(0), |
| 38 | fill: Scanline::new_empty(0), |
| 39 | stroke_right: Scanline::new_empty(0), |
| 40 | stroke_color: style.stroke_color, |
| 41 | fill_color: style.fill_color, |
| 42 | } |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | impl<C> Iterator for StyledPixelsIterator<C> |
| 47 | where |
| 48 | C: PixelColor, |
| 49 | { |
| 50 | type Item = Pixel<C>; |
| 51 | |
| 52 | fn next(&mut self) -> Option<Self::Item> { |
| 53 | match (self.stroke_color, self.fill_color) { |
| 54 | (Some(stroke_color), None) => loop { |
| 55 | if let Some(pixel) = self |
| 56 | .stroke_left |
| 57 | .next() |
| 58 | .or_else(|| self.stroke_right.next()) |
| 59 | .map(|p| Pixel(p, stroke_color)) |
| 60 | { |
| 61 | return Some(pixel); |
| 62 | } |
| 63 | |
| 64 | let scanline = self.styled_scanlines.next()?; |
| 65 | self.stroke_left = scanline.stroke_left(); |
| 66 | self.stroke_right = scanline.stroke_right(); |
| 67 | }, |
| 68 | (Some(stroke_color), Some(fill_color)) => loop { |
| 69 | if let Some(pixel) = self |
| 70 | .stroke_left |
| 71 | .next() |
| 72 | .map(|p| Pixel(p, stroke_color)) |
| 73 | .or_else(|| self.fill.next().map(|p| Pixel(p, fill_color))) |
| 74 | .or_else(|| self.stroke_right.next().map(|p| Pixel(p, stroke_color))) |
| 75 | { |
| 76 | return Some(pixel); |
| 77 | } |
| 78 | |
| 79 | let scanline = self.styled_scanlines.next()?; |
| 80 | self.stroke_left = scanline.stroke_left(); |
| 81 | self.fill = scanline.fill(); |
| 82 | self.stroke_right = scanline.stroke_right(); |
| 83 | }, |
| 84 | (None, Some(fill_color)) => loop { |
| 85 | if let Some(pixel) = self.fill.next().map(|p| Pixel(p, fill_color)) { |
| 86 | return Some(pixel); |
| 87 | } |
| 88 | |
| 89 | let scanline = self.styled_scanlines.next()?; |
| 90 | self.fill = scanline.fill(); |
| 91 | }, |
| 92 | (None, None) => None, |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | impl<C: PixelColor> StyledPixels<PrimitiveStyle<C>> for Circle { |
| 98 | type Iter = StyledPixelsIterator<C>; |
| 99 | |
| 100 | fn pixels(&self, style: &PrimitiveStyle<C>) -> Self::Iter { |
| 101 | StyledPixelsIterator::new(self, style) |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | impl<C: PixelColor> StyledDrawable<PrimitiveStyle<C>> for Circle { |
| 106 | type Color = C; |
| 107 | type Output = (); |
| 108 | |
| 109 | fn draw_styled<D>( |
| 110 | &self, |
| 111 | style: &PrimitiveStyle<C>, |
| 112 | target: &mut D, |
| 113 | ) -> Result<Self::Output, D::Error> |
| 114 | where |
| 115 | D: DrawTarget<Color = C>, |
| 116 | { |
| 117 | match (style.effective_stroke_color(), style.fill_color) { |
| 118 | (Some(stroke_color), None) => { |
| 119 | for scanline in |
| 120 | StyledScanlines::new(&style.stroke_area(self), &style.fill_area(self)) |
| 121 | { |
| 122 | scanline.draw_stroke(target, stroke_color)?; |
| 123 | } |
| 124 | } |
| 125 | (Some(stroke_color), Some(fill_color)) => { |
| 126 | for scanline in |
| 127 | StyledScanlines::new(&style.stroke_area(self), &style.fill_area(self)) |
| 128 | { |
| 129 | scanline.draw_stroke_and_fill(target, stroke_color, fill_color)?; |
| 130 | } |
| 131 | } |
| 132 | (None, Some(fill_color)) => { |
| 133 | for scanline in Scanlines::new(&style.fill_area(self)) { |
| 134 | scanline.draw(target, fill_color)?; |
| 135 | } |
| 136 | } |
| 137 | (None, None) => {} |
| 138 | } |
| 139 | |
| 140 | Ok(()) |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for Circle { |
| 145 | fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle { |
| 146 | let offset: i32 = style.outside_stroke_width().saturating_as(); |
| 147 | |
| 148 | self.bounding_box().offset(offset) |
| 149 | } |
| 150 | } |
| 151 | #[derive (Clone, Eq, PartialEq, Hash, Debug)] |
| 152 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
| 153 | struct StyledScanlines { |
| 154 | scanlines: Scanlines, |
| 155 | fill_threshold: u32, |
| 156 | } |
| 157 | |
| 158 | impl StyledScanlines { |
| 159 | pub fn new(stroke_area: &Circle, fill_area: &Circle) -> Self { |
| 160 | Self { |
| 161 | scanlines: Scanlines::new(circle:stroke_area), |
| 162 | fill_threshold: fill_area.threshold(), |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | impl Iterator for StyledScanlines { |
| 168 | type Item = StyledScanline; |
| 169 | |
| 170 | fn next(&mut self) -> Option<Self::Item> { |
| 171 | self.scanlines.next().map(|scanline: Scanline| { |
| 172 | let fill_range: Option> = scanlineOption |
| 173 | .x |
| 174 | .clone() |
| 175 | .find(|x: &i32| { |
| 176 | let delta: Point = Point::new(*x, scanline.y) * 2 - self.scanlines.center_2x; |
| 177 | (delta.length_squared() as u32) < self.fill_threshold |
| 178 | }) |
| 179 | .map(|x: i32| x..scanline.x.end - (x - scanline.x.start)); |
| 180 | |
| 181 | StyledScanline::new(scanline.y, stroke_range:scanline.x, fill_range) |
| 182 | }) |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | #[cfg (test)] |
| 187 | mod tests { |
| 188 | use super::*; |
| 189 | use crate::{ |
| 190 | geometry::{Dimensions, Point}, |
| 191 | iterator::PixelIteratorExt, |
| 192 | mock_display::MockDisplay, |
| 193 | pixelcolor::BinaryColor, |
| 194 | primitives::{ |
| 195 | OffsetOutline, PointsIter, Primitive, PrimitiveStyleBuilder, StrokeAlignment, Styled, |
| 196 | }, |
| 197 | Drawable, |
| 198 | }; |
| 199 | |
| 200 | /// Draws a styled circle by only using the points iterator. |
| 201 | fn draw_circle( |
| 202 | diameter: u32, |
| 203 | stroke_color: Option<BinaryColor>, |
| 204 | stroke_width: u32, |
| 205 | fill_color: Option<BinaryColor>, |
| 206 | ) -> MockDisplay<BinaryColor> { |
| 207 | let circle = Circle::with_center(Point::new_equal(10), diameter); |
| 208 | |
| 209 | let mut display = MockDisplay::new(); |
| 210 | display.set_pixels(circle.points(), stroke_color); |
| 211 | display.set_pixels( |
| 212 | circle.offset(-stroke_width.saturating_as::<i32>()).points(), |
| 213 | fill_color, |
| 214 | ); |
| 215 | |
| 216 | display |
| 217 | } |
| 218 | |
| 219 | #[test ] |
| 220 | fn fill() { |
| 221 | for diameter in 5..=6 { |
| 222 | let expected = draw_circle(diameter, None, 0, Some(BinaryColor::On)); |
| 223 | |
| 224 | let circle = Circle::with_center(Point::new_equal(10), diameter) |
| 225 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)); |
| 226 | |
| 227 | let mut drawable = MockDisplay::new(); |
| 228 | circle.draw(&mut drawable).unwrap(); |
| 229 | drawable.assert_eq_with_message(&expected, |f| write!(f, "diameter = {}" , diameter)); |
| 230 | |
| 231 | let mut pixels = MockDisplay::new(); |
| 232 | circle.pixels().draw(&mut pixels).unwrap(); |
| 233 | pixels.assert_eq_with_message(&expected, |f| write!(f, "diameter = {}" , diameter)); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | #[test ] |
| 238 | fn stroke() { |
| 239 | for (diameter, stroke_width) in [(5, 2), (5, 3), (6, 2), (6, 3)].iter().copied() { |
| 240 | let expected = draw_circle(diameter, Some(BinaryColor::On), stroke_width, None); |
| 241 | |
| 242 | let style = PrimitiveStyleBuilder::new() |
| 243 | .stroke_color(BinaryColor::On) |
| 244 | .stroke_width(stroke_width) |
| 245 | .stroke_alignment(StrokeAlignment::Inside) |
| 246 | .build(); |
| 247 | |
| 248 | let circle = Circle::with_center(Point::new_equal(10), diameter).into_styled(style); |
| 249 | |
| 250 | let mut drawable = MockDisplay::new(); |
| 251 | circle.draw(&mut drawable).unwrap(); |
| 252 | drawable.assert_eq_with_message(&expected, |f| { |
| 253 | write!( |
| 254 | f, |
| 255 | "diameter = {}, stroke_width = {}" , |
| 256 | diameter, stroke_width |
| 257 | ) |
| 258 | }); |
| 259 | |
| 260 | let mut pixels = MockDisplay::new(); |
| 261 | circle.pixels().draw(&mut pixels).unwrap(); |
| 262 | pixels.assert_eq_with_message(&expected, |f| { |
| 263 | write!( |
| 264 | f, |
| 265 | "diameter = {}, stroke_width = {}" , |
| 266 | diameter, stroke_width |
| 267 | ) |
| 268 | }); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | #[test ] |
| 273 | fn stroke_and_fill() { |
| 274 | for (diameter, stroke_width) in [(5, 2), (5, 3), (6, 2), (6, 3)].iter().copied() { |
| 275 | let expected = draw_circle( |
| 276 | diameter, |
| 277 | Some(BinaryColor::On), |
| 278 | stroke_width, |
| 279 | Some(BinaryColor::Off), |
| 280 | ); |
| 281 | |
| 282 | let style = PrimitiveStyleBuilder::new() |
| 283 | .fill_color(BinaryColor::Off) |
| 284 | .stroke_color(BinaryColor::On) |
| 285 | .stroke_width(stroke_width) |
| 286 | .stroke_alignment(StrokeAlignment::Inside) |
| 287 | .build(); |
| 288 | |
| 289 | let circle = Circle::with_center(Point::new_equal(10), diameter).into_styled(style); |
| 290 | |
| 291 | let mut drawable = MockDisplay::new(); |
| 292 | circle.draw(&mut drawable).unwrap(); |
| 293 | drawable.assert_eq_with_message(&expected, |f| { |
| 294 | write!( |
| 295 | f, |
| 296 | "diameter = {}, stroke_width = {}" , |
| 297 | diameter, stroke_width |
| 298 | ) |
| 299 | }); |
| 300 | |
| 301 | let mut pixels = MockDisplay::new(); |
| 302 | circle.pixels().draw(&mut pixels).unwrap(); |
| 303 | pixels.assert_eq_with_message(&expected, |f| { |
| 304 | write!( |
| 305 | f, |
| 306 | "diameter = {}, stroke_width = {}" , |
| 307 | diameter, stroke_width |
| 308 | ) |
| 309 | }); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | #[test ] |
| 314 | fn filled_styled_points_matches_points() { |
| 315 | let circle = Circle::with_center(Point::new(10, 10), 5); |
| 316 | |
| 317 | let styled_points = circle |
| 318 | .clone() |
| 319 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) |
| 320 | .pixels() |
| 321 | .map(|Pixel(p, _)| p); |
| 322 | |
| 323 | assert!(circle.points().eq(styled_points)); |
| 324 | } |
| 325 | |
| 326 | // Check that tiny circles render as a "+" shape with a hole in the center |
| 327 | #[test ] |
| 328 | fn tiny_circle() { |
| 329 | let mut display = MockDisplay::new(); |
| 330 | |
| 331 | Circle::new(Point::new(0, 0), 3) |
| 332 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) |
| 333 | .draw(&mut display) |
| 334 | .unwrap(); |
| 335 | |
| 336 | display.assert_pattern(&[ |
| 337 | " # " , // |
| 338 | "# #" , // |
| 339 | " # " , // |
| 340 | ]); |
| 341 | } |
| 342 | |
| 343 | // Check that tiny filled circle render as a "+" shape with NO hole in the center |
| 344 | #[test ] |
| 345 | fn tiny_circle_filled() { |
| 346 | let mut display = MockDisplay::new(); |
| 347 | |
| 348 | Circle::new(Point::new(0, 0), 3) |
| 349 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) |
| 350 | .draw(&mut display) |
| 351 | .unwrap(); |
| 352 | |
| 353 | display.assert_pattern(&[ |
| 354 | " # " , // |
| 355 | "###" , // |
| 356 | " # " , // |
| 357 | ]); |
| 358 | } |
| 359 | |
| 360 | #[test ] |
| 361 | fn transparent_border() { |
| 362 | let circle: Styled<Circle, PrimitiveStyle<BinaryColor>> = |
| 363 | Circle::new(Point::new(-5, -5), 21) |
| 364 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)); |
| 365 | |
| 366 | assert!(circle.pixels().count() > 0); |
| 367 | } |
| 368 | |
| 369 | #[test ] |
| 370 | fn it_handles_negative_coordinates() { |
| 371 | let positive = Circle::new(Point::new(10, 10), 5) |
| 372 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) |
| 373 | .pixels(); |
| 374 | |
| 375 | let negative = Circle::new(Point::new(-10, -10), 5) |
| 376 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) |
| 377 | .pixels(); |
| 378 | |
| 379 | assert!(negative.eq(positive.map(|Pixel(p, c)| Pixel(p - Point::new(20, 20), c)))); |
| 380 | } |
| 381 | |
| 382 | #[test ] |
| 383 | fn stroke_alignment() { |
| 384 | const CENTER: Point = Point::new(15, 15); |
| 385 | const SIZE: u32 = 10; |
| 386 | |
| 387 | let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3); |
| 388 | |
| 389 | let mut display_center = MockDisplay::new(); |
| 390 | Circle::with_center(CENTER, SIZE) |
| 391 | .into_styled(style) |
| 392 | .draw(&mut display_center) |
| 393 | .unwrap(); |
| 394 | |
| 395 | let mut display_inside = MockDisplay::new(); |
| 396 | Circle::with_center(CENTER, SIZE + 2) |
| 397 | .into_styled( |
| 398 | PrimitiveStyleBuilder::from(&style) |
| 399 | .stroke_alignment(StrokeAlignment::Inside) |
| 400 | .build(), |
| 401 | ) |
| 402 | .draw(&mut display_inside) |
| 403 | .unwrap(); |
| 404 | |
| 405 | let mut display_outside = MockDisplay::new(); |
| 406 | Circle::with_center(CENTER, SIZE - 4) |
| 407 | .into_styled( |
| 408 | PrimitiveStyleBuilder::from(&style) |
| 409 | .stroke_alignment(StrokeAlignment::Outside) |
| 410 | .build(), |
| 411 | ) |
| 412 | .draw(&mut display_outside) |
| 413 | .unwrap(); |
| 414 | |
| 415 | display_inside.assert_eq(&display_center); |
| 416 | display_outside.assert_eq(&display_center); |
| 417 | } |
| 418 | |
| 419 | /// Test for issue #143 |
| 420 | #[test ] |
| 421 | fn issue_143_stroke_and_fill() { |
| 422 | for size in 0..10 { |
| 423 | let circle_no_stroke: Styled<Circle, PrimitiveStyle<BinaryColor>> = |
| 424 | Circle::new(Point::new(10, 16), size) |
| 425 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)); |
| 426 | |
| 427 | let style = PrimitiveStyleBuilder::new() |
| 428 | .fill_color(BinaryColor::On) |
| 429 | .stroke_color(BinaryColor::On) |
| 430 | .stroke_width(1) |
| 431 | .build(); |
| 432 | let circle_stroke: Styled<Circle, PrimitiveStyle<BinaryColor>> = |
| 433 | Circle::new(Point::new(10, 16), size).into_styled(style); |
| 434 | |
| 435 | assert_eq!( |
| 436 | circle_stroke.bounding_box(), |
| 437 | circle_no_stroke.bounding_box(), |
| 438 | "Filled and unfilled circle bounding boxes are unequal for radius {}" , |
| 439 | size |
| 440 | ); |
| 441 | assert!( |
| 442 | circle_no_stroke.pixels().eq(circle_stroke.pixels()), |
| 443 | "Filled and unfilled circle iters are unequal for radius {}" , |
| 444 | size |
| 445 | ); |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | #[test ] |
| 450 | fn bounding_boxes() { |
| 451 | const CENTER: Point = Point::new(15, 15); |
| 452 | const SIZE: u32 = 10; |
| 453 | |
| 454 | let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3); |
| 455 | |
| 456 | let center = Circle::with_center(CENTER, SIZE).into_styled(style); |
| 457 | |
| 458 | let inside = Circle::with_center(CENTER, SIZE).into_styled( |
| 459 | PrimitiveStyleBuilder::from(&style) |
| 460 | .stroke_alignment(StrokeAlignment::Inside) |
| 461 | .build(), |
| 462 | ); |
| 463 | |
| 464 | let outside = Circle::with_center(CENTER, SIZE).into_styled( |
| 465 | PrimitiveStyleBuilder::from(&style) |
| 466 | .stroke_alignment(StrokeAlignment::Outside) |
| 467 | .build(), |
| 468 | ); |
| 469 | |
| 470 | let mut display = MockDisplay::new(); |
| 471 | center.draw(&mut display).unwrap(); |
| 472 | assert_eq!(display.affected_area(), center.bounding_box()); |
| 473 | |
| 474 | let mut display = MockDisplay::new(); |
| 475 | outside.draw(&mut display).unwrap(); |
| 476 | assert_eq!(display.affected_area(), outside.bounding_box()); |
| 477 | |
| 478 | let mut display = MockDisplay::new(); |
| 479 | inside.draw(&mut display).unwrap(); |
| 480 | assert_eq!(display.affected_area(), inside.bounding_box()); |
| 481 | } |
| 482 | |
| 483 | #[test ] |
| 484 | fn bounding_box_is_independent_of_colors() { |
| 485 | let transparent_circle = |
| 486 | Circle::new(Point::new(5, 5), 11).into_styled(PrimitiveStyle::<BinaryColor>::new()); |
| 487 | let filled_circle = Circle::new(Point::new(5, 5), 11) |
| 488 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)); |
| 489 | |
| 490 | assert_eq!( |
| 491 | transparent_circle.bounding_box(), |
| 492 | filled_circle.bounding_box(), |
| 493 | ); |
| 494 | } |
| 495 | } |
| 496 | |