parry2d/shape/
capsule.rs

1use crate::math::{Pose, Real, Rotation, Vector};
2use crate::shape::{Segment, SupportMap};
3
4#[cfg(feature = "alloc")]
5use either::Either;
6
7#[derive(Copy, Clone, Debug)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
10#[cfg_attr(feature = "encase", derive(encase::ShaderType))]
11#[cfg_attr(
12    feature = "rkyv",
13    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
14)]
15#[repr(C)]
16/// A capsule shape, also known as a pill or capped cylinder.
17///
18/// A capsule is defined by a line segment (its central axis) and a radius. It can be
19/// visualized as a cylinder with hemispherical (2D: semicircular) caps on both ends.
20/// This makes it perfect for representing elongated round objects.
21///
22/// # Structure
23///
24/// - **Segment**: The central axis from point `a` to point `b`
25/// - **Radius**: The thickness around the segment
26/// - **In 2D**: Looks like a rounded rectangle or "stadium" shape
27/// - **In 3D**: Looks like a cylinder with spherical caps (a pill)
28///
29/// # Properties
30///
31/// - **Convex**: Yes, capsules are always convex
32/// - **Smooth**: Completely smooth surface (no edges or corners)
33/// - **Support mapping**: Efficient (constant time)
34/// - **Rolling**: Excellent for objects that need to roll smoothly
35///
36/// # Use Cases
37///
38/// Capsules are ideal for:
39/// - Characters and humanoid figures (torso, limbs)
40/// - Pills, medicine capsules
41/// - Elongated projectiles (missiles, torpedoes)
42/// - Smooth rolling objects
43/// - Any object that's "cylinder-like" but needs smooth collision at ends
44///
45/// # Advantages Over Cylinders
46///
47/// - **No sharp edges**: Smoother collision response
48/// - **Better for characters**: More natural movement and rotation
49/// - **Simpler collision detection**: Easier to compute contacts than cylinders
50///
51/// # Example
52///
53/// ```rust
54/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
55/// use parry3d::shape::Capsule;
56/// use parry3d::math::Vector;
57///
58/// // Create a vertical capsule (aligned with Y axis)
59/// // Half-height of 2.0 means the segment is 4.0 units long
60/// let capsule = Capsule::new_y(2.0, 0.5);
61/// assert_eq!(capsule.radius, 0.5);
62/// assert_eq!(capsule.height(), 4.0);
63///
64/// // Create a custom capsule between two points
65/// let a = Vector::ZERO;
66/// let b = Vector::new(3.0, 4.0, 0.0);
67/// let custom = Capsule::new(a, b, 1.0);
68/// assert_eq!(custom.height(), 5.0); // Distance from a to b
69/// # }
70/// ```
71pub struct Capsule {
72    /// The line segment forming the capsule's central axis.
73    ///
74    /// The capsule extends from `segment.a` to `segment.b`, with hemispherical
75    /// caps centered at each endpoint.
76    pub segment: Segment,
77
78    /// The radius of the capsule.
79    ///
80    /// This is the distance from the central axis to the surface. Must be positive.
81    /// The total "thickness" of the capsule is `2 * radius`.
82    pub radius: Real,
83}
84
85impl Capsule {
86    /// Creates a new capsule aligned with the X axis.
87    ///
88    /// The capsule is centered at the origin and extends along the X axis.
89    ///
90    /// # Arguments
91    ///
92    /// * `half_height` - Half the length of the central segment (total length = `2 * half_height`)
93    /// * `radius` - The radius of the capsule
94    ///
95    /// # Example
96    ///
97    /// ```
98    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
99    /// use parry3d::shape::Capsule;
100    ///
101    /// // Create a capsule extending 6 units along X axis (3 units in each direction)
102    /// // with radius 0.5
103    /// let capsule = Capsule::new_x(3.0, 0.5);
104    /// assert_eq!(capsule.height(), 6.0);
105    /// assert_eq!(capsule.radius, 0.5);
106    ///
107    /// // The center is at the origin
108    /// let center = capsule.center();
109    /// assert!(center.length() < 1e-6);
110    /// # }
111    /// ```
112    pub fn new_x(half_height: Real, radius: Real) -> Self {
113        let b = Vector::X * half_height;
114        Self::new(-b, b, radius)
115    }
116
117    /// Creates a new capsule aligned with the Y axis.
118    ///
119    /// The capsule is centered at the origin and extends along the Y axis.
120    /// This is the most common orientation for character capsules (standing upright).
121    ///
122    /// # Arguments
123    ///
124    /// * `half_height` - Half the length of the central segment
125    /// * `radius` - The radius of the capsule
126    ///
127    /// # Example
128    ///
129    /// ```
130    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
131    /// use parry3d::shape::Capsule;
132    ///
133    /// // Create a typical character capsule: 2 units tall with 0.3 radius
134    /// let character = Capsule::new_y(1.0, 0.3);
135    /// assert_eq!(character.height(), 2.0);
136    /// assert_eq!(character.radius, 0.3);
137    ///
138    /// // Total height including the spherical caps: 2.0 + 2 * 0.3 = 2.6
139    /// # }
140    /// ```
141    pub fn new_y(half_height: Real, radius: Real) -> Self {
142        let b = Vector::Y * half_height;
143        Self::new(-b, b, radius)
144    }
145
146    /// Creates a new capsule aligned with the Z axis.
147    ///
148    /// The capsule is centered at the origin and extends along the Z axis.
149    ///
150    /// # Arguments
151    ///
152    /// * `half_height` - Half the length of the central segment
153    /// * `radius` - The radius of the capsule
154    ///
155    /// # Example
156    ///
157    /// ```
158    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
159    /// use parry3d::shape::Capsule;
160    ///
161    /// // Create a capsule for a torpedo extending along Z axis
162    /// let torpedo = Capsule::new_z(5.0, 0.4);
163    /// assert_eq!(torpedo.height(), 10.0);
164    /// assert_eq!(torpedo.radius, 0.4);
165    /// # }
166    /// ```
167    #[cfg(feature = "dim3")]
168    pub fn new_z(half_height: Real, radius: Real) -> Self {
169        let b = Vector::Z * half_height;
170        Self::new(-b, b, radius)
171    }
172
173    /// Creates a new capsule with custom endpoints and radius.
174    ///
175    /// This is the most flexible constructor, allowing you to create a capsule
176    /// with any orientation and position.
177    ///
178    /// # Arguments
179    ///
180    /// * `a` - The first endpoint of the central segment
181    /// * `b` - The second endpoint of the central segment
182    /// * `radius` - The radius of the capsule
183    ///
184    /// # Example
185    ///
186    /// ```
187    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
188    /// use parry3d::shape::Capsule;
189    /// use parry3d::math::Vector;
190    ///
191    /// // Create a diagonal capsule
192    /// let a = Vector::ZERO;
193    /// let b = Vector::new(3.0, 4.0, 0.0);
194    /// let capsule = Capsule::new(a, b, 0.5);
195    ///
196    /// // Height is the distance between a and b
197    /// assert_eq!(capsule.height(), 5.0); // 3-4-5 triangle
198    ///
199    /// // Center is the midpoint
200    /// let center = capsule.center();
201    /// assert_eq!(center, Vector::new(1.5, 2.0, 0.0));
202    /// # }
203    /// ```
204    pub fn new(a: Vector, b: Vector, radius: Real) -> Self {
205        let segment = Segment::new(a, b);
206        Self { segment, radius }
207    }
208
209    /// Returns the length of the capsule's central segment.
210    ///
211    /// This is the distance between the two endpoints, **not** including the
212    /// hemispherical caps. The total length of the capsule including caps is
213    /// `height() + 2 * radius`.
214    ///
215    /// # Example
216    ///
217    /// ```
218    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
219    /// use parry3d::shape::Capsule;
220    ///
221    /// let capsule = Capsule::new_y(3.0, 0.5);
222    ///
223    /// // Height of the central segment
224    /// assert_eq!(capsule.height(), 6.0);
225    ///
226    /// // Total length including spherical caps
227    /// let total_length = capsule.height() + 2.0 * capsule.radius;
228    /// assert_eq!(total_length, 7.0);
229    /// # }
230    /// ```
231    pub fn height(&self) -> Real {
232        (self.segment.b - self.segment.a).length()
233    }
234
235    /// Returns half the length of the capsule's central segment.
236    ///
237    /// This is equivalent to `height() / 2.0`.
238    ///
239    /// # Example
240    ///
241    /// ```
242    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
243    /// use parry3d::shape::Capsule;
244    ///
245    /// let capsule = Capsule::new_y(3.0, 0.5);
246    /// assert_eq!(capsule.half_height(), 3.0);
247    /// assert_eq!(capsule.half_height(), capsule.height() / 2.0);
248    /// # }
249    /// ```
250    pub fn half_height(&self) -> Real {
251        self.height() / 2.0
252    }
253
254    /// Returns the center point of the capsule.
255    ///
256    /// This is the midpoint between the two endpoints of the central segment.
257    ///
258    /// # Example
259    ///
260    /// ```
261    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
262    /// use parry3d::shape::Capsule;
263    /// use parry3d::math::Vector;
264    ///
265    /// let a = Vector::new(-2.0, 0.0, 0.0);
266    /// let b = Vector::new(4.0, 0.0, 0.0);
267    /// let capsule = Capsule::new(a, b, 1.0);
268    ///
269    /// let center = capsule.center();
270    /// assert_eq!(center, Vector::new(1.0, 0.0, 0.0));
271    /// # }
272    /// ```
273    pub fn center(&self) -> Vector {
274        self.segment.a.midpoint(self.segment.b)
275    }
276
277    /// Creates a new capsule equal to `self` with all its endpoints transformed by `pos`.
278    ///
279    /// This applies a rigid transformation (translation and rotation) to the capsule.
280    ///
281    /// # Arguments
282    ///
283    /// * `pos` - The isometry (rigid transformation) to apply
284    ///
285    /// # Example
286    ///
287    /// ```
288    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
289    /// use parry3d::shape::Capsule;
290    /// use parry3d::math::{Pose, Vector};
291    ///
292    /// let capsule = Capsule::new_y(1.0, 0.5);
293    ///
294    /// // Translate the capsule 5 units along X axis
295    /// let transform = Pose::translation(5.0, 0.0, 0.0);
296    /// let transformed = capsule.transform_by(&transform);
297    ///
298    /// // Center moved by 5 units
299    /// assert_eq!(transformed.center().x, 5.0);
300    /// // Radius unchanged
301    /// assert_eq!(transformed.radius, 0.5);
302    /// # }
303    /// ```
304    pub fn transform_by(&self, pos: &Pose) -> Self {
305        Self::new(pos * self.segment.a, pos * self.segment.b, self.radius)
306    }
307
308    /// The transformation such that `t * Y` is collinear with `b - a` and `t * origin` equals
309    /// the capsule's center.
310    pub fn canonical_transform(&self) -> Pose {
311        let tra = self.center();
312        let rot = self.rotation_wrt_y();
313        Pose::from_parts(tra, rot)
314    }
315
316    /// The rotation `r` such that `r * Y` is collinear with `b - a`.
317    pub fn rotation_wrt_y(&self) -> Rotation {
318        let mut dir = self.segment.b - self.segment.a;
319        if dir.y < 0.0 {
320            dir = -dir;
321        }
322        let dir = dir.normalize_or(Vector::Y);
323        Rotation::from_rotation_arc(Vector::Y, dir)
324    }
325
326    /// The transform `t` such that `t * Y` is collinear with `b - a` and such that `t * origin = (b + a) / 2.0`.
327    pub fn transform_wrt_y(&self) -> Pose {
328        let rot = self.rotation_wrt_y();
329        Pose::from_parts(self.center(), rot)
330    }
331
332    /// Computes a scaled version of this capsule.
333    ///
334    /// If the scaling factor is non-uniform, then it can’t be represented as
335    /// capsule. Instead, a convex polygon approximation (with `nsubdivs`
336    /// subdivisions) is returned. Returns `None` if that approximation had degenerate
337    /// normals (for example if the scaling factor along one axis is zero).
338    #[cfg(all(feature = "dim2", feature = "alloc"))]
339    pub fn scaled(
340        self,
341        scale: Vector,
342        nsubdivs: u32,
343    ) -> Option<Either<Self, super::ConvexPolygon>> {
344        if scale.x != scale.y {
345            // The scaled shape is not a capsule.
346            let mut vtx = self.to_polyline(nsubdivs);
347            vtx.iter_mut().for_each(|pt| *pt *= scale);
348            Some(Either::Right(super::ConvexPolygon::from_convex_polyline(
349                vtx,
350            )?))
351        } else {
352            let uniform_scale = scale.x;
353            Some(Either::Left(Self::new(
354                self.segment.a * uniform_scale,
355                self.segment.b * uniform_scale,
356                self.radius * uniform_scale.abs(),
357            )))
358        }
359    }
360
361    /// Computes a scaled version of this capsule.
362    ///
363    /// If the scaling factor is non-uniform, then it can’t be represented as
364    /// capsule. Instead, a convex polygon approximation (with `nsubdivs`
365    /// subdivisions) is returned. Returns `None` if that approximation had degenerate
366    /// normals (for example if the scaling factor along one axis is zero).
367    #[cfg(all(feature = "dim3", feature = "alloc"))]
368    pub fn scaled(
369        self,
370        scale: Vector,
371        nsubdivs: u32,
372    ) -> Option<Either<Self, super::ConvexPolyhedron>> {
373        if scale.x != scale.y || scale.x != scale.z || scale.y != scale.z {
374            // The scaled shape is not a capsule.
375            let (mut vtx, idx) = self.to_trimesh(nsubdivs, nsubdivs);
376            vtx.iter_mut().for_each(|pt| *pt *= scale);
377            Some(Either::Right(super::ConvexPolyhedron::from_convex_mesh(
378                vtx, &idx,
379            )?))
380        } else {
381            let uniform_scale = scale.x;
382            Some(Either::Left(Self::new(
383                self.segment.a * uniform_scale,
384                self.segment.b * uniform_scale,
385                self.radius * uniform_scale.abs(),
386            )))
387        }
388    }
389}
390
391impl SupportMap for Capsule {
392    fn local_support_point(&self, dir: Vector) -> Vector {
393        let dir = dir.normalize_or(Vector::Y);
394        self.local_support_point_toward(dir)
395    }
396
397    fn local_support_point_toward(&self, dir: Vector) -> Vector {
398        if dir.dot(self.segment.a) > dir.dot(self.segment.b) {
399            self.segment.a + dir * self.radius
400        } else {
401            self.segment.b + dir * self.radius
402        }
403    }
404}