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}