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