embedded_graphics/primitives/sector/
mod.rs

1//! The sector primitive
2
3use crate::{
4    geometry::{Angle, Dimensions, Point, Size},
5    primitives::{
6        common::PlaneSector, Circle, ContainsPoint, OffsetOutline, PointsIter, Primitive, Rectangle,
7    },
8    transform::Transform,
9};
10
11mod points;
12mod styled;
13
14pub use points::Points;
15pub use styled::StyledPixelsIterator;
16
17/// Sector primitive
18///
19/// # Examples
20///
21/// ## Create some sectors with different styles
22///
23/// ```rust
24/// use embedded_graphics::{
25///     pixelcolor::Rgb565,
26///     prelude::*,
27///     primitives::{Sector, PrimitiveStyle, PrimitiveStyleBuilder},
28/// };
29/// # use embedded_graphics::mock_display::MockDisplay;
30/// # let mut display = MockDisplay::default();
31/// # display.set_allow_overdraw(true);
32///
33/// // Sector with 1 pixel wide white stroke with top-left point at (10, 20) with a diameter of 30
34/// Sector::new(Point::new(10, 20), 30, 0.0.deg(), 90.0.deg())
35///     .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1))
36///     .draw(&mut display)?;
37///
38/// // Sector with styled stroke and fill with top-left point at (10, 20) with a diameter of 30
39/// let style = PrimitiveStyleBuilder::new()
40///     .stroke_color(Rgb565::RED)
41///     .stroke_width(3)
42///     .fill_color(Rgb565::GREEN)
43///     .build();
44///
45/// Sector::new(Point::new(10, 20), 30, 180.0.deg(), -90.0.deg())
46///     .into_styled(style)
47///     .draw(&mut display)?;
48///
49/// // Sector with blue fill and no stroke with a translation applied
50/// Sector::new(Point::new(10, 20), 30, 0.0.deg(), 90.0.deg())
51///     .translate(Point::new(15, 5))
52///     .into_styled(PrimitiveStyle::with_fill(Rgb565::BLUE))
53///     .draw(&mut display)?;
54/// # Ok::<(), core::convert::Infallible>(())
55/// ```
56#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
57#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
58pub struct Sector {
59    /// Top-left point of the bounding-box of the circle supporting the sector
60    pub top_left: Point,
61
62    /// Diameter of the circle supporting the sector
63    pub diameter: u32,
64
65    /// Angle at which the sector starts
66    pub angle_start: Angle,
67
68    /// Angle defining the sector sweep starting at angle_start
69    pub angle_sweep: Angle,
70}
71
72impl Sector {
73    /// Create a new sector delimited with a top-left point with a specific diameter and start and sweep angles
74    pub const fn new(
75        top_left: Point,
76        diameter: u32,
77        angle_start: Angle,
78        angle_sweep: Angle,
79    ) -> Self {
80        Sector {
81            top_left,
82            diameter,
83            angle_start,
84            angle_sweep,
85        }
86    }
87
88    /// Create a new sector centered around a given point with a specific diameter and start and sweep angles
89    pub const fn with_center(
90        center: Point,
91        diameter: u32,
92        angle_start: Angle,
93        angle_sweep: Angle,
94    ) -> Self {
95        let top_left = Rectangle::with_center(center, Size::new_equal(diameter)).top_left;
96
97        Sector {
98            top_left,
99            diameter,
100            angle_start,
101            angle_sweep,
102        }
103    }
104
105    /// Creates an arc based on a circle.
106    ///
107    /// The resulting sector will match the `top_left` and `diameter` of the base circle.
108    pub const fn from_circle(circle: Circle, angle_start: Angle, angle_sweep: Angle) -> Self {
109        Sector {
110            top_left: circle.top_left,
111            diameter: circle.diameter,
112            angle_start,
113            angle_sweep,
114        }
115    }
116
117    /// Returns a circle with the same `top_left` and `diameter` as this sector.
118    pub const fn to_circle(&self) -> Circle {
119        Circle::new(self.top_left, self.diameter)
120    }
121
122    /// Return the center point of the sector
123    pub fn center(&self) -> Point {
124        self.bounding_box().center()
125    }
126
127    /// Returns the center point of the sector scaled by a factor of 2.
128    ///
129    /// This method is used to accurately calculate the outside edge of the sector.
130    /// The result is not equivalent to `self.center() * 2` because of rounding.
131    fn center_2x(&self) -> Point {
132        // The radius scaled up by a factor of 2 is equal to the diameter
133        let radius = self.diameter.saturating_sub(1);
134
135        self.top_left * 2 + Size::new(radius, radius)
136    }
137}
138
139impl OffsetOutline for Sector {
140    fn offset(&self, offset: i32) -> Self {
141        let circle = self.to_circle().offset(offset);
142
143        Self::from_circle(circle, self.angle_start, self.angle_sweep)
144    }
145}
146
147impl Primitive for Sector {}
148
149impl PointsIter for Sector {
150    type Iter = Points;
151
152    fn points(&self) -> Self::Iter {
153        Points::new(self)
154    }
155}
156
157impl ContainsPoint for Sector {
158    fn contains(&self, point: Point) -> bool {
159        if self.to_circle().contains(point) {
160            let delta = point * 2 - self.center_2x();
161            PlaneSector::new(self.angle_start, self.angle_sweep).contains(delta)
162        } else {
163            false
164        }
165    }
166}
167
168impl Dimensions for Sector {
169    fn bounding_box(&self) -> Rectangle {
170        Rectangle::new(self.top_left, Size::new_equal(self.diameter))
171    }
172}
173
174impl Transform for Sector {
175    /// Translate the sector from its current position to a new position by (x, y) pixels,
176    /// returning a new `Sector`. For a mutating transform, see `translate_mut`.
177    ///
178    /// ```
179    /// # use embedded_graphics::primitives::Sector;
180    /// # use embedded_graphics::prelude::*;
181    /// let sector = Sector::new(Point::new(5, 10), 10, 0.0.deg(), 90.0.deg());
182    /// let moved = sector.translate(Point::new(10, 10));
183    ///
184    /// assert_eq!(moved.top_left, Point::new(15, 20));
185    /// ```
186    fn translate(&self, by: Point) -> Self {
187        Self {
188            top_left: self.top_left + by,
189            ..*self
190        }
191    }
192
193    /// Translate the sector from its current position to a new position by (x, y) pixels.
194    ///
195    /// ```
196    /// # use embedded_graphics::primitives::Sector;
197    /// # use embedded_graphics::prelude::*;
198    /// let mut sector = Sector::new(Point::new(5, 10), 10, 0.0.deg(), 90.0.deg());
199    /// sector.translate_mut(Point::new(10, 10));
200    ///
201    /// assert_eq!(sector.top_left, Point::new(15, 20));
202    /// ```
203    fn translate_mut(&mut self, by: Point) -> &mut Self {
204        self.top_left += by;
205
206        self
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::geometry::AngleUnit;
214
215    #[test]
216    fn negative_dimensions() {
217        let sector = Sector::new(Point::new(-15, -15), 10, 0.0.deg(), 90.0.deg());
218
219        assert_eq!(
220            sector.bounding_box(),
221            Rectangle::new(Point::new(-15, -15), Size::new(10, 10))
222        );
223    }
224
225    #[test]
226    fn dimensions() {
227        let sector = Sector::new(Point::new(5, 15), 10, 0.0.deg(), 90.0.deg());
228
229        assert_eq!(
230            sector.bounding_box(),
231            Rectangle::new(Point::new(5, 15), Size::new(10, 10))
232        );
233    }
234
235    #[test]
236    fn it_handles_negative_coordinates() {
237        let positive = Sector::new(Point::new(10, 10), 5, 0.0.deg(), 90.0.deg()).points();
238
239        let negative = Sector::new(Point::new(-10, -10), 5, 0.0.deg(), 90.0.deg()).points();
240
241        assert!(negative.eq(positive.map(|p| p - Point::new(20, 20))));
242    }
243
244    #[test]
245    fn center_is_correct() {
246        // odd diameter
247        let sector = Sector::new(Point::new(10, 10), 5, 0.0.deg(), 90.0.deg());
248        assert_eq!(sector.center(), Point::new(12, 12));
249
250        // even diameter
251        let sector = Sector::new(Point::new(10, 10), 6, 0.0.deg(), 90.0.deg());
252        assert_eq!(sector.center(), Point::new(12, 12));
253
254        // odd diameter
255        let sector = Sector::with_center(Point::new(10, 10), 5, 0.0.deg(), 90.0.deg());
256        assert_eq!(sector.center(), Point::new(10, 10));
257
258        // even diameter
259        let sector = Sector::with_center(Point::new(10, 10), 6, 0.0.deg(), 90.0.deg());
260        assert_eq!(sector.center(), Point::new(10, 10));
261    }
262
263    #[test]
264    fn contains() {
265        let sector = Sector::new(Point::zero(), 10, 0.0.deg(), 90.0.deg());
266
267        let contained_points = Rectangle::new(Point::new(-10, -10), Size::new(30, 30))
268            .points()
269            .filter(|p| sector.contains(*p));
270
271        assert!(contained_points.eq(sector.points()));
272    }
273
274    #[test]
275    fn offset() {
276        let center = Point::new(5, 7);
277        let sector = Sector::with_center(center, 3, 0.0.deg(), 90.0.deg());
278
279        assert_eq!(sector.offset(0), sector);
280
281        assert_eq!(
282            sector.offset(1),
283            Sector::with_center(center, 5, 0.0.deg(), 90.0.deg())
284        );
285        assert_eq!(
286            sector.offset(2),
287            Sector::with_center(center, 7, 0.0.deg(), 90.0.deg())
288        );
289
290        assert_eq!(
291            sector.offset(-1),
292            Sector::with_center(center, 1, 0.0.deg(), 90.0.deg())
293        );
294        assert_eq!(
295            sector.offset(-2),
296            Sector::with_center(center, 0, 0.0.deg(), 90.0.deg())
297        );
298        assert_eq!(
299            sector.offset(-3),
300            Sector::with_center(center, 0, 0.0.deg(), 90.0.deg())
301        );
302    }
303}