parry2d/shape/
convex_polygon.rs

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