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}