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