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///
9/// A convex polygon is a closed 2D shape where all interior angles are less than 180 degrees,
10/// and any line segment drawn between two points inside the polygon stays entirely within the polygon.
11/// Common examples include triangles, rectangles, and regular polygons like hexagons.
12///
13/// # What is a convex polygon?
14///
15/// In 2D space, a polygon is **convex** if:
16/// - Every interior angle is less than or equal to 180 degrees
17/// - The line segment between any two points inside the polygon lies entirely inside the polygon
18/// - All vertices "bulge outward" - there are no indentations or concave areas
19///
20/// Examples of **convex** polygons: triangle, square, regular pentagon, regular hexagon
21/// Examples of **non-convex** (concave) polygons: star shapes, L-shapes, crescents
22///
23/// # Use cases
24///
25/// Convex polygons are widely used in:
26/// - **Game development**: Character hitboxes, platform boundaries, simple building shapes
27/// - **Physics simulations**: Rigid body collision detection (more efficient than arbitrary polygons)
28/// - **Robotics**: Simplified environment representations, obstacle boundaries
29/// - **Computer graphics**: Fast rendering primitives, clipping regions
30/// - **Computational geometry**: As building blocks for more complex operations
31///
32/// # Representation
33///
34/// This structure stores:
35/// - **Points**: The vertices of the polygon in counter-clockwise order
36/// - **Normals**: Unit vectors perpendicular to each edge, pointing outward
37///
38/// The normals are pre-computed for efficient collision detection algorithms.
39///
40/// # Example: Creating a simple triangle
41///
42/// ```
43/// # #[cfg(all(feature = "dim2", feature = "f32"))] {
44/// use parry2d::shape::ConvexPolygon;
45/// use nalgebra::Point2;
46///
47/// // Create a triangle from three vertices (counter-clockwise order)
48/// let vertices = vec![
49///     Point2::origin(),    // bottom-left
50///     Point2::new(2.0, 0.0),    // bottom-right
51///     Point2::new(1.0, 2.0),    // top
52/// ];
53///
54/// let triangle = ConvexPolygon::from_convex_polyline(vertices)
55///     .expect("Failed to create triangle");
56///
57/// // The polygon has 3 vertices
58/// assert_eq!(triangle.points().len(), 3);
59/// // and 3 edge normals (one per edge)
60/// assert_eq!(triangle.normals().len(), 3);
61/// # }
62/// ```
63#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
64#[cfg_attr(
65    feature = "rkyv",
66    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize),
67    archive(check_bytes)
68)]
69#[derive(Clone, Debug)]
70pub struct ConvexPolygon {
71    points: Vec<Point<Real>>,
72    normals: Vec<Unit<Vector<Real>>>,
73}
74
75impl ConvexPolygon {
76    /// Creates a new 2D convex polygon from an arbitrary set of points by computing their convex hull.
77    ///
78    /// This is the most flexible constructor - it automatically computes the **convex hull** of the
79    /// given points, which is the smallest convex polygon that contains all the input points.
80    /// Think of it as wrapping a rubber band around the points.
81    ///
82    /// Use this when:
83    /// - You have an arbitrary collection of points and want the convex boundary
84    /// - You're not sure if your points form a convex shape
85    /// - You want to simplify a point cloud to its convex outline
86    ///
87    /// # Returns
88    ///
89    /// - `Some(ConvexPolygon)` if successful
90    /// - `None` if the convex hull computation failed (e.g., all points are collinear or coincident)
91    ///
92    /// # Example: Creating a convex polygon from arbitrary points
93    ///
94    /// ```
95    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
96    /// use parry2d::shape::ConvexPolygon;
97    /// use nalgebra::Point2;
98    ///
99    /// // Some arbitrary points (including one inside the convex hull)
100    /// let points = vec![
101    ///     Point2::origin(),
102    ///     Point2::new(4.0, 0.0),
103    ///     Point2::new(2.0, 3.0),
104    ///     Point2::new(2.0, 1.0),  // This point is inside the triangle
105    /// ];
106    ///
107    /// let polygon = ConvexPolygon::from_convex_hull(&points)
108    ///     .expect("Failed to create convex hull");
109    ///
110    /// // The convex hull only has 3 vertices (the interior point was excluded)
111    /// assert_eq!(polygon.points().len(), 3);
112    /// # }
113    /// ```
114    ///
115    /// # Example: Simplifying a point cloud
116    ///
117    /// ```
118    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
119    /// use parry2d::shape::ConvexPolygon;
120    /// use nalgebra::Point2;
121    ///
122    /// // A cloud of points that roughly forms a circle
123    /// let mut points = Vec::new();
124    /// for i in 0..20 {
125    ///     let angle = (i as f32) * std::f32::consts::TAU / 20.0;
126    ///     points.push(Point2::new(angle.cos(), angle.sin()));
127    /// }
128    /// // Add some interior points
129    /// points.push(Point2::origin());
130    /// points.push(Point2::new(0.5, 0.5));
131    ///
132    /// let polygon = ConvexPolygon::from_convex_hull(&points)
133    ///     .expect("Failed to create convex hull");
134    ///
135    /// // The convex hull has 20 vertices (the boundary points)
136    /// assert_eq!(polygon.points().len(), 20);
137    /// # }
138    /// ```
139    pub fn from_convex_hull(points: &[Point<Real>]) -> Option<Self> {
140        let vertices = crate::transformation::convex_hull(points);
141        Self::from_convex_polyline(vertices)
142    }
143
144    /// Creates a new 2D convex polygon from vertices that already form a convex shape.
145    ///
146    /// This constructor is more efficient than [`from_convex_hull`] because it **assumes** the input
147    /// points already form a convex polygon in counter-clockwise order, and it doesn't compute the
148    /// convex hull. The convexity is **not verified** - if you pass non-convex points, the resulting
149    /// shape may behave incorrectly in collision detection.
150    ///
151    /// **Important**: Points must be ordered **counter-clockwise** (CCW). If you're unsure about the
152    /// ordering or convexity, use [`from_convex_hull`] instead.
153    ///
154    /// This method automatically removes collinear vertices (points that lie on the line between
155    /// their neighbors) to simplify the polygon. If you want to preserve all vertices exactly as given,
156    /// use [`from_convex_polyline_unmodified`].
157    ///
158    /// # When to use this
159    ///
160    /// Use this constructor when:
161    /// - You already know your points form a convex polygon
162    /// - The points are ordered counter-clockwise around the shape
163    /// - You want better performance by skipping convex hull computation
164    ///
165    /// # Returns
166    ///
167    /// - `Some(ConvexPolygon)` if successful
168    /// - `None` if all points are nearly collinear (form an almost flat line) or there are fewer than 3 vertices after removing collinear points
169    ///
170    /// # Example: Creating a square
171    ///
172    /// ```
173    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
174    /// use parry2d::shape::ConvexPolygon;
175    /// use nalgebra::Point2;
176    ///
177    /// // A square with vertices in counter-clockwise order
178    /// let square = ConvexPolygon::from_convex_polyline(vec![
179    ///     Point2::origin(),  // bottom-left
180    ///     Point2::new(1.0, 0.0),  // bottom-right
181    ///     Point2::new(1.0, 1.0),  // top-right
182    ///     Point2::new(0.0, 1.0),  // top-left
183    /// ]).expect("Failed to create square");
184    ///
185    /// assert_eq!(square.points().len(), 4);
186    /// # }
187    /// ```
188    ///
189    /// # Example: Collinear points are automatically removed
190    ///
191    /// ```
192    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
193    /// use parry2d::shape::ConvexPolygon;
194    /// use nalgebra::Point2;
195    ///
196    /// // A quadrilateral with one vertex on an edge (making it collinear)
197    /// let polygon = ConvexPolygon::from_convex_polyline(vec![
198    ///     Point2::origin(),
199    ///     Point2::new(2.0, 0.0),
200    ///     Point2::new(2.0, 1.0),   // This point is on the line from (2,0) to (2,2)
201    ///     Point2::new(2.0, 2.0),
202    ///     Point2::new(0.0, 2.0),
203    /// ]).expect("Failed to create polygon");
204    ///
205    /// // The collinear point at (2.0, 1.0) was removed, leaving a rectangle
206    /// assert_eq!(polygon.points().len(), 4);
207    /// # }
208    /// ```
209    ///
210    /// [`from_convex_hull`]: ConvexPolygon::from_convex_hull
211    /// [`from_convex_polyline_unmodified`]: ConvexPolygon::from_convex_polyline_unmodified
212    pub fn from_convex_polyline(mut points: Vec<Point<Real>>) -> Option<Self> {
213        if points.is_empty() {
214            return None;
215        }
216        let eps = ComplexField::sqrt(crate::math::DEFAULT_EPSILON);
217        let mut normals = Vec::with_capacity(points.len());
218        // First, compute all normals.
219        for i1 in 0..points.len() {
220            let i2 = (i1 + 1) % points.len();
221            normals.push(utils::ccw_face_normal([&points[i1], &points[i2]])?);
222        }
223
224        let mut nremoved = 0;
225        // See if the first vertex must be removed.
226        if normals[0].dot(&*normals[normals.len() - 1]) > 1.0 - eps {
227            nremoved = 1;
228        }
229
230        // Second, find vertices that can be removed because
231        // of collinearity of adjacent faces.
232        for i2 in 1..points.len() {
233            let i1 = i2 - 1;
234            if normals[i1].dot(&*normals[i2]) > 1.0 - eps {
235                // Remove
236                nremoved += 1;
237            } else {
238                points[i2 - nremoved] = points[i2];
239                normals[i2 - nremoved] = normals[i2];
240            }
241        }
242
243        let new_length = points.len() - nremoved;
244        points.truncate(new_length);
245        normals.truncate(new_length);
246
247        if points.len() > 2 {
248            Some(ConvexPolygon { points, normals })
249        } else {
250            None
251        }
252    }
253
254    /// Creates a new 2D convex polygon from a set of points assumed to
255    /// describe a counter-clockwise convex polyline.
256    ///
257    /// This is the same as [`ConvexPolygon::from_convex_polyline`] but without removing any point
258    /// from the input even if some are coplanar.
259    ///
260    /// Returns `None` if `points` doesn’t contain at least three points.
261    pub fn from_convex_polyline_unmodified(points: Vec<Point<Real>>) -> Option<Self> {
262        if points.len() <= 2 {
263            return None;
264        }
265        let mut normals = Vec::with_capacity(points.len());
266        // First, compute all normals.
267        for i1 in 0..points.len() {
268            let i2 = (i1 + 1) % points.len();
269            normals.push(utils::ccw_face_normal([&points[i1], &points[i2]])?);
270        }
271
272        Some(ConvexPolygon { points, normals })
273    }
274
275    /// Returns the vertices of this convex polygon.
276    ///
277    /// The vertices are stored in counter-clockwise order around the polygon.
278    /// This is a slice reference to the internal vertex storage.
279    ///
280    /// # Example
281    ///
282    /// ```
283    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
284    /// use parry2d::shape::ConvexPolygon;
285    /// use nalgebra::Point2;
286    ///
287    /// let triangle = ConvexPolygon::from_convex_polyline(vec![
288    ///     Point2::origin(),
289    ///     Point2::new(1.0, 0.0),
290    ///     Point2::new(0.5, 1.0),
291    /// ]).unwrap();
292    ///
293    /// let vertices = triangle.points();
294    /// assert_eq!(vertices.len(), 3);
295    /// assert_eq!(vertices[0], Point2::origin());
296    /// # }
297    /// ```
298    #[inline]
299    pub fn points(&self) -> &[Point<Real>] {
300        &self.points
301    }
302
303    /// Returns the outward-pointing normals of the edges of this convex polygon.
304    ///
305    /// Each normal is a unit vector perpendicular to an edge, pointing outward from the polygon.
306    /// The normals are stored in the same order as the edges, so `normals()[i]` is the normal
307    /// for the edge from `points()[i]` to `points()[(i+1) % len]`.
308    ///
309    /// These pre-computed normals are used internally for efficient collision detection.
310    ///
311    /// # Example
312    ///
313    /// ```
314    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
315    /// use parry2d::shape::ConvexPolygon;
316    /// use nalgebra::{Point2, Vector2};
317    ///
318    /// // Create a square aligned with the axes
319    /// let square = ConvexPolygon::from_convex_polyline(vec![
320    ///     Point2::origin(),  // bottom-left
321    ///     Point2::new(1.0, 0.0),  // bottom-right
322    ///     Point2::new(1.0, 1.0),  // top-right
323    ///     Point2::new(0.0, 1.0),  // top-left
324    /// ]).unwrap();
325    ///
326    /// let normals = square.normals();
327    /// assert_eq!(normals.len(), 4);
328    ///
329    /// // The first normal points downward (perpendicular to bottom edge)
330    /// let bottom_normal = normals[0];
331    /// assert!((bottom_normal.y - (-1.0)).abs() < 1e-5);
332    /// assert!(bottom_normal.x.abs() < 1e-5);
333    /// # }
334    /// ```
335    #[inline]
336    pub fn normals(&self) -> &[Unit<Vector<Real>>] {
337        &self.normals
338    }
339
340    /// Computes a scaled version of this convex polygon.
341    ///
342    /// This method scales the polygon by multiplying each vertex coordinate by the corresponding
343    /// component of the `scale` vector. This allows for non-uniform scaling (different scale factors
344    /// for x and y axes).
345    ///
346    /// The normals are also updated to reflect the scaling transformation.
347    ///
348    /// # Returns
349    ///
350    /// - `Some(ConvexPolygon)` with the scaled shape
351    /// - `None` if the scaling results in degenerate normals (e.g., if the scale factor along
352    ///   one axis is zero or nearly zero)
353    ///
354    /// # Example: Uniform scaling
355    ///
356    /// ```
357    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
358    /// use parry2d::shape::ConvexPolygon;
359    /// use nalgebra::{Point2, Vector2};
360    ///
361    /// let triangle = ConvexPolygon::from_convex_polyline(vec![
362    ///     Point2::origin(),
363    ///     Point2::new(1.0, 0.0),
364    ///     Point2::new(0.5, 1.0),
365    /// ]).unwrap();
366    ///
367    /// // Scale uniformly by 2x
368    /// let scaled = triangle.scaled(&Vector2::new(2.0, 2.0))
369    ///     .expect("Failed to scale");
370    ///
371    /// // All coordinates are doubled
372    /// assert_eq!(scaled.points()[1], Point2::new(2.0, 0.0));
373    /// assert_eq!(scaled.points()[2], Point2::new(1.0, 2.0));
374    /// # }
375    /// ```
376    ///
377    /// # Example: Non-uniform scaling
378    ///
379    /// ```
380    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
381    /// use parry2d::shape::ConvexPolygon;
382    /// use nalgebra::{Point2, Vector2};
383    ///
384    /// let square = ConvexPolygon::from_convex_polyline(vec![
385    ///     Point2::origin(),
386    ///     Point2::new(1.0, 0.0),
387    ///     Point2::new(1.0, 1.0),
388    ///     Point2::new(0.0, 1.0),
389    /// ]).unwrap();
390    ///
391    /// // Scale to make it wider (2x) and taller (3x)
392    /// let rectangle = square.scaled(&Vector2::new(2.0, 3.0))
393    ///     .expect("Failed to scale");
394    ///
395    /// assert_eq!(rectangle.points()[2], Point2::new(2.0, 3.0));
396    /// # }
397    /// ```
398    pub fn scaled(mut self, scale: &Vector<Real>) -> Option<Self> {
399        self.points
400            .iter_mut()
401            .for_each(|pt| pt.coords.component_mul_assign(scale));
402
403        for n in &mut self.normals {
404            *n = Unit::try_new(n.component_mul(scale), 0.0)?;
405        }
406
407        Some(self)
408    }
409
410    /// Returns a mitered offset (expanded or contracted) version of the polygon.
411    ///
412    /// This method creates a new polygon by moving each edge outward (or inward for negative values)
413    /// by the specified `amount`. The vertices are adjusted to maintain sharp corners (mitered joints).
414    /// This is also known as "polygon dilation" or "Minkowski sum with a circle".
415    ///
416    /// # Use cases
417    ///
418    /// - Creating "safe zones" or margins around objects
419    /// - Implementing "thick" polygon collision detection
420    /// - Creating rounded rectangular shapes (when combined with rounding)
421    /// - Growing or shrinking shapes for morphological operations
422    ///
423    /// # Arguments
424    ///
425    /// * `amount` - The distance to move each edge. Positive values expand the polygon outward,
426    ///   making it larger. Must be a non-negative finite number.
427    ///
428    /// # Panics
429    ///
430    /// Panics if `amount` is not a non-negative finite number (NaN, infinity, or negative).
431    ///
432    /// # Example: Expanding a triangle
433    ///
434    /// ```
435    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
436    /// use parry2d::shape::ConvexPolygon;
437    /// use nalgebra::Point2;
438    ///
439    /// let triangle = ConvexPolygon::from_convex_polyline(vec![
440    ///     Point2::origin(),
441    ///     Point2::new(2.0, 0.0),
442    ///     Point2::new(1.0, 2.0),
443    /// ]).unwrap();
444    ///
445    /// // Expand the triangle by 0.5 units
446    /// let expanded = triangle.offsetted(0.5);
447    ///
448    /// // The expanded triangle has the same number of vertices
449    /// assert_eq!(expanded.points().len(), 3);
450    /// // But the vertices have moved outward
451    /// // (exact positions depend on the miter calculation)
452    /// # }
453    /// ```
454    ///
455    /// # Example: Creating a margin around a square
456    ///
457    /// ```
458    /// # #[cfg(all(feature = "dim2", feature = "f32"))] {
459    /// use parry2d::shape::ConvexPolygon;
460    /// use nalgebra::Point2;
461    ///
462    /// let square = ConvexPolygon::from_convex_polyline(vec![
463    ///     Point2::origin(),
464    ///     Point2::new(1.0, 0.0),
465    ///     Point2::new(1.0, 1.0),
466    ///     Point2::new(0.0, 1.0),
467    /// ]).unwrap();
468    ///
469    /// // Create a 0.2 unit margin around the square
470    /// let margin = square.offsetted(0.2);
471    ///
472    /// // The shape is still a square (4 vertices)
473    /// assert_eq!(margin.points().len(), 4);
474    /// # }
475    /// ```
476    pub fn offsetted(&self, amount: Real) -> Self {
477        if !amount.is_finite() || amount < 0. {
478            panic!(
479                "Offset amount must be a non-negative finite number, got {}.",
480                amount
481            );
482        }
483
484        let mut points = Vec::with_capacity(self.points.len());
485        let normals = self.normals.clone();
486
487        for i2 in 0..self.points.len() {
488            let i1 = if i2 == 0 {
489                self.points.len() - 1
490            } else {
491                i2 - 1
492            };
493            let normal_a = normals[i1];
494            let direction = normal_a.into_inner() + normals[i2].into_inner();
495            points.push(self.points[i2] + (amount / direction.dot(&normal_a)) * direction);
496        }
497
498        ConvexPolygon { points, normals }
499    }
500
501    /// Get the ID of the feature with a normal that maximizes the dot product with `local_dir`.
502    pub fn support_feature_id_toward(&self, local_dir: &Unit<Vector<Real>>) -> FeatureId {
503        let eps: Real = Real::pi() / 180.0;
504        let ceps = ComplexField::cos(eps);
505
506        // Check faces.
507        for i in 0..self.normals.len() {
508            let normal = &self.normals[i];
509
510            if normal.dot(local_dir.as_ref()) >= ceps {
511                return FeatureId::Face(i as u32);
512            }
513        }
514
515        // Support vertex.
516        FeatureId::Vertex(
517            utils::point_cloud_support_point_id(local_dir.as_ref(), &self.points) as u32,
518        )
519    }
520
521    /// The normal of the given feature.
522    pub fn feature_normal(&self, feature: FeatureId) -> Option<Unit<Vector<Real>>> {
523        match feature {
524            FeatureId::Face(id) => Some(self.normals[id as usize]),
525            FeatureId::Vertex(id2) => {
526                let id1 = if id2 == 0 {
527                    self.normals.len() - 1
528                } else {
529                    id2 as usize - 1
530                };
531                Some(Unit::new_normalize(
532                    *self.normals[id1] + *self.normals[id2 as usize],
533                ))
534            }
535            _ => None,
536        }
537    }
538}
539
540impl SupportMap for ConvexPolygon {
541    #[inline]
542    fn local_support_point(&self, dir: &Vector<Real>) -> Point<Real> {
543        utils::point_cloud_support_point(dir, self.points())
544    }
545}
546
547impl PolygonalFeatureMap for ConvexPolygon {
548    fn local_support_feature(&self, dir: &Unit<Vector<Real>>, out_feature: &mut PolygonalFeature) {
549        let cuboid = crate::shape::Cuboid::new(self.points[2].coords);
550        cuboid.local_support_feature(dir, out_feature);
551        let mut best_face = 0;
552        let mut max_dot = self.normals[0].dot(dir);
553
554        for i in 1..self.normals.len() {
555            let dot = self.normals[i].dot(dir);
556
557            if dot > max_dot {
558                max_dot = dot;
559                best_face = i;
560            }
561        }
562
563        let i1 = best_face;
564        let i2 = (best_face + 1) % self.points.len();
565        *out_feature = PolygonalFeature {
566            vertices: [self.points[i1], self.points[i2]],
567            vids: PackedFeatureId::vertices([i1 as u32 * 2, i2 as u32 * 2]),
568            fid: PackedFeatureId::face(i1 as u32 * 2 + 1),
569            num_vertices: 2,
570        };
571    }
572}
573
574/*
575impl ConvexPolyhedron for ConvexPolygon {
576    fn vertex(&self, id: FeatureId) -> Point<Real> {
577        self.points[id.unwrap_vertex() as usize]
578    }
579
580    fn face(&self, id: FeatureId, out: &mut ConvexPolygonalFeature) {
581        out.clear();
582
583        let ia = id.unwrap_face() as usize;
584        let ib = (ia + 1) % self.points.len();
585        out.push(self.points[ia], FeatureId::Vertex(ia as u32));
586        out.push(self.points[ib], FeatureId::Vertex(ib as u32));
587
588        out.set_normal(self.normals[ia as usize]);
589        out.set_feature_id(FeatureId::Face(ia as u32));
590    }
591
592
593
594    fn support_face_toward(
595        &self,
596        m: &Isometry<Real>,
597        dir: &Unit<Vector<Real>>,
598        out: &mut ConvexPolygonalFeature,
599    ) {
600        let ls_dir = m.inverse_transform_vector(dir);
601        let mut best_face = 0;
602        let mut max_dot = self.normals[0].dot(&ls_dir);
603
604        for i in 1..self.points.len() {
605            let dot = self.normals[i].dot(&ls_dir);
606
607            if dot > max_dot {
608                max_dot = dot;
609                best_face = i;
610            }
611        }
612
613        self.face(FeatureId::Face(best_face as u32), out);
614        out.transform_by(m);
615    }
616
617    fn support_feature_toward(
618        &self,
619        transform: &Isometry<Real>,
620        dir: &Unit<Vector<Real>>,
621        _angle: Real,
622        out: &mut ConvexPolygonalFeature,
623    ) {
624        out.clear();
625        // TODO: actually find the support feature.
626        self.support_face_toward(transform, dir, out)
627    }
628
629    fn support_feature_id_toward(&self, local_dir: &Unit<Vector<Real>>) -> FeatureId {
630        let eps: Real = na::convert::<f64, Real>(f64::consts::PI / 180.0);
631        let ceps = ComplexField::cos(eps);
632
633        // Check faces.
634        for i in 0..self.normals.len() {
635            let normal = &self.normals[i];
636
637            if normal.dot(local_dir.as_ref()) >= ceps {
638                return FeatureId::Face(i as u32);
639            }
640        }
641
642        // Support vertex.
643        FeatureId::Vertex(
644            utils::point_cloud_support_point_id(local_dir.as_ref(), &self.points) as u32,
645        )
646    }
647}
648*/
649
650#[cfg(feature = "dim2")]
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn test_dilation() {
657        let polygon = ConvexPolygon::from_convex_polyline(vec![
658            Point::new(1., 0.),
659            Point::new(-1., 0.),
660            Point::new(0., -1.),
661        ])
662        .unwrap();
663
664        let offsetted = polygon.offsetted(0.5);
665        let expected = vec![
666            Point::new(2.207, 0.5),
667            Point::new(-2.207, 0.5),
668            Point::new(0., -1.707),
669        ];
670
671        assert_eq!(offsetted.points().len(), 3);
672        assert!(offsetted
673            .points()
674            .iter()
675            .zip(expected.iter())
676            .all(|(a, b)| (a.coords - b.coords).magnitude() < 0.001));
677    }
678}