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