1use crate::{
2 draw_target::DrawTarget,
3 geometry::angle_consts::ANGLE_90DEG,
4 geometry::{Angle, Dimensions},
5 pixelcolor::PixelColor,
6 primitives::{
7 common::{
8 DistanceIterator, LineSide, LinearEquation, PlaneSector, PointType, NORMAL_VECTOR_SCALE,
9 },
10 styled::{StyledDimensions, StyledDrawable, StyledPixels},
11 PrimitiveStyle, Rectangle, Sector,
12 },
13 Pixel,
14};
15use az::SaturatingAs;
16
17/// Pixel iterator for each pixel in the sector border
18#[derive(Clone, PartialEq, Debug)]
19#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
20pub struct StyledPixelsIterator<C> {
21 iter: DistanceIterator,
22
23 plane_sector: PlaneSector,
24
25 outer_threshold: u32,
26 inner_threshold: u32,
27
28 stroke_threshold_inside: i32,
29 stroke_threshold_outside: i32,
30
31 bevel: Option<(BevelKind, LinearEquation)>,
32
33 stroke_color: Option<C>,
34 fill_color: Option<C>,
35}
36
37impl<C: PixelColor> StyledPixelsIterator<C> {
38 fn new(primitive: &Sector, style: &PrimitiveStyle<C>) -> Self {
39 let stroke_area = style.stroke_area(primitive);
40 let fill_area = style.fill_area(primitive);
41
42 let stroke_area_circle = stroke_area.to_circle();
43
44 let iter = if !style.is_transparent() {
45 // PERF: The distance iterator should use the smaller sector bounding box
46 stroke_area_circle.distances()
47 } else {
48 DistanceIterator::empty()
49 };
50
51 let outer_threshold = stroke_area_circle.threshold();
52 let inner_threshold = fill_area.to_circle().threshold();
53
54 let plane_sector = PlaneSector::new(stroke_area.angle_start, stroke_area.angle_sweep);
55
56 let inside_stroke_width: i32 = style.inside_stroke_width().saturating_as();
57 let outside_stroke_width: i32 = style.outside_stroke_width().saturating_as();
58
59 let stroke_threshold_inside =
60 inside_stroke_width * NORMAL_VECTOR_SCALE * 2 - NORMAL_VECTOR_SCALE;
61 let stroke_threshold_outside =
62 outside_stroke_width * NORMAL_VECTOR_SCALE * 2 + NORMAL_VECTOR_SCALE;
63
64 // TODO: Polylines and sectors should use the same miter limit.
65 let angle_sweep_abs = primitive.angle_sweep.abs();
66 let exterior_bevel = angle_sweep_abs < Angle::from_degrees(55.0);
67 let interior_bevel = angle_sweep_abs > Angle::from_degrees(360.0 - 55.0)
68 && angle_sweep_abs < Angle::from_degrees(360.0);
69
70 let bevel = if exterior_bevel || interior_bevel {
71 let half_sweep = primitive.angle_start
72 + Angle::from_radians(primitive.angle_sweep.to_radians() / 2.0);
73 let threshold = -outside_stroke_width * NORMAL_VECTOR_SCALE * 4;
74
75 if interior_bevel {
76 Some((
77 BevelKind::Interior,
78 LinearEquation::with_angle_and_distance(half_sweep + ANGLE_90DEG, threshold),
79 ))
80 } else {
81 Some((
82 BevelKind::Exterior,
83 LinearEquation::with_angle_and_distance(half_sweep - ANGLE_90DEG, threshold),
84 ))
85 }
86 } else {
87 None
88 };
89
90 Self {
91 iter,
92 plane_sector,
93 outer_threshold,
94 inner_threshold,
95 stroke_threshold_inside,
96 stroke_threshold_outside,
97 bevel,
98 stroke_color: style.stroke_color,
99 fill_color: style.fill_color,
100 }
101 }
102}
103
104impl<C: PixelColor> Iterator for StyledPixelsIterator<C> {
105 type Item = Pixel<C>;
106
107 fn next(&mut self) -> Option<Self::Item> {
108 let outer_threshold = self.outer_threshold;
109
110 loop {
111 let (point, delta, distance) = self
112 .iter
113 .find(|(_, _, distance)| *distance < outer_threshold)?;
114
115 // Check if point is inside the radial stroke lines or the fill.
116 let mut point_type = match self.plane_sector.point_type(
117 delta,
118 self.stroke_threshold_inside,
119 self.stroke_threshold_outside,
120 ) {
121 Some(point_type) => point_type,
122 None => continue,
123 };
124
125 // Bevel the line join.
126 if point_type == PointType::Stroke {
127 if let Some((kind, equation)) = self.bevel {
128 if equation.check_side(delta, LineSide::Left) {
129 match kind {
130 BevelKind::Interior => point_type = PointType::Fill,
131 BevelKind::Exterior => continue,
132 }
133 }
134 }
135 }
136
137 // Add the outer circular stroke.
138 if point_type == PointType::Fill && distance >= self.inner_threshold {
139 point_type = PointType::Stroke;
140 }
141
142 let color = match point_type {
143 PointType::Stroke => self.stroke_color,
144 PointType::Fill => self.fill_color,
145 };
146
147 if let Some(color) = color {
148 return Some(Pixel(point, color));
149 }
150 }
151 }
152}
153
154impl<C: PixelColor> StyledPixels<PrimitiveStyle<C>> for Sector {
155 type Iter = StyledPixelsIterator<C>;
156
157 fn pixels(&self, style: &PrimitiveStyle<C>) -> Self::Iter {
158 StyledPixelsIterator::new(self, style)
159 }
160}
161
162impl<C: PixelColor> StyledDrawable<PrimitiveStyle<C>> for Sector {
163 type Color = C;
164 type Output = ();
165
166 fn draw_styled<D>(
167 &self,
168 style: &PrimitiveStyle<C>,
169 target: &mut D,
170 ) -> Result<Self::Output, D::Error>
171 where
172 D: DrawTarget<Color = C>,
173 {
174 target.draw_iter(pixels:StyledPixelsIterator::new(self, style))
175 }
176}
177
178impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for Sector {
179 // FIXME: This doesn't take into account start/end angles. This should be fixed to close #405.
180 fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle {
181 let offset: i32 = style.outside_stroke_width().saturating_as();
182
183 self.bounding_box().offset(offset)
184 }
185}
186
187#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
188#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
189enum BevelKind {
190 Interior,
191 Exterior,
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::{
198 geometry::{AngleUnit, Point},
199 mock_display::MockDisplay,
200 pixelcolor::{BinaryColor, Rgb888, RgbColor},
201 primitives::{
202 Circle, Primitive, PrimitiveStyle, PrimitiveStyleBuilder, StrokeAlignment, Styled,
203 },
204 Drawable,
205 };
206
207 // Check the rendering of a simple sector
208 #[test]
209 fn tiny_sector() {
210 let mut display = MockDisplay::new();
211
212 Sector::new(Point::zero(), 9, 210.0.deg(), 120.0.deg())
213 .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
214 .draw(&mut display)
215 .unwrap();
216
217 display.assert_pattern(&[
218 " ##### ", //
219 " ## ## ", //
220 "## ##", //
221 " ## ## ", //
222 " ### ", //
223 ]);
224 }
225
226 // Check the rendering of a filled sector with negative sweep
227 // TODO: Re-enable this test for `fixed_point` and track as part of #484
228 #[cfg_attr(not(feature = "fixed_point"), test)]
229 #[cfg_attr(feature = "fixed_point", allow(unused))]
230 fn tiny_sector_filled() {
231 let mut display = MockDisplay::new();
232
233 Sector::new(Point::zero(), 7, -30.0.deg(), -300.0.deg())
234 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
235 .draw(&mut display)
236 .unwrap();
237
238 display.assert_pattern(&[
239 " ### ", //
240 " ##### ", //
241 "###### ", //
242 "##### ", //
243 "###### ", //
244 " ##### ", //
245 " ### ", //
246 ]);
247 }
248
249 #[test]
250 fn transparent_border() {
251 let sector: Styled<Sector, PrimitiveStyle<BinaryColor>> =
252 Sector::new(Point::new(-5, -5), 21, 0.0.deg(), 90.0.deg())
253 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On));
254
255 assert!(sector.pixels().count() > 0);
256 }
257
258 fn test_stroke_alignment(
259 stroke_alignment: StrokeAlignment,
260 diameter: u32,
261 expected_pattern: &[&str],
262 ) {
263 let style = PrimitiveStyleBuilder::new()
264 .stroke_color(BinaryColor::On)
265 .stroke_width(3)
266 .stroke_alignment(stroke_alignment)
267 .build();
268
269 let mut display = MockDisplay::new();
270
271 Sector::with_center(Point::new(3, 10), diameter, 0.0.deg(), -90.0.deg())
272 .into_styled(style)
273 .draw(&mut display)
274 .unwrap();
275
276 display.assert_pattern(expected_pattern);
277 }
278
279 #[test]
280 fn stroke_alignment_inside() {
281 test_stroke_alignment(
282 StrokeAlignment::Inside,
283 19 + 2,
284 &[
285 " #### ",
286 " ###### ",
287 " ####### ",
288 " ######## ",
289 " ### #### ",
290 " ### #### ",
291 " ### ### ",
292 " ### ####",
293 " ###########",
294 " ###########",
295 " ###########",
296 ],
297 );
298 }
299
300 #[test]
301 fn stroke_alignment_center() {
302 test_stroke_alignment(
303 StrokeAlignment::Center,
304 19,
305 &[
306 " ##### ",
307 " ####### ",
308 " ######## ",
309 " ### ##### ",
310 " ### #### ",
311 " ### #### ",
312 " ### ### ",
313 " ### ####",
314 " ### ###",
315 " ############",
316 " ############",
317 " ############",
318 ],
319 );
320 }
321
322 #[test]
323 fn stroke_alignment_outside() {
324 test_stroke_alignment(
325 StrokeAlignment::Outside,
326 19 - 4,
327 &[
328 "####### ",
329 "######### ",
330 "########## ",
331 "### ##### ",
332 "### #### ",
333 "### #### ",
334 "### ### ",
335 "### ####",
336 "### ###",
337 "### ###",
338 "### ###",
339 "##############",
340 "##############",
341 "##############",
342 ],
343 );
344 }
345
346 #[test]
347 fn bounding_boxes() {
348 const CENTER: Point = Point::new(15, 15);
349 const SIZE: u32 = 10;
350
351 let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
352
353 let center = Sector::with_center(CENTER, SIZE, 0.0.deg(), 90.0.deg()).into_styled(style);
354 let inside = Sector::with_center(CENTER, SIZE + 2, 0.0.deg(), 90.0.deg()).into_styled(
355 PrimitiveStyleBuilder::from(&style)
356 .stroke_alignment(StrokeAlignment::Inside)
357 .build(),
358 );
359 let outside = Sector::with_center(CENTER, SIZE - 4, 0.0.deg(), 90.0.deg()).into_styled(
360 PrimitiveStyleBuilder::from(&style)
361 .stroke_alignment(StrokeAlignment::Outside)
362 .build(),
363 );
364 let transparent = Sector::with_center(CENTER, SIZE, 0.0.deg(), 90.0.deg()).into_styled(
365 PrimitiveStyleBuilder::<BinaryColor>::new()
366 .stroke_width(3)
367 .build(),
368 );
369
370 // TODO: Uncomment when arc bounding box is fixed in #405
371 // let mut display = MockDisplay::new();
372 // center.draw(&mut display).unwrap();
373 // assert_eq!(display.affected_area(), center.bounding_box());
374
375 assert_eq!(center.bounding_box(), inside.bounding_box());
376 assert_eq!(outside.bounding_box(), inside.bounding_box());
377 assert_eq!(transparent.bounding_box(), inside.bounding_box());
378 }
379
380 /// The radial lines should be connected using a line join.
381 #[test]
382 fn issue_484_line_join_90_deg() {
383 let mut display = MockDisplay::<Rgb888>::new();
384
385 Sector::new(Point::new(-6, 1), 15, 0.0.deg(), -90.0.deg())
386 .into_styled(
387 PrimitiveStyleBuilder::new()
388 .stroke_color(Rgb888::RED)
389 .stroke_width(3)
390 .fill_color(Rgb888::GREEN)
391 .build(),
392 )
393 .draw(&mut display)
394 .unwrap();
395
396 display.assert_pattern(&[
397 "RRRR ",
398 "RRRRRR ",
399 "RRRRRRRR ",
400 "RRRGRRRR ",
401 "RRRGGRRRR ",
402 "RRRGGGRRR ",
403 "RRRGGGGRRR",
404 "RRRRRRRRRR",
405 "RRRRRRRRRR",
406 "RRRRRRRRRR",
407 ]);
408 }
409
410 /// The radial lines should be connected using a line join.
411 #[test]
412 fn issue_484_line_join_20_deg() {
413 let mut display = MockDisplay::<Rgb888>::new();
414
415 Sector::new(Point::new(-4, -3), 15, 0.0.deg(), -20.0.deg())
416 .into_styled(
417 PrimitiveStyleBuilder::new()
418 .stroke_color(Rgb888::RED)
419 .stroke_width(3)
420 .fill_color(Rgb888::GREEN)
421 .build(),
422 )
423 .draw(&mut display)
424 .unwrap();
425
426 display.assert_pattern(&[
427 " R ",
428 " RRRR ",
429 " RRRRRRR",
430 " RRRRRRRRRR",
431 " RRRRRRRRRRR",
432 " RRRRRRRRRR",
433 ]);
434 }
435
436 /// The radial lines should be connected using a line join.
437 #[test]
438 fn issue_484_line_join_340_deg() {
439 let mut display = MockDisplay::<Rgb888>::new();
440
441 Sector::new(Point::new_equal(2), 15, 20.0.deg(), 340.0.deg())
442 .into_styled(
443 PrimitiveStyleBuilder::new()
444 .stroke_color(Rgb888::RED)
445 .stroke_width(3)
446 .fill_color(Rgb888::GREEN)
447 .build(),
448 )
449 .draw(&mut display)
450 .unwrap();
451
452 display.assert_pattern(&[
453 " ",
454 " RRRRR ",
455 " RRRRRRRRR ",
456 " RRRRRRRRRRRRR ",
457 " RRRRGGGGGRRRR ",
458 " RRRRGGGGGGGRRRR ",
459 " RRRGGGGGGGGGRRR ",
460 " RRRGGGGGGGGGGGRRR",
461 " RRRGGGGRRRRRRRRRR",
462 " RRRGGGRRRRRRRRRRR",
463 " RRRGGGGRRRRRRRRRR",
464 " RRRGGGGGGGRRRRRRR",
465 " RRRGGGGGGGGRRRR ",
466 " RRRRGGGGGGGRRRR ",
467 " RRRRGGGGGRRRR ",
468 " RRRRRRRRRRRRR ",
469 " RRRRRRRRR ",
470 " RRRRR ",
471 ]);
472 }
473
474 /// The stroke for the radial lines shouldn't overlap the outer edge of the stroke on the
475 /// circular part of the sector.
476 #[test]
477 #[ignore]
478 fn issue_484_stroke_should_not_overlap_outer_edge() {
479 let mut display = MockDisplay::<Rgb888>::new();
480
481 Sector::with_center(Point::new(10, 15), 11, 0.0.deg(), 90.0.deg())
482 .into_styled(
483 PrimitiveStyleBuilder::new()
484 .stroke_color(Rgb888::RED)
485 .stroke_width(21)
486 .fill_color(Rgb888::GREEN)
487 .build(),
488 )
489 .draw(&mut display)
490 .unwrap();
491
492 display.assert_pattern(&[
493 "RRRRRRRRRRRRRR ",
494 "RRRRRRRRRRRRRRRRR ",
495 "RRRRRRRRRRRRRRRRRRR ",
496 "RRRRRRRRRRRRRRRRRRRR ",
497 "RRRRRRRRRRRRRRRRRRRRR ",
498 "RRRRRRRRRRRRRRRRRRRRRR ",
499 "RRRRRRRRRRRRRRRRRRRRRRR ",
500 "RRRRRRRRRRRRRRRRRRRRRRRR ",
501 "RRRRRRRRRRRRRRRRRRRRRRRR ",
502 "RRRRRRRRRRRRRRRRRRRRRRRRR ",
503 "RRRRRRRRRRRRRRRRRRRRRRRRR ",
504 "RRRRRRRRRRRRRRRRRRRRRRRRR ",
505 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
506 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
507 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
508 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
509 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
510 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
511 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
512 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
513 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
514 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
515 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
516 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
517 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
518 "RRRRRRRRRRRRRRRRRRRRRRRRRR",
519 ]);
520 }
521
522 /// Both radial lines should be perfectly aligned for 180° sweep angle.
523 #[test]
524 fn issue_484_stroke_center_semicircle() {
525 let mut display = MockDisplay::new();
526
527 Sector::new(Point::new_equal(1), 15, 180.0.deg(), 180.0.deg())
528 .into_styled(
529 PrimitiveStyleBuilder::new()
530 .fill_color(BinaryColor::On)
531 .stroke_color(BinaryColor::Off)
532 .stroke_width(2)
533 .stroke_alignment(StrokeAlignment::Center)
534 .build(),
535 )
536 .draw(&mut display)
537 .unwrap();
538
539 display.assert_pattern(&[
540 " ..... ",
541 " ......... ",
542 " ....#####.... ",
543 " ..#########.. ",
544 " ..###########.. ",
545 " ..###########.. ",
546 "..#############..",
547 "..#############..",
548 ".................",
549 ".................",
550 ]);
551 }
552
553 /// Both radial lines should be perfectly aligned for 180° sweep angle.
554 #[test]
555 fn issue_484_stroke_center_semicircle_vertical() {
556 let mut display = MockDisplay::new();
557
558 Sector::new(Point::new_equal(1), 15, 90.0.deg(), 180.0.deg())
559 .into_styled(
560 PrimitiveStyleBuilder::new()
561 .fill_color(BinaryColor::On)
562 .stroke_color(BinaryColor::Off)
563 .stroke_width(2)
564 .stroke_alignment(StrokeAlignment::Center)
565 .build(),
566 )
567 .draw(&mut display)
568 .unwrap();
569
570 display.assert_pattern(&[
571 " ....",
572 " ......",
573 " ....##..",
574 " ..####..",
575 " ..#####..",
576 " ..#####..",
577 "..######..",
578 "..######..",
579 "..######..",
580 "..######..",
581 "..######..",
582 " ..#####..",
583 " ..#####..",
584 " ..####..",
585 " ....##..",
586 " ......",
587 " ....",
588 ]);
589 }
590
591 /// The fill shouldn't overlap the stroke and there should be no gaps between stroke and fill.
592 #[test]
593 fn issue_484_gaps_and_overlap() {
594 let mut display = MockDisplay::new();
595
596 Sector::with_center(Point::new(2, 20), 40, 14.0.deg(), -90.0.deg())
597 .into_styled(
598 PrimitiveStyleBuilder::new()
599 .fill_color(Rgb888::GREEN)
600 .stroke_color(Rgb888::RED)
601 .stroke_width(2)
602 .build(),
603 )
604 .draw(&mut display)
605 .unwrap();
606
607 display.assert_pattern(&[
608 " R ",
609 " RRRRR ",
610 " RRRRRRR ",
611 " RRGGRRRRR ",
612 " RRGGGGRRRR ",
613 " RRGGGGGGGRRR ",
614 " RRGGGGGGGGRRR ",
615 " RRGGGGGGGGGRRR ",
616 " RRGGGGGGGGGGRRR ",
617 " RRGGGGGGGGGGGGRRR ",
618 " RRGGGGGGGGGGGGGRR ",
619 " RRGGGGGGGGGGGGGRRR ",
620 " RRGGGGGGGGGGGGGGRR ",
621 " RRGGGGGGGGGGGGGGGRRR ",
622 " RRGGGGGGGGGGGGGGGGRR ",
623 " RRGGGGGGGGGGGGGGGGRR ",
624 " RRGGGGGGGGGGGGGGGGRRR",
625 " RRGGGGGGGGGGGGGGGGGGRR",
626 " RRGGGGGGGGGGGGGGGGGGRR",
627 " RRGGGGGGGGGGGGGGGGGGRR",
628 " RRGGGGGGGGGGGGGGGGGGRR",
629 " RRRRRRGGGGGGGGGGGGGGGRR",
630 " RRRRRRRRGGGGGGGGGGGRR",
631 " RRRRRRRRGGGGGGGRR",
632 " RRRRRRRRGGGRR",
633 " RRRRRRRRR",
634 " RRRR ",
635 ]);
636 }
637
638 /// No radial lines should be drawn if the sweep angle is 360°.
639 #[test]
640 fn issue_484_no_radial_lines_for_360_degree_sweep_angle() {
641 let style = PrimitiveStyleBuilder::new()
642 .fill_color(Rgb888::GREEN)
643 .stroke_color(Rgb888::RED)
644 .stroke_width(1)
645 .build();
646
647 let circle = Circle::new(Point::new_equal(1), 11);
648
649 let mut expected = MockDisplay::new();
650 circle.into_styled(style).draw(&mut expected).unwrap();
651
652 let mut display = MockDisplay::new();
653
654 Sector::new(Point::new_equal(1), 11, 0.0.deg(), 360.0.deg())
655 .into_styled(style)
656 .draw(&mut display)
657 .unwrap();
658
659 display.assert_eq(&expected);
660 }
661
662 /// No radial lines should be drawn for sweep angles larger than 360°.
663 #[test]
664 fn issue_484_no_radial_lines_for_sweep_angles_larger_than_360_degree() {
665 let style = PrimitiveStyleBuilder::new()
666 .fill_color(Rgb888::GREEN)
667 .stroke_color(Rgb888::RED)
668 .stroke_width(1)
669 .build();
670
671 let circle = Circle::new(Point::new_equal(1), 11);
672
673 let mut expected = MockDisplay::new();
674 circle.into_styled(style).draw(&mut expected).unwrap();
675
676 let mut display = MockDisplay::new();
677
678 Sector::from_circle(circle, 90.0.deg(), -472.0.deg())
679 .into_styled(style)
680 .draw(&mut display)
681 .unwrap();
682
683 display.assert_eq(&expected);
684 }
685
686 /// The sector was mirrored along the Y axis if the start angle was exactly 360°.
687 #[test]
688 fn issue_484_sector_flips_at_360_degrees() {
689 let mut display = MockDisplay::new();
690
691 // This would trigger the out of bounds drawing check if the sector
692 // would be mirrored along the Y axis.
693 Sector::new(Point::new(-15, 0), 31, 360.0.deg(), 90.0.deg())
694 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
695 .draw(&mut display)
696 .unwrap();
697 }
698}
699