embedded_graphics/primitives/sector/
styled.rs

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(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 = 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}