Skip to main content

embedded_graphics/primitives/common/
plane_sector.rs

1use crate::{
2    geometry::{angle_consts::*, Angle, Point},
3    primitives::common::{LineSide, OriginLinearEquation, PointType},
4};
5
6#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
7#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
8enum Operation {
9    /// Return the intersection of both half planes.
10    Intersection,
11    /// Return the union of both half planes.
12    Union,
13    /// Return the entire plane.
14    EntirePlane,
15}
16
17impl Operation {
18    /// Executes the operation.
19    const fn execute(self, first: bool, second: bool) -> bool {
20        match self {
21            Operation::Intersection => first && second,
22            Operation::Union => first || second,
23            Operation::EntirePlane => true,
24        }
25    }
26}
27
28/// Sector shaped part of a plane.
29///
30/// The shape is described by two half-planes that divide the XY plane along the two
31/// lines from the center point to the arc's end points. For sweep angles < 180° the
32/// intersection of both half-planes is used and for angles >= 180° the union of both
33/// half-planes.
34#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
35#[cfg_attr(feature = "defmt", derive(::defmt::Format))]
36pub struct PlaneSector {
37    /// Half plane on the left side of a line.
38    half_plane_left: OriginLinearEquation,
39
40    /// Half plane on the right side of a line.
41    half_plane_right: OriginLinearEquation,
42
43    /// The operation used to combine the two half planes.
44    operation: Operation,
45}
46
47impl PlaneSector {
48    pub fn new(mut angle_start: Angle, angle_sweep: Angle) -> Self {
49        let angle_sweep_abs = angle_sweep.abs();
50
51        let operation = if angle_sweep_abs >= ANGLE_360DEG {
52            // Skip calculation of half planes if the absolute value of the sweep angle is >= 360°.
53            return Self {
54                half_plane_left: OriginLinearEquation::new_horizontal(),
55                half_plane_right: OriginLinearEquation::new_horizontal(),
56                operation: Operation::EntirePlane,
57            };
58        } else if angle_sweep_abs >= ANGLE_180DEG {
59            Operation::Union
60        } else {
61            Operation::Intersection
62        };
63
64        let mut angle_end = angle_start + angle_sweep;
65
66        // Swap angles for negative sweeps to use the correct sides of the half planes.
67        if angle_sweep < Angle::zero() {
68            core::mem::swap(&mut angle_start, &mut angle_end)
69        }
70
71        Self {
72            half_plane_right: OriginLinearEquation::with_angle(angle_start),
73            half_plane_left: OriginLinearEquation::with_angle(angle_end),
74            operation,
75        }
76    }
77
78    pub fn contains(&self, point: Point) -> bool {
79        let correct_side_1 = self.half_plane_left.check_side(point, LineSide::Left);
80        let correct_side_2 = self.half_plane_right.check_side(point, LineSide::Right);
81
82        self.operation.execute(correct_side_1, correct_side_2)
83    }
84
85    /// Checks if a point is inside the stroke or fill area.
86    pub fn point_type(
87        &self,
88        point: Point,
89        inside_threshold: i32,
90        outside_threshold: i32,
91    ) -> Option<PointType> {
92        let distance_right = self.half_plane_right.distance(point);
93        let distance_left = self.half_plane_left.distance(point);
94
95        if self.operation.execute(
96            distance_right >= -outside_threshold,
97            distance_left <= outside_threshold,
98        ) {
99            if self.operation.execute(
100                distance_right >= inside_threshold,
101                distance_left <= -inside_threshold,
102            ) {
103                Some(PointType::Fill)
104            } else {
105                Some(PointType::Stroke)
106            }
107        } else {
108            None
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::geometry::AngleUnit;
117
118    /// Checks if the plane sector contains 8 different points.
119    ///
120    /// Four of the points lie on the boundary between two adjacent quadrants and should be
121    /// contained in both quadrants. The remaining points are inside a single quadrant.
122    fn contains(plane_sector: &PlaneSector) -> [bool; 8] {
123        [
124            plane_sector.contains(Point::new(10, 0)),
125            plane_sector.contains(Point::new(10, 10)),
126            plane_sector.contains(Point::new(0, 10)),
127            plane_sector.contains(Point::new(-10, 10)),
128            plane_sector.contains(Point::new(-10, 0)),
129            plane_sector.contains(Point::new(-10, -10)),
130            plane_sector.contains(Point::new(0, -10)),
131            plane_sector.contains(Point::new(10, -10)),
132        ]
133    }
134
135    #[test]
136    fn plane_sector_quadrants_positive_sweep() {
137        let plane_sector = PlaneSector::new(0.0.deg(), 90.0.deg());
138        assert_eq!(
139            contains(&plane_sector),
140            [true, true, true, false, false, false, false, false]
141        );
142
143        let plane_sector = PlaneSector::new(90.0.deg(), 90.0.deg());
144        assert_eq!(
145            contains(&plane_sector),
146            [false, false, true, true, true, false, false, false]
147        );
148
149        let plane_sector = PlaneSector::new(180.0.deg(), 90.0.deg());
150        assert_eq!(
151            contains(&plane_sector),
152            [false, false, false, false, true, true, true, false]
153        );
154
155        let plane_sector = PlaneSector::new(270.0.deg(), 90.0.deg());
156        assert_eq!(
157            contains(&plane_sector),
158            [true, false, false, false, false, false, true, true]
159        );
160    }
161
162    #[test]
163    fn plane_sector_quadrants_negative_sweep() {
164        let plane_sector = PlaneSector::new(0.0.deg(), -90.0.deg());
165        assert_eq!(
166            contains(&plane_sector),
167            [true, false, false, false, false, false, true, true]
168        );
169
170        let plane_sector = PlaneSector::new(90.0.deg(), -90.0.deg());
171        assert_eq!(
172            contains(&plane_sector),
173            [true, true, true, false, false, false, false, false]
174        );
175
176        let plane_sector = PlaneSector::new(180.0.deg(), -90.0.deg());
177        assert_eq!(
178            contains(&plane_sector),
179            [false, false, true, true, true, false, false, false]
180        );
181
182        let plane_sector = PlaneSector::new(270.0.deg(), -90.0.deg());
183        assert_eq!(
184            contains(&plane_sector),
185            [false, false, false, false, true, true, true, false]
186        );
187    }
188}