bevy_symbios_shape 0.6.0

Bevy integration for Symbios Shape.
Documentation
//! Bevy Component surface for upstream `symbios-shape` mass properties.
//!
//! When a terminal's grammar carries a `Mat(id, density)` operation with a
//! non-`None` density, the interpreter computes [`symbios_shape::MassProperties`]
//! (mass, centroid, optional inertia tensor) on that terminal. This module
//! exposes that data as a Bevy [`Component`] — [`TerminalMass`] — so physics,
//! IK, and LOD systems can read it directly from the ECS.

use bevy::math::{Mat3, Vec3};
use bevy::prelude::Component;
use symbios_shape::MassProperties;

/// Volumetric mass properties for a terminal entity.
///
/// Inserted by [`SpawnShapeExt::spawn_shape`] onto every spawned terminal whose
/// upstream `Material` carried a density. Quantities are in world-frame SI
/// units, downcast from the upstream `f64` values:
///
/// - `mass` in kg
/// - `centroid` in metres from the world origin
/// - `inertia` is the moment-of-inertia tensor about the centroid in kg·m²
///
/// `inertia` is `None` for face profiles whose closed-form tensor isn't yet
/// implemented upstream (currently `FaceProfile::Taper`). For high-precision
/// physics, read [`symbios_shape::MassProperties`] directly off the
/// [`symbios_shape::Terminal`] before spawning, rather than relying on this
/// f32 component.
///
/// [`SpawnShapeExt::spawn_shape`]: crate::spawner::SpawnShapeExt::spawn_shape
#[derive(Component, Debug, Clone, Copy)]
pub struct TerminalMass {
    pub mass: f32,
    pub centroid: Vec3,
    pub inertia: Option<Mat3>,
}

impl From<&MassProperties> for TerminalMass {
    fn from(p: &MassProperties) -> Self {
        Self {
            mass: p.mass as f32,
            centroid: Vec3::new(
                p.centroid.x as f32,
                p.centroid.y as f32,
                p.centroid.z as f32,
            ),
            inertia: p.inertia.map(|m| {
                let c = m.to_cols_array();
                Mat3::from_cols_array(&[
                    c[0] as f32,
                    c[1] as f32,
                    c[2] as f32,
                    c[3] as f32,
                    c[4] as f32,
                    c[5] as f32,
                    c[6] as f32,
                    c[7] as f32,
                    c[8] as f32,
                ])
            }),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use glam::{DMat3, DVec3};

    #[test]
    fn from_upstream_box_mass_properties() {
        let upstream = MassProperties {
            mass: 12.5,
            centroid: DVec3::new(1.0, 2.0, 3.0),
            inertia: Some(DMat3::IDENTITY),
        };
        let c = TerminalMass::from(&upstream);
        assert_eq!(c.mass, 12.5);
        assert_eq!(c.centroid, Vec3::new(1.0, 2.0, 3.0));
        assert!(c.inertia.is_some());
        let m = c.inertia.unwrap();
        assert_eq!(m.x_axis, Vec3::new(1.0, 0.0, 0.0));
        assert_eq!(m.y_axis, Vec3::new(0.0, 1.0, 0.0));
        assert_eq!(m.z_axis, Vec3::new(0.0, 0.0, 1.0));
    }

    #[test]
    fn from_upstream_taper_no_inertia() {
        let upstream = MassProperties {
            mass: 5.0,
            centroid: DVec3::ZERO,
            inertia: None,
        };
        let c = TerminalMass::from(&upstream);
        assert!(c.inertia.is_none());
    }
}