parry3d/shape/
cylinder.rs

1//! Support mapping based Cylinder shape.
2
3use crate::math::{Point, Real, Vector};
4use crate::shape::SupportMap;
5use na;
6use num::Zero;
7
8#[cfg(feature = "alloc")]
9use either::Either;
10
11#[cfg(feature = "rkyv")]
12use rkyv::{bytecheck, CheckBytes};
13
14/// A 3D cylinder shape with axis aligned along the Y axis.
15///
16/// A cylinder is a shape with circular cross-sections perpendicular to its axis.
17/// In Parry, cylinders are always aligned with the Y axis in their local coordinate
18/// system and centered at the origin.
19///
20/// # Structure
21///
22/// - **Axis**: Always aligned with Y axis (up/down)
23/// - **half_height**: Half the length along the Y axis
24/// - **radius**: The radius of the circular cross-section
25/// - **Height**: Total height = `2 * half_height`
26///
27/// # Properties
28///
29/// - **3D only**: Only available with the `dim3` feature
30/// - **Convex**: Yes, cylinders are convex shapes
31/// - **Flat caps**: The top and bottom are flat circles (not rounded)
32/// - **Sharp edges**: The rim where cap meets side is a sharp edge
33///
34/// # vs Capsule
35///
36/// If you need rounded ends instead of flat caps, use [`Capsule`](super::Capsule):
37/// - **Cylinder**: Flat circular caps, sharp edges at rims
38/// - **Capsule**: Hemispherical caps, completely smooth (no edges)
39/// - **Capsule**: Better for characters and rolling objects
40/// - **Cylinder**: Better for columns, cans, pipes
41///
42/// # Use Cases
43///
44/// - Pillars and columns
45/// - Cans and barrels
46/// - Wheels and disks
47/// - Pipes and tubes
48/// - Any object with flat circular ends
49///
50/// # Example
51///
52/// ```rust
53/// # #[cfg(all(feature = "dim3", feature = "f32"))] {
54/// use parry3d::shape::Cylinder;
55///
56/// // Create a cylinder: radius 2.0, total height 10.0
57/// let cylinder = Cylinder::new(5.0, 2.0);
58///
59/// assert_eq!(cylinder.half_height, 5.0);
60/// assert_eq!(cylinder.radius, 2.0);
61///
62/// // Total height is 2 * half_height
63/// let total_height = cylinder.half_height * 2.0;
64/// assert_eq!(total_height, 10.0);
65/// # }
66/// ```
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
69#[cfg_attr(feature = "encase", derive(encase::ShaderType))]
70#[cfg_attr(
71    feature = "rkyv",
72    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, CheckBytes),
73    archive(as = "Self")
74)]
75#[derive(PartialEq, Debug, Copy, Clone)]
76#[repr(C)]
77pub struct Cylinder {
78    /// Half the length of the cylinder along the Y axis.
79    ///
80    /// The cylinder extends from `-half_height` to `+half_height` along Y.
81    /// Total height = `2 * half_height`. Must be positive.
82    pub half_height: Real,
83
84    /// The radius of the circular cross-section.
85    ///
86    /// All points on the cylindrical surface are at this distance from the Y axis.
87    /// Must be positive.
88    pub radius: Real,
89}
90
91impl Cylinder {
92    /// Creates a new cylinder aligned with the Y axis.
93    ///
94    /// # Arguments
95    ///
96    /// * `half_height` - Half the total height along the Y axis
97    /// * `radius` - The radius of the circular cross-section
98    ///
99    /// # Panics
100    ///
101    /// Panics if `half_height` or `radius` is not positive.
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
107    /// use parry3d::shape::Cylinder;
108    ///
109    /// // Create a cylinder with radius 3.0 and height 8.0
110    /// let cylinder = Cylinder::new(4.0, 3.0);
111    ///
112    /// assert_eq!(cylinder.half_height, 4.0);
113    /// assert_eq!(cylinder.radius, 3.0);
114    ///
115    /// // The cylinder:
116    /// // - Extends from y = -4.0 to y = 4.0 (total height 8.0)
117    /// // - Has circular cross-section with radius 3.0 in the XZ plane
118    /// # }
119    /// ```
120    pub fn new(half_height: Real, radius: Real) -> Cylinder {
121        assert!(half_height.is_sign_positive() && radius.is_sign_positive());
122
123        Cylinder {
124            half_height,
125            radius,
126        }
127    }
128
129    /// Computes a scaled version of this cylinder.
130    ///
131    /// Scaling a cylinder can produce different results depending on the scale factors:
132    ///
133    /// - **Uniform scaling** (all axes equal): Produces another cylinder
134    /// - **Y different from X/Z**: Produces another cylinder (if X == Z)
135    /// - **Non-uniform X/Z**: Produces an elliptical cylinder approximated as a convex mesh
136    ///
137    /// # Arguments
138    ///
139    /// * `scale` - Scaling factors for X, Y, Z axes
140    /// * `nsubdivs` - Number of subdivisions for mesh approximation (if needed)
141    ///
142    /// # Returns
143    ///
144    /// * `Some(Either::Left(Cylinder))` - If X and Z scales are equal
145    /// * `Some(Either::Right(ConvexPolyhedron))` - If X and Z scales differ (elliptical)
146    /// * `None` - If mesh approximation failed (e.g., zero scale on an axis)
147    ///
148    /// # Example
149    ///
150    /// ```
151    /// # #[cfg(all(feature = "dim3", feature = "f32", feature = "alloc"))] {
152    /// use parry3d::shape::Cylinder;
153    /// use nalgebra::Vector3;
154    /// use either::Either;
155    ///
156    /// let cylinder = Cylinder::new(2.0, 1.0);
157    ///
158    /// // Uniform scaling: produces a larger cylinder
159    /// let scale1 = Vector3::new(2.0, 2.0, 2.0);
160    /// if let Some(Either::Left(scaled)) = cylinder.scaled(&scale1, 20) {
161    ///     assert_eq!(scaled.radius, 2.0);      // 1.0 * 2.0
162    ///     assert_eq!(scaled.half_height, 4.0); // 2.0 * 2.0
163    /// }
164    ///
165    /// // Different Y scale: still a cylinder
166    /// let scale2 = Vector3::new(1.5, 3.0, 1.5);
167    /// if let Some(Either::Left(scaled)) = cylinder.scaled(&scale2, 20) {
168    ///     assert_eq!(scaled.radius, 1.5);      // 1.0 * 1.5
169    ///     assert_eq!(scaled.half_height, 6.0); // 2.0 * 3.0
170    /// }
171    ///
172    /// // Non-uniform X/Z: produces elliptical cylinder (mesh approximation)
173    /// let scale3 = Vector3::new(2.0, 1.0, 1.0);
174    /// if let Some(Either::Right(polyhedron)) = cylinder.scaled(&scale3, 20) {
175    ///     // Result is a convex mesh approximating an elliptical cylinder
176    ///     assert!(polyhedron.points().len() > 0);
177    /// }
178    /// # }
179    /// ```
180    #[cfg(feature = "alloc")]
181    #[inline]
182    pub fn scaled(
183        self,
184        scale: &Vector<Real>,
185        nsubdivs: u32,
186    ) -> Option<Either<Self, super::ConvexPolyhedron>> {
187        if scale.x != scale.z {
188            // The scaled shape isn’t a cylinder.
189            let (mut vtx, idx) = self.to_trimesh(nsubdivs);
190            vtx.iter_mut()
191                .for_each(|pt| pt.coords = pt.coords.component_mul(scale));
192            Some(Either::Right(super::ConvexPolyhedron::from_convex_mesh(
193                vtx, &idx,
194            )?))
195        } else {
196            Some(Either::Left(Self::new(
197                self.half_height * scale.y,
198                self.radius * scale.x,
199            )))
200        }
201    }
202}
203
204impl SupportMap for Cylinder {
205    fn local_support_point(&self, dir: &Vector<Real>) -> Point<Real> {
206        let mut vres = *dir;
207
208        vres[1] = 0.0;
209
210        if vres.normalize_mut().is_zero() {
211            vres = na::zero()
212        } else {
213            vres *= self.radius;
214        }
215
216        vres[1] = self.half_height.copysign(dir[1]);
217
218        Point::from(vres)
219    }
220}