parry2d/shape/
convex_polygon.rs

1use crate::math::{Point, Real, Vector};
2use crate::shape::{FeatureId, PackedFeatureId, PolygonalFeature, PolygonalFeatureMap, SupportMap};
3use crate::utils;
4use alloc::vec::Vec;
5use na::{self, ComplexField, RealField, Unit};
6
7/// A 2D convex polygon.
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9#[cfg_attr(
10    feature = "rkyv",
11    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
12    archive(check_bytes)
13)]
14#[derive(Clone, Debug)]
15pub struct ConvexPolygon {
16    points: Vec<Point<Real>>,
17    normals: Vec<Unit<Vector<Real>>>,
18}
19
20impl ConvexPolygon {
21    /// Creates a new 2D convex polygon from an arbitrary set of points.
22    ///
23    /// This explicitly computes the convex hull of the given set of points.
24    /// Returns `None` if the convex hull computation failed.
25    pub fn from_convex_hull(points: &[Point<Real>]) -> Option<Self> {
26        let vertices = crate::transformation::convex_hull(points);
27        Self::from_convex_polyline(vertices)
28    }
29
30    /// Creates a new 2D convex polygon from a set of points assumed to
31    /// describe a counter-clockwise convex polyline.
32    ///
33    /// This does **not** compute the convex-hull of the input points: convexity of the input is
34    /// assumed and not checked. For a version that calculates the convex hull of the given points,
35    /// use [`ConvexPolygon::from_convex_hull`] instead.
36    ///
37    /// The generated [`ConvexPolygon`] will contain the given `points` with any point
38    /// collinear to the previous and next ones removed. For a version that leaves the input
39    /// `points` unmodified, use [`ConvexPolygon::from_convex_polyline_unmodified`].
40    ///
41    /// Returns `None` if all points form an almost flat line.
42    pub fn from_convex_polyline(mut points: Vec<Point<Real>>) -> Option<Self> {
43        if points.is_empty() {
44            return None;
45        }
46        let eps = ComplexField::sqrt(crate::math::DEFAULT_EPSILON);
47        let mut normals = Vec::with_capacity(points.len());
48        // First, compute all normals.
49        for i1 in 0..points.len() {
50            let i2 = (i1 + 1) % points.len();
51            normals.push(utils::ccw_face_normal([&points[i1], &points[i2]])?);
52        }
53
54        let mut nremoved = 0;
55        // See if the first vertex must be removed.
56        if normals[0].dot(&*normals[normals.len() - 1]) > 1.0 - eps {
57            nremoved = 1;
58        }
59
60        // Second, find vertices that can be removed because
61        // of collinearity of adjacent faces.
62        for i2 in 1..points.len() {
63            let i1 = i2 - 1;
64            if normals[i1].dot(&*normals[i2]) > 1.0 - eps {
65                // Remove
66                nremoved += 1;
67            } else {
68                points[i2 - nremoved] = points[i2];
69                normals[i2 - nremoved] = normals[i2];
70            }
71        }
72
73        let new_length = points.len() - nremoved;
74        points.truncate(new_length);
75        normals.truncate(new_length);
76
77        if points.len() > 2 {
78            Some(ConvexPolygon { points, normals })
79        } else {
80            None
81        }
82    }
83
84    /// Creates a new 2D convex polygon from a set of points assumed to
85    /// describe a counter-clockwise convex polyline.
86    ///
87    /// This is the same as [`ConvexPolygon::from_convex_polyline`] but without removing any point
88    /// from the input even if some are coplanar.
89    ///
90    /// Returns `None` if `points` doesn’t contain at least three points.
91    pub fn from_convex_polyline_unmodified(points: Vec<Point<Real>>) -> Option<Self> {
92        if points.len() <= 2 {
93            return None;
94        }
95        let mut normals = Vec::with_capacity(points.len());
96        // First, compute all normals.
97        for i1 in 0..points.len() {
98            let i2 = (i1 + 1) % points.len();
99            normals.push(utils::ccw_face_normal([&points[i1], &points[i2]])?);
100        }
101
102        Some(ConvexPolygon { points, normals })
103    }
104
105    /// The vertices of this convex polygon.
106    #[inline]
107    pub fn points(&self) -> &[Point<Real>] {
108        &self.points
109    }
110
111    /// The normals of the edges of this convex polygon.
112    #[inline]
113    pub fn normals(&self) -> &[Unit<Vector<Real>>] {
114        &self.normals
115    }
116
117    /// Computes a scaled version of this convex polygon.
118    ///
119    /// Returns `None` if the result had degenerate normals (for example if
120    /// the scaling factor along one axis is zero).
121    pub fn scaled(mut self, scale: &Vector<Real>) -> Option<Self> {
122        self.points
123            .iter_mut()
124            .for_each(|pt| pt.coords.component_mul_assign(scale));
125
126        for n in &mut self.normals {
127            *n = Unit::try_new(n.component_mul(scale), 0.0)?;
128        }
129
130        Some(self)
131    }
132
133    /// Returns a mitered offset of the polygon.
134    ///
135    /// # Arguments
136    ///
137    /// * `amount` - size of the inflation. Each edge is moved outwards by this
138    ///   amount.
139    ///
140    /// # Panics
141    ///
142    /// Panics if `amount` is not a non-negative finite number.
143    pub fn offsetted(&self, amount: Real) -> Self {
144        if !amount.is_finite() || amount < 0. {
145            panic!(
146                "Offset amount must be a non-negative finite number, got {}.",
147                amount
148            );
149        }
150
151        let mut points = Vec::with_capacity(self.points.len());
152        let normals = self.normals.clone();
153
154        for i2 in 0..self.points.len() {
155            let i1 = if i2 == 0 {
156                self.points.len() - 1
157            } else {
158                i2 - 1
159            };
160            let normal_a = normals[i1];
161            let direction = normal_a.into_inner() + normals[i2].into_inner();
162            points.push(self.points[i2] + (amount / direction.dot(&normal_a)) * direction);
163        }
164
165        ConvexPolygon { points, normals }
166    }
167
168    /// Get the ID of the feature with a normal that maximizes the dot product with `local_dir`.
169    pub fn support_feature_id_toward(&self, local_dir: &Unit<Vector<Real>>) -> FeatureId {
170        let eps: Real = Real::pi() / 180.0;
171        let ceps = ComplexField::cos(eps);
172
173        // Check faces.
174        for i in 0..self.normals.len() {
175            let normal = &self.normals[i];
176
177            if normal.dot(local_dir.as_ref()) >= ceps {
178                return FeatureId::Face(i as u32);
179            }
180        }
181
182        // Support vertex.
183        FeatureId::Vertex(
184            utils::point_cloud_support_point_id(local_dir.as_ref(), &self.points) as u32,
185        )
186    }
187
188    /// The normal of the given feature.
189    pub fn feature_normal(&self, feature: FeatureId) -> Option<Unit<Vector<Real>>> {
190        match feature {
191            FeatureId::Face(id) => Some(self.normals[id as usize]),
192            FeatureId::Vertex(id2) => {
193                let id1 = if id2 == 0 {
194                    self.normals.len() - 1
195                } else {
196                    id2 as usize - 1
197                };
198                Some(Unit::new_normalize(
199                    *self.normals[id1] + *self.normals[id2 as usize],
200                ))
201            }
202            _ => None,
203        }
204    }
205}
206
207impl SupportMap for ConvexPolygon {
208    #[inline]
209    fn local_support_point(&self, dir: &Vector<Real>) -> Point<Real> {
210        utils::point_cloud_support_point(dir, self.points())
211    }
212}
213
214impl PolygonalFeatureMap for ConvexPolygon {
215    fn local_support_feature(&self, dir: &Unit<Vector<Real>>, out_feature: &mut PolygonalFeature) {
216        let cuboid = crate::shape::Cuboid::new(self.points[2].coords);
217        cuboid.local_support_feature(dir, out_feature);
218        let mut best_face = 0;
219        let mut max_dot = self.normals[0].dot(dir);
220
221        for i in 1..self.normals.len() {
222            let dot = self.normals[i].dot(dir);
223
224            if dot > max_dot {
225                max_dot = dot;
226                best_face = i;
227            }
228        }
229
230        let i1 = best_face;
231        let i2 = (best_face + 1) % self.points.len();
232        *out_feature = PolygonalFeature {
233            vertices: [self.points[i1], self.points[i2]],
234            vids: PackedFeatureId::vertices([i1 as u32 * 2, i2 as u32 * 2]),
235            fid: PackedFeatureId::face(i1 as u32 * 2 + 1),
236            num_vertices: 2,
237        };
238    }
239}
240
241/*
242impl ConvexPolyhedron for ConvexPolygon {
243    fn vertex(&self, id: FeatureId) -> Point<Real> {
244        self.points[id.unwrap_vertex() as usize]
245    }
246
247    fn face(&self, id: FeatureId, out: &mut ConvexPolygonalFeature) {
248        out.clear();
249
250        let ia = id.unwrap_face() as usize;
251        let ib = (ia + 1) % self.points.len();
252        out.push(self.points[ia], FeatureId::Vertex(ia as u32));
253        out.push(self.points[ib], FeatureId::Vertex(ib as u32));
254
255        out.set_normal(self.normals[ia as usize]);
256        out.set_feature_id(FeatureId::Face(ia as u32));
257    }
258
259
260
261    fn support_face_toward(
262        &self,
263        m: &Isometry<Real>,
264        dir: &Unit<Vector<Real>>,
265        out: &mut ConvexPolygonalFeature,
266    ) {
267        let ls_dir = m.inverse_transform_vector(dir);
268        let mut best_face = 0;
269        let mut max_dot = self.normals[0].dot(&ls_dir);
270
271        for i in 1..self.points.len() {
272            let dot = self.normals[i].dot(&ls_dir);
273
274            if dot > max_dot {
275                max_dot = dot;
276                best_face = i;
277            }
278        }
279
280        self.face(FeatureId::Face(best_face as u32), out);
281        out.transform_by(m);
282    }
283
284    fn support_feature_toward(
285        &self,
286        transform: &Isometry<Real>,
287        dir: &Unit<Vector<Real>>,
288        _angle: Real,
289        out: &mut ConvexPolygonalFeature,
290    ) {
291        out.clear();
292        // TODO: actually find the support feature.
293        self.support_face_toward(transform, dir, out)
294    }
295
296    fn support_feature_id_toward(&self, local_dir: &Unit<Vector<Real>>) -> FeatureId {
297        let eps: Real = na::convert::<f64, Real>(f64::consts::PI / 180.0);
298        let ceps = ComplexField::cos(eps);
299
300        // Check faces.
301        for i in 0..self.normals.len() {
302            let normal = &self.normals[i];
303
304            if normal.dot(local_dir.as_ref()) >= ceps {
305                return FeatureId::Face(i as u32);
306            }
307        }
308
309        // Support vertex.
310        FeatureId::Vertex(
311            utils::point_cloud_support_point_id(local_dir.as_ref(), &self.points) as u32,
312        )
313    }
314}
315*/
316
317#[cfg(feature = "dim2")]
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_dilation() {
324        let polygon = ConvexPolygon::from_convex_polyline(vec![
325            Point::new(1., 0.),
326            Point::new(-1., 0.),
327            Point::new(0., -1.),
328        ])
329        .unwrap();
330
331        let offsetted = polygon.offsetted(0.5);
332        let expected = vec![
333            Point::new(2.207, 0.5),
334            Point::new(-2.207, 0.5),
335            Point::new(0., -1.707),
336        ];
337
338        assert_eq!(offsetted.points().len(), 3);
339        assert!(offsetted
340            .points()
341            .iter()
342            .zip(expected.iter())
343            .all(|(a, b)| (a.coords - b.coords).magnitude() < 0.001));
344    }
345}