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}