Skip to main content

rscad_core/objects/two_dimensional/
polygon.rs

1use crate::prelude_::*;
2
3/// A polygon with a fixed number of points.
4/// Now openscad uses points to initialize polygons which is fine but not very ergonomic.
5#[derive(Clone, Copy, Debug, PartialEq)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
8pub struct Polygon<const N: usize> {
9    pub radius: f64,
10}
11
12pub type Triangle = Polygon<3>;
13pub type SquarePolygon = Polygon<4>;
14pub type Pentagon = Polygon<5>;
15pub type Hexagon = Polygon<6>;
16pub type Heptagon = Polygon<7>;
17pub type Octagon = Polygon<8>;
18
19impl<const N: usize> Polygon<N> {
20    /// Create a regular polygon inscribed in a circle with the given radius.
21    pub const fn with_radius(radius: f64) -> Self {
22        Self { radius }
23    }
24
25    /// Create a regular polygon inscribed in a circle with the given diameter.
26    pub const fn with_diameter(diameter: f64) -> Self {
27        let radius = diameter / 2.0;
28        Self::with_radius(radius)
29    }
30
31    /// side = (2 * r * sin(angle / 2.0))
32    /// where angle = 2 * PI / N
33    /// So the radius is calculated as:
34    /// radius = side / (2 * sin(PI / N))
35    pub fn with_side(length: f64) -> Self {
36        let radius = N as f64 / (2.0 * (core::f64::consts::PI / N as f64).sin()) * length;
37        Self::with_radius(radius)
38    }
39
40    pub fn with_apothem(apothem: f64) -> Self {
41        let radius = apothem / (core::f64::consts::PI / N as f64).cos();
42        Self::with_radius(radius)
43    }
44
45    pub fn side(&self) -> f64 {
46        2.0 * self.radius * (core::f64::consts::PI / N as f64).sin()
47    }
48
49    pub fn apothem(&self) -> f64 {
50        self.radius * (core::f64::consts::PI / N as f64).cos()
51    }
52
53    pub fn radius(&self) -> f64 {
54        self.radius
55    }
56
57    pub fn points(&self) -> core::array::IntoIter<DVec2, N> {
58        self.points_array().into_iter()
59    }
60
61    pub fn points_array(&self) -> [glam::DVec2; N] {
62        core::array::from_fn(|i| {
63            let angle = 2.0 * core::f64::consts::PI * i as f64 / N as f64;
64            glam::DVec2::new(self.radius * angle.cos(), self.radius * angle.sin())
65        })
66    }
67
68    #[allow(clippy::type_complexity)]
69    pub fn sides(
70        &self,
71    ) -> core::iter::Map<
72        itertools::CircularTupleWindows<core::array::IntoIter<DVec2, N>, (DVec2, DVec2)>,
73        impl FnMut((DVec2, DVec2)) -> [DVec2; 2],
74    > {
75        use itertools::Itertools;
76        self.points_array()
77            .into_iter()
78            .circular_tuple_windows()
79            .map(|(a, b)| [a, b])
80    }
81}
82
83impl<const N: usize> Object for Polygon<N> {}
84
85#[test]
86pub fn test_hexagon_side_and_apothem() {
87    let hexagon = Hexagon::with_radius(10.0);
88    assert_eq!(
89        hexagon.side(),
90        10.0 * (core::f64::consts::PI / 6.0).sin() * 2.0
91    );
92    assert_eq!(
93        hexagon.apothem(),
94        10.0 * (core::f64::consts::PI / 6.0).cos()
95    );
96    assert!(hexagon.apothem() - 10.0 / 2.0 * 3.0f64.sqrt() < 0.00000000000001);
97    assert!(hexagon.side() - hexagon.radius() < 0.00000000000001);
98}
99
100#[derive(Clone, Copy, Debug, PartialEq)]
101#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
102#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
103#[cfg(feature = "std")]
104pub struct PolygonDynamic {
105    pub radius: f64,
106    pub points: usize,
107}
108
109#[cfg(feature = "std")]
110impl Object for PolygonDynamic {}
111
112#[cfg(feature = "std")]
113impl PolygonDynamic {
114    pub fn new(radius: f64, points: usize) -> Self {
115        Self { radius, points }
116    }
117
118    pub fn apothem(&self) -> f64 {
119        self.radius * (core::f64::consts::PI / self.points as f64).cos()
120    }
121
122    pub fn radius(&self) -> f64 {
123        self.radius
124    }
125
126    pub fn points(&self) -> core::iter::Map<core::ops::Range<usize>, impl FnMut(usize) -> DVec2> {
127        (0..self.points).map(|i| {
128            let angle = 2.0 * core::f64::consts::PI * i as f64 / self.points as f64;
129            glam::DVec2::new(self.radius * angle.cos(), self.radius * angle.sin())
130        })
131    }
132
133    #[allow(clippy::type_complexity)]
134    pub fn sides(
135        &self,
136    ) -> core::iter::Map<
137        itertools::CircularTupleWindows<std::vec::IntoIter<DVec2>, (DVec2, DVec2)>,
138        impl FnMut((DVec2, DVec2)) -> [DVec2; 2],
139    > {
140        use itertools::Itertools;
141        self.points()
142            .collect_vec()
143            .into_iter()
144            .circular_tuple_windows()
145            .map(|(a, b)| [a, b])
146    }
147}
148
149#[cfg(feature = "std")]
150impl<const N: usize> From<Polygon<N>> for PolygonDynamic {
151    fn from(polygon: Polygon<N>) -> Self {
152        Self {
153            radius: polygon.radius,
154            points: N,
155        }
156    }
157}
158
159#[cfg(feature = "std")]
160impl<const N: usize> TryFrom<PolygonDynamic> for Polygon<N> {
161    type Error = Report<Error>;
162
163    fn try_from(value: PolygonDynamic) -> Result<Self, Self::Error> {
164        if value.points == N {
165            Ok(Polygon::with_radius(value.radius))
166        } else {
167            Err(Error)
168                .attach("Mismatched number of points")
169                .attach_with(|| format!("Expected {}, got {}", N, value.points))
170        }
171    }
172}