parry3d/mass_properties/
mass_properties.rs

1use crate::math::{AngVector, AngularInertia, Isometry, Point, Real, Rotation, Vector};
2use crate::utils;
3use core::ops::{Add, AddAssign, Sub, SubAssign};
4use na::ComplexField;
5use num::Zero;
6#[cfg(feature = "dim3")]
7use {core::ops::MulAssign, na::Matrix3};
8
9#[cfg(feature = "rkyv")]
10use rkyv::{bytecheck, CheckBytes};
11
12#[cfg_attr(feature = "f32", expect(clippy::unnecessary_cast))]
13const EPSILON: Real = f32::EPSILON as Real;
14
15#[derive(Copy, Clone, Debug, Default, PartialEq)]
16#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
17#[cfg_attr(
18    feature = "rkyv",
19    derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, CheckBytes),
20    archive(as = "Self")
21)]
22/// The local mass properties of a rigid-body.
23pub struct MassProperties {
24    /// The center of mass of a rigid-body expressed in its local-space.
25    pub local_com: Point<Real>,
26    /// The inverse of the mass of a rigid-body.
27    ///
28    /// If this is zero, the rigid-body is assumed to have infinite mass.
29    pub inv_mass: Real,
30    /// The inverse of the principal angular inertia of the rigid-body.
31    ///
32    /// The angular inertia is calculated relative to [`Self::local_com`].
33    /// Components set to zero are assumed to be infinite along the corresponding principal axis.
34    pub inv_principal_inertia_sqrt: AngVector<Real>,
35    #[cfg(feature = "dim3")]
36    /// The principal vectors of the local angular inertia tensor of the rigid-body.
37    pub principal_inertia_local_frame: Rotation<Real>,
38}
39
40impl MassProperties {
41    /// Initializes the mass properties with the given center-of-mass, mass, and angular inertia.
42    ///
43    /// The center-of-mass is specified in the local-space of the rigid-body.
44    #[cfg(feature = "dim2")]
45    pub fn new(local_com: Point<Real>, mass: Real, principal_inertia: Real) -> Self {
46        let inv_mass = utils::inv(mass);
47        let inv_principal_inertia_sqrt = utils::inv(ComplexField::sqrt(principal_inertia));
48        Self {
49            local_com,
50            inv_mass,
51            inv_principal_inertia_sqrt,
52        }
53    }
54
55    /// Initializes the mass properties from the given center-of-mass, mass, and principal angular inertia.
56    ///
57    /// The center-of-mass is specified in the local-space of the rigid-body.
58    /// The principal angular inertia are the angular inertia along the coordinate axes in the local-space
59    /// of the rigid-body.
60    #[cfg(feature = "dim3")]
61    pub fn new(local_com: Point<Real>, mass: Real, principal_inertia: AngVector<Real>) -> Self {
62        Self::with_principal_inertia_frame(local_com, mass, principal_inertia, Rotation::identity())
63    }
64
65    /// Initializes the mass properties from the given center-of-mass, mass, and principal angular inertia.
66    ///
67    /// The center-of-mass is specified in the local-space of the rigid-body.
68    /// The principal angular inertia are the angular inertia along the coordinate axes defined by
69    /// the `principal_inertia_local_frame` expressed in the local-space of the rigid-body.
70    #[cfg(feature = "dim3")]
71    pub fn with_principal_inertia_frame(
72        local_com: Point<Real>,
73        mass: Real,
74        principal_inertia: AngVector<Real>,
75        principal_inertia_local_frame: Rotation<Real>,
76    ) -> Self {
77        let inv_mass = utils::inv(mass);
78        let inv_principal_inertia_sqrt =
79            principal_inertia.map(|e| utils::inv(ComplexField::sqrt(e)));
80        Self {
81            local_com,
82            inv_mass,
83            inv_principal_inertia_sqrt,
84            principal_inertia_local_frame,
85        }
86    }
87
88    /// Initialize a new `MassProperties` from a given center-of-mass, mass, and angular inertia matrix.
89    ///
90    /// The angular inertia matrix will be diagonalized in order to extract the principal inertia
91    /// values and principal inertia frame.
92    #[cfg(feature = "dim3")]
93    pub fn with_inertia_matrix(local_com: Point<Real>, mass: Real, inertia: Matrix3<Real>) -> Self {
94        let mut eigen = inertia.symmetric_eigen();
95
96        if eigen.eigenvectors.determinant() < 0.0 {
97            eigen.eigenvectors.swap_columns(1, 2);
98            eigen.eigenvalues.swap_rows(1, 2);
99        }
100        let mut principal_inertia_local_frame = Rotation::from_rotation_matrix(
101            &na::Rotation3::from_matrix_unchecked(eigen.eigenvectors),
102        );
103        let _ = principal_inertia_local_frame.renormalize();
104
105        // Drop negative eigenvalues.
106        let principal_inertia = eigen.eigenvalues.map(|e| e.max(0.0));
107
108        Self::with_principal_inertia_frame(
109            local_com,
110            mass,
111            principal_inertia,
112            principal_inertia_local_frame,
113        )
114    }
115
116    /// The mass.
117    pub fn mass(&self) -> Real {
118        utils::inv(self.inv_mass)
119    }
120
121    /// The angular inertia along the principal inertia axes and center of mass of the rigid-body.
122    pub fn principal_inertia(&self) -> AngVector<Real> {
123        #[cfg(feature = "dim2")]
124        return utils::inv(self.inv_principal_inertia_sqrt * self.inv_principal_inertia_sqrt);
125        #[cfg(feature = "dim3")]
126        return self.inv_principal_inertia_sqrt.map(|e| utils::inv(e * e));
127    }
128
129    /// The world-space center of mass of the rigid-body.
130    pub fn world_com(&self, pos: &Isometry<Real>) -> Point<Real> {
131        pos * self.local_com
132    }
133
134    #[cfg(feature = "dim2")]
135    /// The world-space inverse angular inertia tensor of the rigid-body.
136    pub fn world_inv_inertia_sqrt(&self, _rot: &Rotation<Real>) -> AngularInertia<Real> {
137        self.inv_principal_inertia_sqrt
138    }
139
140    #[cfg(feature = "dim3")]
141    /// The world-space inverse angular inertia tensor of the rigid-body.
142    pub fn world_inv_inertia_sqrt(&self, rot: &Rotation<Real>) -> AngularInertia<Real> {
143        if !self.inv_principal_inertia_sqrt.is_zero() {
144            let mut lhs = (rot * self.principal_inertia_local_frame)
145                .to_rotation_matrix()
146                .into_inner();
147            let rhs = lhs.transpose();
148            lhs.column_mut(0)
149                .mul_assign(self.inv_principal_inertia_sqrt.x);
150            lhs.column_mut(1)
151                .mul_assign(self.inv_principal_inertia_sqrt.y);
152            lhs.column_mut(2)
153                .mul_assign(self.inv_principal_inertia_sqrt.z);
154            let inertia = lhs * rhs;
155            AngularInertia::from_sdp_matrix(inertia)
156        } else {
157            AngularInertia::zero()
158        }
159    }
160
161    #[cfg(feature = "dim3")]
162    /// Reconstructs the inverse angular inertia tensor of the rigid body from its principal inertia values and axes.
163    pub fn reconstruct_inverse_inertia_matrix(&self) -> Matrix3<Real> {
164        let inv_principal_inertia = self.inv_principal_inertia_sqrt.map(|e| e * e);
165        self.principal_inertia_local_frame.to_rotation_matrix()
166            * Matrix3::from_diagonal(&inv_principal_inertia)
167            * self
168                .principal_inertia_local_frame
169                .inverse()
170                .to_rotation_matrix()
171    }
172
173    #[cfg(feature = "dim3")]
174    /// Reconstructs the angular inertia tensor of the rigid body from its principal inertia values and axes.
175    pub fn reconstruct_inertia_matrix(&self) -> Matrix3<Real> {
176        let principal_inertia = self.inv_principal_inertia_sqrt.map(|e| utils::inv(e * e));
177        self.principal_inertia_local_frame.to_rotation_matrix()
178            * Matrix3::from_diagonal(&principal_inertia)
179            * self
180                .principal_inertia_local_frame
181                .inverse()
182                .to_rotation_matrix()
183    }
184
185    #[cfg(feature = "dim2")]
186    pub(crate) fn construct_shifted_inertia_matrix(&self, shift: Vector<Real>) -> Real {
187        let i = utils::inv(self.inv_principal_inertia_sqrt * self.inv_principal_inertia_sqrt);
188
189        if self.inv_mass != 0.0 {
190            let mass = 1.0 / self.inv_mass;
191            i + shift.norm_squared() * mass
192        } else {
193            i
194        }
195    }
196
197    #[cfg(feature = "dim3")]
198    pub(crate) fn construct_shifted_inertia_matrix(&self, shift: Vector<Real>) -> Matrix3<Real> {
199        let matrix = self.reconstruct_inertia_matrix();
200
201        if self.inv_mass != 0.0 {
202            let mass = 1.0 / self.inv_mass;
203            let diag = shift.norm_squared();
204            let diagm = Matrix3::from_diagonal_element(diag);
205            matrix + (diagm - shift * shift.transpose()) * mass
206        } else {
207            matrix
208        }
209    }
210
211    /// Transform each element of the mass properties.
212    pub fn transform_by(&self, m: &Isometry<Real>) -> Self {
213        // NOTE: we don't apply the parallel axis theorem here
214        // because the center of mass is also transformed.
215        Self {
216            local_com: m * self.local_com,
217            inv_mass: self.inv_mass,
218            inv_principal_inertia_sqrt: self.inv_principal_inertia_sqrt,
219            #[cfg(feature = "dim3")]
220            principal_inertia_local_frame: m.rotation * self.principal_inertia_local_frame,
221        }
222    }
223
224    /// Changes the mass on these mass-properties.
225    ///
226    /// The `adjust_angular_inertia` argument should always be `true`, unless
227    /// there are some specific reasons not to do so. Setting this to `true`
228    /// will automatically adjust the angular inertia of `self` to account
229    /// for the mass change (i.e. it will multiply the angular inertia by
230    /// `new_mass / prev_mass`). Setting it to `false` will not change the
231    /// current angular inertia.
232    pub fn set_mass(&mut self, new_mass: Real, adjust_angular_inertia: bool) {
233        let new_inv_mass = utils::inv(new_mass);
234
235        if adjust_angular_inertia {
236            let curr_mass = utils::inv(self.inv_mass);
237            self.inv_principal_inertia_sqrt *= new_inv_mass.sqrt() * curr_mass.sqrt();
238        }
239
240        self.inv_mass = new_inv_mass;
241    }
242}
243
244impl Zero for MassProperties {
245    fn zero() -> Self {
246        Self {
247            inv_mass: 0.0,
248            inv_principal_inertia_sqrt: na::zero(),
249            #[cfg(feature = "dim3")]
250            principal_inertia_local_frame: Rotation::identity(),
251            local_com: Point::origin(),
252        }
253    }
254
255    fn is_zero(&self) -> bool {
256        *self == Self::zero()
257    }
258}
259
260impl Sub<MassProperties> for MassProperties {
261    type Output = Self;
262
263    #[cfg(feature = "dim2")]
264    fn sub(self, other: MassProperties) -> Self {
265        if self.is_zero() || other.is_zero() {
266            return self;
267        }
268
269        let m1 = utils::inv(self.inv_mass);
270        let m2 = utils::inv(other.inv_mass);
271
272        let mut new_mass = m1 - m2;
273
274        if new_mass < EPSILON {
275            // Account for small numerical errors.
276            new_mass = 0.0;
277        }
278
279        let inv_mass = utils::inv(new_mass);
280
281        let local_com = (self.local_com * m1 - other.local_com.coords * m2) * inv_mass;
282        let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
283        let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
284        let mut inertia = i1 - i2;
285
286        if inertia < EPSILON {
287            // Account for small numerical errors.
288            inertia = 0.0;
289        }
290
291        // NOTE: we drop the negative eigenvalues that may result from subtraction rounding errors.
292        let inv_principal_inertia_sqrt = utils::inv(ComplexField::sqrt(inertia));
293
294        Self {
295            local_com,
296            inv_mass,
297            inv_principal_inertia_sqrt,
298        }
299    }
300
301    #[cfg(feature = "dim3")]
302    fn sub(self, other: MassProperties) -> Self {
303        if self.is_zero() || other.is_zero() {
304            return self;
305        }
306
307        let m1 = utils::inv(self.inv_mass);
308        let m2 = utils::inv(other.inv_mass);
309        let mut new_mass = m1 - m2;
310
311        if new_mass < EPSILON {
312            new_mass = 0.0;
313        }
314
315        let inv_mass = utils::inv(new_mass);
316        let local_com = (self.local_com * m1 - other.local_com.coords * m2) * inv_mass;
317        let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
318        let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
319        let inertia = i1 - i2;
320        Self::with_inertia_matrix(local_com, new_mass, inertia)
321    }
322}
323
324impl SubAssign<MassProperties> for MassProperties {
325    fn sub_assign(&mut self, rhs: MassProperties) {
326        *self = *self - rhs
327    }
328}
329
330impl Add<MassProperties> for MassProperties {
331    type Output = Self;
332
333    #[cfg(feature = "dim2")]
334    fn add(self, other: MassProperties) -> Self {
335        if self.is_zero() {
336            return other;
337        } else if other.is_zero() {
338            return self;
339        }
340
341        let m1 = utils::inv(self.inv_mass);
342        let m2 = utils::inv(other.inv_mass);
343        let inv_mass = utils::inv(m1 + m2);
344        let local_com = (self.local_com * m1 + other.local_com.coords * m2) * inv_mass;
345        let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
346        let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
347        let inertia = i1 + i2;
348        let inv_principal_inertia_sqrt = utils::inv(ComplexField::sqrt(inertia));
349
350        Self {
351            local_com,
352            inv_mass,
353            inv_principal_inertia_sqrt,
354        }
355    }
356
357    #[cfg(feature = "dim3")]
358    fn add(self, other: MassProperties) -> Self {
359        if self.is_zero() {
360            return other;
361        } else if other.is_zero() {
362            return self;
363        }
364
365        let m1 = utils::inv(self.inv_mass);
366        let m2 = utils::inv(other.inv_mass);
367        let inv_mass = utils::inv(m1 + m2);
368        let local_com = (self.local_com * m1 + other.local_com.coords * m2) * inv_mass;
369        let i1 = self.construct_shifted_inertia_matrix(local_com - self.local_com);
370        let i2 = other.construct_shifted_inertia_matrix(local_com - other.local_com);
371        let inertia = i1 + i2;
372
373        Self::with_inertia_matrix(local_com, m1 + m2, inertia)
374    }
375}
376
377impl AddAssign<MassProperties> for MassProperties {
378    fn add_assign(&mut self, rhs: MassProperties) {
379        *self = *self + rhs
380    }
381}
382
383#[cfg(feature = "alloc")]
384impl core::iter::Sum<MassProperties> for MassProperties {
385    #[cfg(feature = "dim2")]
386    fn sum<I>(iter: I) -> Self
387    where
388        I: Iterator<Item = Self>,
389    {
390        use alloc::vec::Vec;
391
392        let mut total_mass = 0.0;
393        let mut total_com = Point::origin();
394        let mut total_inertia = 0.0;
395        // TODO: avoid this allocation.
396        // This is needed because we iterate twice.
397        let mut all_props = Vec::new();
398
399        for props in iter {
400            let mass = utils::inv(props.inv_mass);
401            total_mass += mass;
402            total_com += props.local_com.coords * mass;
403            all_props.push(props);
404        }
405
406        if total_mass > 0.0 {
407            total_com /= total_mass;
408        }
409
410        for props in all_props {
411            total_inertia += props.construct_shifted_inertia_matrix(total_com - props.local_com);
412        }
413
414        Self {
415            local_com: total_com,
416            inv_mass: utils::inv(total_mass),
417            inv_principal_inertia_sqrt: utils::inv(ComplexField::sqrt(total_inertia)),
418        }
419    }
420
421    #[cfg(feature = "dim3")]
422    fn sum<I>(iter: I) -> Self
423    where
424        I: Iterator<Item = Self>,
425    {
426        use alloc::vec::Vec;
427
428        let mut total_mass = 0.0;
429        let mut total_com = Point::origin();
430        let mut total_inertia = Matrix3::zeros();
431        // TODO: avoid this allocation.
432        // This is needed because we iterate twice.
433        let mut all_props = Vec::new();
434
435        for props in iter {
436            let mass = utils::inv(props.inv_mass);
437            total_mass += mass;
438            total_com += props.local_com.coords * mass;
439            all_props.push(props);
440        }
441
442        if total_mass > 0.0 {
443            total_com /= total_mass;
444        }
445
446        for props in all_props {
447            total_inertia += props.construct_shifted_inertia_matrix(total_com - props.local_com);
448        }
449
450        Self::with_inertia_matrix(total_com, total_mass, total_inertia)
451    }
452}
453
454impl approx::AbsDiffEq for MassProperties {
455    type Epsilon = Real;
456    fn default_epsilon() -> Self::Epsilon {
457        Real::default_epsilon()
458    }
459
460    fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
461        #[cfg(feature = "dim2")]
462        let inertia_is_ok = self
463            .inv_principal_inertia_sqrt
464            .abs_diff_eq(&other.inv_principal_inertia_sqrt, epsilon);
465
466        #[cfg(feature = "dim3")]
467        let inertia_is_ok = self
468            .reconstruct_inverse_inertia_matrix()
469            .abs_diff_eq(&other.reconstruct_inverse_inertia_matrix(), epsilon);
470
471        inertia_is_ok
472            && self.local_com.abs_diff_eq(&other.local_com, epsilon)
473            && self.inv_mass.abs_diff_eq(&other.inv_mass, epsilon)
474            && self
475                .inv_principal_inertia_sqrt
476                .abs_diff_eq(&other.inv_principal_inertia_sqrt, epsilon)
477    }
478}
479
480impl approx::RelativeEq for MassProperties {
481    fn default_max_relative() -> Self::Epsilon {
482        Real::default_max_relative()
483    }
484
485    fn relative_eq(
486        &self,
487        other: &Self,
488        epsilon: Self::Epsilon,
489        max_relative: Self::Epsilon,
490    ) -> bool {
491        #[cfg(feature = "dim2")]
492        let inertia_is_ok = self.inv_principal_inertia_sqrt.relative_eq(
493            &other.inv_principal_inertia_sqrt,
494            epsilon,
495            max_relative,
496        );
497
498        #[cfg(feature = "dim3")]
499        let inertia_is_ok = self.reconstruct_inverse_inertia_matrix().relative_eq(
500            &other.reconstruct_inverse_inertia_matrix(),
501            epsilon,
502            max_relative,
503        );
504
505        inertia_is_ok
506            && self
507                .local_com
508                .relative_eq(&other.local_com, epsilon, max_relative)
509            && self
510                .inv_mass
511                .relative_eq(&other.inv_mass, epsilon, max_relative)
512    }
513}
514
515#[cfg(test)]
516mod test {
517    use super::MassProperties;
518    use crate::math::{AngVector, Point};
519    #[cfg(feature = "dim3")]
520    use crate::math::{Rotation, Vector};
521    use crate::shape::{Ball, Capsule, Shape};
522    use approx::assert_relative_eq;
523    use num::Zero;
524
525    #[test]
526    fn mass_properties_add_partial_zero() {
527        let m1 = MassProperties {
528            local_com: Point::origin(),
529            inv_mass: 2.0,
530            inv_principal_inertia_sqrt: na::zero(),
531            #[cfg(feature = "dim3")]
532            principal_inertia_local_frame: Rotation::identity(),
533        };
534        let m2 = MassProperties {
535            local_com: Point::origin(),
536            inv_mass: 0.0,
537            #[cfg(feature = "dim2")]
538            inv_principal_inertia_sqrt: 1.0,
539            #[cfg(feature = "dim3")]
540            inv_principal_inertia_sqrt: Vector::new(1.0, 2.0, 3.0),
541            #[cfg(feature = "dim3")]
542            principal_inertia_local_frame: Rotation::identity(),
543        };
544        let result = MassProperties {
545            local_com: Point::origin(),
546            inv_mass: 2.0,
547            #[cfg(feature = "dim2")]
548            inv_principal_inertia_sqrt: 1.0,
549            #[cfg(feature = "dim3")]
550            inv_principal_inertia_sqrt: Vector::new(1.0, 2.0, 3.0),
551            #[cfg(feature = "dim3")]
552            principal_inertia_local_frame: Rotation::identity(),
553        };
554
555        assert_eq!(m1 + m2, result);
556        assert_eq!(m2 + m1, result);
557    }
558
559    #[test]
560    fn mass_properties_add_sub() {
561        // Check that addition and subtraction of mass properties behave as expected.
562        let c1 = Capsule::new_x(1.0, 2.0);
563        let c2 = Capsule::new_y(3.0, 4.0);
564        let c3 = Ball::new(2.0);
565
566        let m1 = c1.mass_properties(1.0);
567        let m2 = c2.mass_properties(1.0);
568        let m3 = c3.mass_properties(1.0);
569        let m1m2m3 = m1 + m2 + m3;
570
571        assert_relative_eq!(m1 + m2, m2 + m1, epsilon = 1.0e-6);
572        assert_relative_eq!(m1m2m3 - m1, m2 + m3, epsilon = 1.0e-6);
573        assert_relative_eq!(m1m2m3 - m2, m1 + m3, epsilon = 1.0e-6);
574        assert_relative_eq!(m1m2m3 - m3, m1 + m2, epsilon = 1.0e-6);
575        assert_relative_eq!(m1m2m3 - (m1 + m2), m3, epsilon = 1.0e-6);
576        assert_relative_eq!(m1m2m3 - (m1 + m3), m2, epsilon = 1.0e-6);
577        assert_relative_eq!(m1m2m3 - (m2 + m3), m1, epsilon = 1.0e-6);
578        assert_relative_eq!(m1m2m3 - m1 - m2, m3, epsilon = 1.0e-6);
579        assert_relative_eq!(m1m2m3 - m1 - m3, m2, epsilon = 1.0e-6);
580        assert_relative_eq!(m1m2m3 - m2 - m3, m1, epsilon = 1.0e-6);
581
582        // NOTE: converting the inverse inertia matrices don’t work well here because
583        //       tiny inertia value originating from the subtraction can result in a non-zero
584        //       (but large) inverse.
585        assert_relative_eq!(
586            (((m1m2m3 - m1) - m2) - m3).principal_inertia(),
587            AngVector::zero(),
588            epsilon = 1.0e-6
589        );
590        assert_relative_eq!((((m1m2m3 - m1) - m2) - m3).mass(), 0.0, epsilon = 1.0e-6);
591    }
592
593    #[test]
594    #[cfg(feature = "alloc")]
595    fn mass_properties_compound() {
596        use na::Isometry;
597
598        use crate::{
599            math::Vector,
600            shape::{Compound, Cuboid, SharedShape},
601        };
602
603        // Compute the mass properties of a compound shape made of three 1x1x1 cuboids.
604        let shape = Cuboid::new(Vector::repeat(0.5));
605        let mp = shape.mass_properties(1.0);
606        let iso2 = Isometry::from_parts(Vector::y().into(), Default::default());
607        let iso3 = Isometry::from_parts((-Vector::y()).into(), Default::default());
608
609        // Test sum shifted result through `MassProperties::add`
610        let sum = [mp, mp.transform_by(&iso2), mp.transform_by(&iso3)]
611            .into_iter()
612            .sum::<MassProperties>();
613
614        // Test compound through `MassProperties::from_compound`
615        let compound_shape = Compound::new(vec![
616            (
617                Isometry::from_parts(Vector::default().into(), Default::default()),
618                SharedShape::new(shape),
619            ),
620            (iso2, SharedShape::new(shape)),
621            (iso3, SharedShape::new(shape)),
622        ]);
623        let mp_compound = compound_shape.mass_properties(1.0);
624
625        // Check that the mass properties of the compound shape match the mass properties
626        // of a single 1x3x1 cuboid.
627        #[cfg(feature = "dim2")]
628        let expected = Cuboid::new(Vector::new(0.5, 1.5)).mass_properties(1.0);
629        #[cfg(feature = "dim3")]
630        let expected = Cuboid::new(Vector::new(0.5, 1.5, 0.5)).mass_properties(1.0);
631
632        // Sum shifted
633        assert_relative_eq!(sum.local_com, expected.local_com, epsilon = 1.0e-6);
634        assert_relative_eq!(sum.inv_mass, expected.inv_mass, epsilon = 1.0e-6);
635        assert_relative_eq!(
636            sum.inv_principal_inertia_sqrt,
637            expected.inv_principal_inertia_sqrt,
638            epsilon = 1.0e-6
639        );
640
641        // Compound
642        assert_relative_eq!(mp_compound.local_com, expected.local_com, epsilon = 1.0e-6);
643        assert_relative_eq!(mp_compound.inv_mass, expected.inv_mass, epsilon = 1.0e-6);
644        assert_relative_eq!(
645            mp_compound.inv_principal_inertia_sqrt,
646            expected.inv_principal_inertia_sqrt,
647            epsilon = 1.0e-6
648        );
649    }
650
651    #[test]
652    #[cfg(feature = "alloc")]
653    fn mass_properties_sum_no_nan() {
654        let mp: MassProperties = [MassProperties::zero()].iter().copied().sum();
655        assert!(!mp.local_com.x.is_nan() && !mp.local_com.y.is_nan());
656        #[cfg(feature = "dim3")]
657        assert!(!mp.local_com.z.is_nan());
658    }
659}