parry3d/mass_properties/
mass_properties_cylinder.rs

1use crate::mass_properties::MassProperties;
2use crate::math::{PrincipalAngularInertia, Real, Vector};
3#[cfg(feature = "dim3")]
4use {
5    crate::math::{Point, Rotation},
6    na::RealField,
7};
8
9impl MassProperties {
10    pub(crate) fn cylinder_y_volume_unit_inertia(
11        half_height: Real,
12        radius: Real,
13    ) -> (Real, PrincipalAngularInertia<Real>) {
14        #[cfg(feature = "dim2")]
15        {
16            Self::cuboid_volume_unit_inertia(Vector::new(radius, half_height))
17        }
18
19        #[cfg(feature = "dim3")]
20        {
21            let volume = half_height * radius * radius * Real::pi() * 2.0;
22            let sq_radius = radius * radius;
23            let sq_height = half_height * half_height * 4.0;
24            let off_principal = (sq_radius * 3.0 + sq_height) / 12.0;
25
26            let inertia = Vector::new(off_principal, sq_radius / 2.0, off_principal);
27            (volume, inertia)
28        }
29    }
30
31    /// Computes the mass properties of a cylinder (3D only).
32    ///
33    /// A cylinder is a 3D shape with circular cross-section and flat ends, aligned along
34    /// the Y-axis and centered at the origin. Unlike a capsule, a cylinder has sharp edges
35    /// at the top and bottom.
36    ///
37    /// **Note**: In 2D, this function is used internally but cylinders don't exist as a
38    /// distinct 2D shape (rectangles are used instead).
39    ///
40    /// # Arguments
41    ///
42    /// * `density` - The material density in kg/m³ (e.g., aluminum = 2700, plastic = 950)
43    /// * `half_height` - Half the total height of the cylinder (center to top/bottom)
44    /// * `radius` - The radius of the circular cross-section
45    ///
46    /// # Returns
47    ///
48    /// A `MassProperties` struct containing:
49    /// - **mass**: Total mass calculated from volume and density
50    /// - **local_com**: Center of mass at the origin (cylinders are symmetric)
51    /// - **inv_principal_inertia**: Inverse angular inertia (different for each axis)
52    /// - **principal_inertia_local_frame**: Identity rotation (aligned with Y-axis)
53    ///
54    /// # Physics Background
55    ///
56    /// Cylinders have rotational symmetry around the Y-axis:
57    /// - Volume = π × radius² × height
58    /// - Center of mass is at the geometric center (origin)
59    /// - Angular inertia around Y-axis (spinning like a top): I_y = (1/2) × mass × radius²
60    /// - Angular inertia around X/Z axes (tipping over): I_x = I_z = (1/12) × mass × (3×radius² + height²)
61    /// - Easier to spin around the central axis than to tip over
62    ///
63    /// # Example - Aluminum Can
64    ///
65    /// ```
66    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
67    /// use parry3d::mass_properties::MassProperties;
68    /// use nalgebra::Point3;
69    ///
70    /// // Standard soda can: 12.3cm tall, 6.6cm diameter
71    /// // Aluminum density: ~2700 kg/m³
72    /// let half_height = 0.0615; // 6.15 cm in meters
73    /// let radius = 0.033;        // 3.3 cm in meters
74    /// let density = 2700.0;
75    ///
76    /// let can_props = MassProperties::from_cylinder(density, half_height, radius);
77    ///
78    /// let mass = can_props.mass();
79    /// println!("Can mass: {:.2} kg", mass); // Approximately 0.15 kg (150 grams)
80    ///
81    /// // Center of mass at origin
82    /// assert_eq!(can_props.local_com, Point3::origin());
83    ///
84    /// // Check inertia differences
85    /// let inertia = can_props.principal_inertia();
86    /// println!("Spin inertia (Y): {:.6}", inertia.y); // Low (easy to spin)
87    /// println!("Tip inertia (X): {:.6}", inertia.x);  // Higher (harder to tip)
88    /// assert!(inertia.y < inertia.x); // Easier to spin than tip
89    /// # }
90    /// ```
91    ///
92    /// # Example - Concrete Column
93    ///
94    /// ```
95    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
96    /// use parry3d::mass_properties::MassProperties;
97    ///
98    /// // Concrete support column: 3m tall, 0.5m diameter
99    /// // Concrete density: ~2400 kg/m³
100    /// let half_height = 1.5;  // 3m / 2
101    /// let radius = 0.25;      // 0.5m / 2
102    /// let density = 2400.0;
103    ///
104    /// let column_props = MassProperties::from_cylinder(density, half_height, radius);
105    ///
106    /// // Volume = π × (0.25)² × 3 = 0.589 m³
107    /// // Mass = 0.589 × 2400 = 1414 kg
108    /// let mass = column_props.mass();
109    /// assert!((mass - 1414.0).abs() < 10.0);
110    ///
111    /// println!("Column mass: {:.0} kg", mass);
112    /// # }
113    /// ```
114    ///
115    /// # Example - Comparing Cylinder vs Capsule
116    ///
117    /// ```
118    /// # #[cfg(all(feature = "dim3", feature = "f32"))] {
119    /// use parry3d::mass_properties::MassProperties;
120    /// use nalgebra::Point3;
121    ///
122    /// let half_height = 1.0;
123    /// let radius = 0.5;
124    /// let density = 1000.0;
125    ///
126    /// // Cylinder has flat ends (sharp edges)
127    /// let cylinder = MassProperties::from_cylinder(density, half_height, radius);
128    ///
129    /// // Capsule has rounded ends (smooth)
130    /// let a = Point3::new(0.0, -half_height, 0.0);
131    /// let b = Point3::new(0.0, half_height, 0.0);
132    /// let capsule = MassProperties::from_capsule(density, a, b, radius);
133    ///
134    /// // Capsule has more mass due to hemispherical caps
135    /// println!("Cylinder mass: {:.2} kg", cylinder.mass());
136    /// println!("Capsule mass: {:.2} kg", capsule.mass());
137    /// assert!(capsule.mass() > cylinder.mass());
138    /// # }
139    /// ```
140    ///
141    /// # Use Cases
142    ///
143    /// - **Structural elements**: Columns, pillars, posts
144    /// - **Containers**: Cans, drums, barrels, tanks
145    /// - **Mechanical parts**: Shafts, pistons, rollers
146    /// - **Tree trunks**: Natural cylindrical objects
147    /// - **Wheels**: When viewed from the side (use with proper orientation)
148    ///
149    /// # Cylinder vs Capsule
150    ///
151    /// **Use Cylinder when**:
152    /// - Sharp edges are acceptable or desired
153    /// - Object is truly flat-ended (cans, pipes)
154    /// - Static/kinematic objects (don't need smooth rolling)
155    ///
156    /// **Use Capsule when**:
157    /// - Smooth collision response is needed
158    /// - Object needs to roll or slide smoothly
159    /// - Character controllers or dynamic objects
160    ///
161    /// # Common Mistakes
162    ///
163    /// - **Wrong axis**: Cylinders are aligned with Y-axis by default. Use
164    ///   `transform_by()` or create with proper orientation if you need X or Z alignment.
165    /// - **Half height confusion**: Total height is `2 × half_height`, not just `half_height`
166    ///
167    /// # Performance Note
168    ///
169    /// Cylinder collision detection is more expensive than capsules due to sharp edges,
170    /// but still reasonably efficient. For dynamic objects, prefer capsules.
171    #[cfg(feature = "dim3")]
172    pub fn from_cylinder(density: Real, half_height: Real, radius: Real) -> Self {
173        let (cyl_vol, cyl_unit_i) = Self::cylinder_y_volume_unit_inertia(half_height, radius);
174        let cyl_mass = cyl_vol * density;
175
176        Self::with_principal_inertia_frame(
177            Point::origin(),
178            cyl_mass,
179            cyl_unit_i * cyl_mass,
180            Rotation::identity(),
181        )
182    }
183}