iron_shapes/
shape.rs

1// Copyright (c) 2018-2020 Thomas Kramer.
2// SPDX-FileCopyrightText: 2018-2022 Thomas Kramer
3//
4// SPDX-License-Identifier: AGPL-3.0-or-later
5
6//! Abstractions for geometrical shapes.
7
8use crate::prelude::*;
9use crate::traits::{MapPointwise, TryBoundingBox};
10use num_traits::{Num, NumCast};
11
12/// Abstracted geometrical shape.
13#[derive(PartialEq, Eq, Clone, Debug, Hash)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub enum Geometry<T> {
16    /// Point.
17    Point(Point<T>),
18    /// Edge.
19    Edge(Edge<T>),
20    /// Rect.
21    Rect(Rect<T>),
22    /// SimplePolygon.
23    SimplePolygon(SimplePolygon<T>),
24    /// SimpleRPolygon.
25    SimpleRPolygon(SimpleRPolygon<T>),
26    /// Polygon.
27    Polygon(Polygon<T>),
28    /// Path.
29    Path(Path<T>),
30    /// Text.
31    Text(Text<T>),
32}
33
34impl<T: CoordinateType> Geometry<T> {
35    /// Create a transformed copy of the geometric object.
36    pub fn transformed(&self, tf: &SimpleTransform<T>) -> Self {
37        let trans = |p| tf.transform_point(p);
38        match self {
39            Geometry::Point(p) => tf.transform_point(*p).into(),
40            Geometry::Edge(g) => g.transform(trans).into(),
41            Geometry::Rect(g) => g.transform(trans).into(),
42            Geometry::SimplePolygon(g) => g.transform(trans).into(),
43            Geometry::SimpleRPolygon(g) => g.transformed(tf).into(),
44            Geometry::Polygon(g) => g.transform(trans).into(),
45            Geometry::Path(p) => p.transform(tf).into(),
46            Geometry::Text(g) => g.transform(trans).into(),
47        }
48    }
49}
50
51/// Implement `From` for `Geometry`.
52macro_rules! geometry_from {
53    ( $t:tt ) => {
54        impl<T> From<$t<T>> for Geometry<T> {
55            fn from(x: $t<T>) -> Geometry<T> {
56                Geometry::$t(x)
57            }
58        }
59    };
60}
61
62// Implement `From<_<T>> for Geometry<T>` for all shapes.
63geometry_from!(Point);
64geometry_from!(Edge);
65geometry_from!(Rect);
66geometry_from!(SimplePolygon);
67geometry_from!(SimpleRPolygon);
68geometry_from!(Polygon);
69geometry_from!(Path);
70geometry_from!(Text);
71
72impl<T: Copy + PartialOrd + Num> TryBoundingBox<T> for Geometry<T> {
73    /// Calculate the bounding box of this geometrical shape by calling the bounding box method of the concrete type.
74    fn try_bounding_box(&self) -> Option<Rect<T>> {
75        match self {
76            Geometry::Point(p) => p.try_bounding_box(),
77            Geometry::Edge(e) => e.try_bounding_box(),
78            Geometry::Rect(e) => e.try_bounding_box(),
79            Geometry::SimplePolygon(e) => e.try_bounding_box(),
80            Geometry::SimpleRPolygon(e) => e.try_bounding_box(),
81            Geometry::Polygon(e) => e.try_bounding_box(),
82            Geometry::Path(p) => p.try_bounding_box(),
83            Geometry::Text(t) => t.try_bounding_box(),
84        }
85    }
86}
87
88// impl<T: CoordinateType> MapPointwise<T> for Geometry<T> {
89//     /// Point wise transformation.
90//     fn transform<F>(&self, transformation: F) -> Self
91//         where F: Fn(Point<T>) -> Point<T> {
92//         match self {
93//             Geometry::Point(e) => e.transform(transformation).into(),
94//             Geometry::Edge(e) => e.transform(transformation).into(),
95//             Geometry::Rect(e) => e.transform(transformation).into(),
96//             Geometry::SimplePolygon(e) => e.transform(transformation).into(),
97//             Geometry::Polygon(e) => e.transform(transformation).into(),
98//             Geometry::Path(p) => unimplemented!(),
99//             Geometry::Text(t) => t.transform(transformation).into(),
100//         }
101//     }
102// }
103
104impl<T: CoordinateType + NumCast> DoubledOrientedArea<T> for Geometry<T> {
105    /// Area calculation.
106    fn area_doubled_oriented(&self) -> T {
107        match self {
108            Geometry::Point(_) => T::zero(),
109            Geometry::Edge(_) => T::zero(),
110            Geometry::Rect(e) => e.area_doubled_oriented(),
111            Geometry::SimplePolygon(e) => e.area_doubled_oriented(),
112            Geometry::SimpleRPolygon(e) => e.area_doubled_oriented(),
113            Geometry::Polygon(e) => e.area_doubled_oriented(),
114            Geometry::Path(p) => {
115                // TODO: Find a way without type conversions.
116                T::from(FloatType::round(p.area_approx::<FloatType>() * 2.0_f64)).unwrap()
117            }
118            Geometry::Text(_) => T::zero(),
119        }
120    }
121}
122
123impl<T: CoordinateType + NumCast> ToPolygon<T> for Geometry<T> {
124    /// Convert a geometry into a polygon.
125    ///
126    /// The coordinate type must implement `NumCast` because there is currently
127    /// no way to convert a `Path` into a polygon without converting it to a float type first.
128    ///
129    /// # Examples
130    /// ```
131    /// use iron_shapes::prelude::*;
132    /// let rect = Rect::new((0, 0), (1, 2));
133    /// // Convert the rectangle to a `Geometry`.
134    /// let g: Geometry<_> = rect.into();
135    /// assert_eq!(g.to_polygon(), rect.to_polygon())
136    /// ```
137    fn to_polygon(&self) -> Polygon<T> {
138        match self {
139            Geometry::Point(_) => Polygon::empty(),
140            Geometry::Edge(_) => Polygon::empty(),
141            Geometry::Rect(e) => e.to_polygon(),
142            Geometry::SimplePolygon(e) => Polygon::from(e),
143            Geometry::SimpleRPolygon(p) => Polygon::from(p.to_simple_polygon()),
144            Geometry::Polygon(e) => e.clone(),
145            Geometry::Path(p) => p.to_polygon_approx().cast().into(),
146            Geometry::Text(_) => Polygon::empty(),
147        }
148    }
149}
150
151impl<T: CoordinateType + NumCast> From<Geometry<T>> for Polygon<T> {
152    /// Convert a geometry into a polygon.
153    fn from(g: Geometry<T>) -> Self {
154        g.to_polygon()
155    }
156}
157
158impl<T: CoordinateType + NumCast, Dst: CoordinateType + NumCast> TryCastCoord<T, Dst>
159    for Geometry<T>
160{
161    type Output = Geometry<Dst>;
162
163    fn try_cast(&self) -> Option<Self::Output> {
164        match self {
165            Geometry::Point(p) => p.try_cast().map(|s| s.into()),
166            Geometry::Edge(e) => e.try_cast().map(|s| s.into()),
167            Geometry::Rect(r) => r.try_cast().map(|s| s.into()),
168            Geometry::SimplePolygon(p) => p.try_cast().map(|s| s.into()),
169            Geometry::SimpleRPolygon(p) => p.try_cast().map(|s| s.into()),
170            Geometry::Polygon(p) => p.try_cast().map(|s| s.into()),
171            Geometry::Path(p) => p.try_cast().map(|s| s.into()),
172            Geometry::Text(t) => t.try_cast().map(|s| s.into()),
173        }
174    }
175}
176
177#[test]
178/// Sanity check to make sure that area computation is consistent with conversion to polygons.
179fn test_convert_to_polygon() {
180    let geometries: Vec<Geometry<_>> = vec![
181        Point::new(0, 0).into(),
182        Edge::new((0, 0), (1, 1)).into(),
183        Rect::new((0, 0), (1, 1)).into(),
184        SimplePolygon::from(vec![(0, 0), (1, 0), (1, 1)]).into(),
185        Polygon::new(vec![(0, 0), (1, 0), (1, 1)]).into(),
186    ];
187
188    for g in geometries {
189        assert_eq!(
190            g.area_doubled_oriented(),
191            g.to_polygon().area_doubled_oriented()
192        );
193    }
194}