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}