embedded_graphics/primitives/arc/
styled.rs

1use crate::{
2    draw_target::DrawTarget,
3    geometry::Dimensions,
4    pixelcolor::PixelColor,
5    primitives::{
6        arc::Arc,
7        common::{DistanceIterator, PlaneSector},
8        styled::{StyledDimensions, StyledDrawable, StyledPixels},
9        OffsetOutline, PrimitiveStyle, Rectangle,
10    },
11    Pixel,
12};
13
14use az::SaturatingAs;
15
16/// Pixel iterator for each pixel in the arc border
17#[derive(Clone, PartialEq, Debug)]
18#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
19pub struct StyledPixelsIterator<C> {
20    iter: DistanceIterator,
21
22    plane_sector: PlaneSector,
23
24    outer_threshold: u32,
25    inner_threshold: u32,
26
27    stroke_color: Option<C>,
28}
29
30impl<C: PixelColor> StyledPixelsIterator<C> {
31    fn new(primitive: &Arc, style: &PrimitiveStyle<C>) -> Self {
32        let circle = primitive.to_circle();
33
34        let outside_edge = circle.offset(style.outside_stroke_width().saturating_as());
35        let inside_edge = circle.offset(-style.inside_stroke_width().saturating_as::<i32>());
36
37        let iter = if !style.is_transparent() {
38            // PERF: The distance iterator should use the smaller arc bounding box
39            outside_edge.distances()
40        } else {
41            DistanceIterator::empty()
42        };
43
44        let plane_sector = PlaneSector::new(primitive.angle_start, primitive.angle_sweep);
45
46        Self {
47            iter,
48            plane_sector,
49            outer_threshold: outside_edge.threshold(),
50            inner_threshold: inside_edge.threshold(),
51            stroke_color: style.stroke_color,
52        }
53    }
54}
55
56impl<C: PixelColor> Iterator for StyledPixelsIterator<C> {
57    type Item = Pixel<C>;
58
59    fn next(&mut self) -> Option<Self::Item> {
60        let stroke_color = self.stroke_color?;
61
62        self.iter
63            .find(|(_, delta, distance)| {
64                *distance < self.outer_threshold
65                    && *distance >= self.inner_threshold
66                    && self.plane_sector.contains(*delta)
67            })
68            .map(|(point, ..)| Pixel(point, stroke_color))
69    }
70}
71
72impl<C: PixelColor> StyledDrawable<PrimitiveStyle<C>> for Arc {
73    type Color = C;
74    type Output = ();
75
76    fn draw_styled<D>(
77        &self,
78        style: &PrimitiveStyle<C>,
79        target: &mut D,
80    ) -> Result<Self::Output, D::Error>
81    where
82        D: DrawTarget<Color = C>,
83    {
84        target.draw_iter(StyledPixelsIterator::new(self, style))
85    }
86}
87
88impl<C: PixelColor> StyledPixels<PrimitiveStyle<C>> for Arc {
89    type Iter = StyledPixelsIterator<C>;
90
91    fn pixels(&self, style: &PrimitiveStyle<C>) -> Self::Iter {
92        StyledPixelsIterator::new(self, style)
93    }
94}
95
96impl<C: PixelColor> StyledDimensions<PrimitiveStyle<C>> for Arc {
97    // FIXME: This doesn't take into account start/end angles. This should be fixed to close #405.
98    fn styled_bounding_box(&self, style: &PrimitiveStyle<C>) -> Rectangle {
99        let offset = style.outside_stroke_width().saturating_as();
100
101        self.bounding_box().offset(offset)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::{
109        draw_target::DrawTargetExt,
110        geometry::{AnchorPoint, AngleUnit, Point, Size},
111        mock_display::MockDisplay,
112        pixelcolor::BinaryColor,
113        primitives::{Circle, Primitive, PrimitiveStyle, PrimitiveStyleBuilder, StrokeAlignment},
114        Drawable,
115    };
116
117    // Check the rendering of a simple arc
118    #[test]
119    fn tiny_arc() -> Result<(), core::convert::Infallible> {
120        let mut display = MockDisplay::new();
121
122        Arc::new(Point::zero(), 7, 210.0.deg(), 120.0.deg())
123            .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1))
124            .draw(&mut display)?;
125
126        display.assert_pattern(&[
127            "  ###  ", //
128            " #   # ", //
129        ]);
130
131        Ok(())
132    }
133
134    /// Draws arcs with +/-90° sweep angle and compares the result with drawing a quarter circle.
135    #[test]
136    fn quadrant_arcs() {
137        let style = PrimitiveStyle::with_stroke(BinaryColor::On, 2);
138
139        for &diameter in &[11, 12] {
140            for &(angle_start, angle_sweep, anchor_point) in &[
141                (0.0.deg(), 90.0.deg(), AnchorPoint::BottomRight),
142                (90.0.deg(), 90.0.deg(), AnchorPoint::BottomLeft),
143                (180.0.deg(), 90.0.deg(), AnchorPoint::TopLeft),
144                (270.0.deg(), 90.0.deg(), AnchorPoint::TopRight),
145                (0.0.deg(), -90.0.deg(), AnchorPoint::TopRight),
146                (90.0.deg(), -90.0.deg(), AnchorPoint::BottomRight),
147                (180.0.deg(), -90.0.deg(), AnchorPoint::BottomLeft),
148                (270.0.deg(), -90.0.deg(), AnchorPoint::TopLeft),
149            ] {
150                let circle = Circle::new(Point::new(1, 1), diameter).into_styled(style);
151
152                // Calculate a clip rectangle for the tested quadrant.
153                let bounding_box = circle.bounding_box();
154                let clip_rect = bounding_box
155                    .resized((bounding_box.size + Size::new_equal(1)) / 2, anchor_point);
156
157                // Draw expected display by clipping the circle to the quadrant.
158                let mut expected = MockDisplay::new();
159                circle.draw(&mut expected.clipped(&clip_rect)).unwrap();
160
161                // Draw the arc.
162                let mut display = MockDisplay::new();
163                Arc::new(Point::new(1, 1), diameter, angle_start, angle_sweep)
164                    .into_styled(style)
165                    .draw(&mut display)
166                    .unwrap();
167
168                display.assert_eq_with_message(&expected, |f| {
169                    write!(
170                        f,
171                        "diameter: {}, angle_start: {}, angle_sweep: {}",
172                        diameter,
173                        angle_start.to_degrees(),
174                        angle_sweep.to_degrees()
175                    )
176                });
177            }
178        }
179    }
180
181    #[test]
182    fn stroke_alignment() {
183        const CENTER: Point = Point::new(15, 15);
184        const SIZE: u32 = 10;
185
186        let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
187
188        let mut display_center = MockDisplay::new();
189        Arc::with_center(CENTER, SIZE, 0.0.deg(), 90.0.deg())
190            .into_styled(style)
191            .draw(&mut display_center)
192            .unwrap();
193
194        let mut display_inside = MockDisplay::new();
195        Arc::with_center(CENTER, SIZE + 2, 0.0.deg(), 90.0.deg())
196            .into_styled(
197                PrimitiveStyleBuilder::from(&style)
198                    .stroke_alignment(StrokeAlignment::Inside)
199                    .build(),
200            )
201            .draw(&mut display_inside)
202            .unwrap();
203
204        let mut display_outside = MockDisplay::new();
205        Arc::with_center(CENTER, SIZE - 4, 0.0.deg(), 90.0.deg())
206            .into_styled(
207                PrimitiveStyleBuilder::from(&style)
208                    .stroke_alignment(StrokeAlignment::Outside)
209                    .build(),
210            )
211            .draw(&mut display_outside)
212            .unwrap();
213
214        display_inside.assert_eq(&display_center);
215        display_outside.assert_eq(&display_center);
216    }
217
218    #[test]
219    fn bounding_boxes() {
220        const CENTER: Point = Point::new(15, 15);
221        const SIZE: u32 = 10;
222
223        let style = PrimitiveStyle::with_stroke(BinaryColor::On, 3);
224
225        let center = Arc::with_center(CENTER, SIZE, 0.0.deg(), 90.0.deg()).into_styled(style);
226        let inside = Arc::with_center(CENTER, SIZE + 2, 0.0.deg(), 90.0.deg()).into_styled(
227            PrimitiveStyleBuilder::from(&style)
228                .stroke_alignment(StrokeAlignment::Inside)
229                .build(),
230        );
231        let outside = Arc::with_center(CENTER, SIZE - 4, 0.0.deg(), 90.0.deg()).into_styled(
232            PrimitiveStyleBuilder::from(&style)
233                .stroke_alignment(StrokeAlignment::Outside)
234                .build(),
235        );
236
237        assert_eq!(center.bounding_box(), inside.bounding_box());
238        assert_eq!(outside.bounding_box(), inside.bounding_box());
239
240        // TODO: Uncomment when arc bounding box is fixed in #405
241        // let mut display = MockDisplay::new();
242        // center.draw(&mut display).unwrap();
243        // assert_eq!(display.affected_area(), center.bounding_box());
244    }
245
246    #[test]
247    fn bounding_box_is_independent_of_colors() {
248        const CENTER: Point = Point::new(15, 15);
249        const SIZE: u32 = 10;
250
251        let arc = Arc::with_center(CENTER, SIZE, 0.0.deg(), 90.0.deg());
252
253        let transparent_arc = arc.into_styled(
254            PrimitiveStyleBuilder::<BinaryColor>::new()
255                .stroke_width(5)
256                .build(),
257        );
258        let stroked_arc = arc.into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 5));
259
260        assert_eq!(transparent_arc.bounding_box(), stroked_arc.bounding_box(),);
261    }
262}