dynamis-model 0.7.0

Host facing physics descriptions, states, and derived mass properties
Documentation
use dynamis_model::{
    BodyDesc, BodyHandle, ColliderDesc, ConstraintDesc, FluidMaterial, MaterialCombine,
    PhysicsConfig, Shape, SoftAttachment, SoftBodyDesc, SoftElement, SoftElementKind,
    SoftElementState, SoftMaterial,
};
use std::panic::{AssertUnwindSafe, catch_unwind};

#[test]
fn shape_constructors_reject_degenerate_geometry() {
    assert!(catch_unwind(|| Shape::sphere(0.0)).is_err());
    assert!(catch_unwind(|| Shape::sphere(-1.0)).is_err());
    assert!(catch_unwind(|| Shape::cuboid([1.0, 0.0, 1.0])).is_err());
    assert!(catch_unwind(|| Shape::cuboid([1.0, 1.0, -1.0])).is_err());
    assert!(catch_unwind(|| Shape::capsule(0.0, 1.0)).is_err());
    assert!(catch_unwind(|| Shape::capsule(1.0, -0.5)).is_err());
    assert!(catch_unwind(|| Shape::cylinder(0.0, 1.0)).is_err());
    assert!(catch_unwind(|| Shape::cylinder(1.0, -0.5)).is_err());
}

#[test]
fn body_descs_validate_inputs() {
    assert!(catch_unwind(|| BodyDesc::sphere(0.5).orientation([1.0, 1.0, 0.0, 0.0])).is_err());
    assert!(catch_unwind(|| BodyDesc::sphere(0.5).mass(-1.0)).is_err());
    assert!(catch_unwind(|| BodyDesc::sphere(0.5).friction(-0.1)).is_err());
    let compound = (0..64).fold(BodyDesc::sphere(0.5), |desc, _| {
        desc.collider(ColliderDesc::new(Shape::sphere(0.1)))
    });
    assert_eq!(compound.colliders.len(), 65);
    let default = BodyDesc::sphere(0.5);
    assert_eq!(default.collision_group, 1);
    assert_eq!(default.collision_mask, u32::MAX);
    assert_eq!(default.mass, 1.0);
}

#[test]
fn collider_descs_validate_inputs() {
    assert!(
        catch_unwind(|| {
            ColliderDesc::new(Shape::sphere(0.5)).rotation([0.0, 1.0, 1.0, 0.0]);
        })
        .is_err()
    );
    assert!(catch_unwind(|| ColliderDesc::new(Shape::sphere(0.5)).friction(-1.0)).is_err());
}

#[test]
fn constraint_descs_validate_inputs() {
    assert!(catch_unwind(|| ConstraintDesc::distance([0.0; 3], [0.0; 3], -1.0)).is_err());
    assert!(catch_unwind(|| ConstraintDesc::revolute([0.0; 3], [0.0; 3], [0.0; 3])).is_err());
    assert!(catch_unwind(|| ConstraintDesc::prismatic([0.0; 3], [0.0; 3], [0.0; 3])).is_err());
    assert!(
        catch_unwind(
            || ConstraintDesc::revolute([0.0; 3], [0.0; 3], [1.0, 0.0, 0.0]).limit(1.0, 0.0)

        )
        .is_err()
    );
    assert!(
        catch_unwind(|| { ConstraintDesc::distance([0.0; 3], [0.0; 3], 1.0).spring(-1.0, 0.5) })
            .is_err()
    );
    assert!(
        catch_unwind(|| { ConstraintDesc::distance([0.0; 3], [0.0; 3], 1.0).spring(1.0, -0.5) })
            .is_err()
    );
}

#[test]
fn plane_shape_and_scaled_colliders_validate() {
    let plane = Shape::plane();
    assert!(plane.is_world_geometry());
    assert!(!plane.is_convex());
    let scaled = ColliderDesc::new(Shape::sphere(0.5)).scale([2.0, 1.0, 1.0]);
    assert_eq!(scaled.scale, [2.0, 1.0, 1.0]);
    let invalid = catch_unwind(AssertUnwindSafe(|| {
        ColliderDesc::new(Shape::sphere(0.5)).scale([0.0, 1.0, 1.0])
    }));
    assert!(invalid.is_err(), "scale must be strictly positive");
}

#[test]
fn combine_modes_and_constraint_apis() {
    let config = PhysicsConfig {
        friction_combine: MaterialCombine::Min,
        restitution_combine: MaterialCombine::Average,
        ..PhysicsConfig::default()
    };
    assert_eq!(config.friction_combine, MaterialCombine::Min);
    assert_eq!(
        PhysicsConfig::default().restitution_combine,
        MaterialCombine::Max
    );
    let motor = ConstraintDesc::revolute([0.0; 3], [0.0; 3], [0.0, 1.0, 0.0])
        .motor(3.0)
        .motor_force(50.0);
    assert_eq!(motor.motor.unwrap().target_velocity, 3.0);
    assert_eq!(motor.motor.unwrap().max_force, 50.0);
    let broken = ConstraintDesc::ball([0.0; 3], [0.0; 3]).break_threshold(100.0, 10.0);
    assert_eq!(broken.break_threshold.unwrap().force, 100.0);
    let swiveling = ConstraintDesc::ball([0.0; 3], [0.0; 3])
        .limit(-0.5, 0.5)
        .swing(0.7, 0.9);
    assert_eq!(swiveling.swing.unwrap().swing_a, 0.7);
    let gear = ConstraintDesc::gear([0.0, 1.0, 0.0], [0.0, 0.0, 1.0], 2.5);
    assert_eq!(gear.gear_ratio, 2.5);
    assert_eq!(gear.axis_b, [0.0, 0.0, 1.0]);
    let pulley = ConstraintDesc::pulley(
        [0.0; 3],
        [1.0, 0.0, 0.0],
        [0.0, 2.0, 0.0],
        [2.0, 2.0, 0.0],
        3.0,
    );
    assert_eq!(pulley.rest_length, 3.0);
    assert_eq!(pulley.pulley_fixed_a, [0.0, 2.0, 0.0]);
    let invalid = catch_unwind(AssertUnwindSafe(|| {
        ConstraintDesc::gear([0.0; 3], [0.0, 1.0, 0.0], 1.0)
    }));
    assert!(invalid.is_err(), "gear axis must be non-zero");
    let invalid = catch_unwind(AssertUnwindSafe(|| {
        ConstraintDesc::pulley([0.0; 3], [0.0; 3], [0.0; 3], [0.0; 3], 0.0)
    }));
    assert!(invalid.is_err(), "pulley length must be positive");
}

#[test]
fn a_fluid_material_bounds_its_spacing_and_support() {
    let material = FluidMaterial::new(0.3, 0.4);
    assert_eq!(material.spacing(), 0.3);
    assert_eq!(material.support(), 0.4);
    assert!(catch_unwind(|| FluidMaterial::new(0.0, 0.4)).is_err());
    assert!(catch_unwind(|| FluidMaterial::new(0.3, 0.3)).is_err());
    let fluid = SoftBodyDesc::fluid(vec![[0.0; 3], [0.3, 0.0, 0.0]], 0.1, material);
    assert!(fluid.elements.is_empty());
    assert_eq!(fluid.radius, 0.1);
    assert_eq!(fluid.fluid.unwrap().support(), 0.4);
    let crowded =
        catch_unwind(|| SoftBodyDesc::fluid(vec![[0.0; 3]], 0.2, FluidMaterial::new(0.3, 0.4)));
    assert!(
        crowded.is_err(),
        "a fluid particle must fit its rest spacing"
    );
}

#[test]
fn a_soft_element_bounds_its_strength() {
    let yielding = SoftElement::distance(0, 1, 1.0)
        .yielding(0.2, 0.5)
        .fracturing(0.4);
    assert_eq!(yielding.yield_strain_of(), 0.2);
    assert_eq!(yielding.break_strain_of(), 0.4);
    assert_eq!(yielding.plastic_flow_of(), 0.5);
    assert!(yielding.carries_strength());
    assert!(!SoftElement::distance(0, 1, 1.0).carries_strength());
    let brittle = SoftElement::area(0, 1, 2, 0.5).fracturing(0.05);
    assert!(brittle.carries_strength());
    assert!(brittle.yield_strain_of().is_infinite());
    assert!(catch_unwind(|| SoftElement::distance(0, 1, 1.0).yielding(-0.1, 0.5)).is_err());
    assert!(catch_unwind(|| SoftElement::distance(0, 1, 1.0).yielding(0.1, 1.5)).is_err());
    assert!(catch_unwind(|| SoftElement::distance(0, 1, 1.0).fracturing(-0.1)).is_err());
}

#[test]
fn a_soft_material_spreads_its_strength_over_its_elements() {
    let material = SoftMaterial::new(0.01, 0.02, 0.03, 0.04)
        .yielding(0.2, 0.25)
        .fracturing(0.4);
    let desc = SoftBodyDesc::cloth([3, 3], 1.0, material);
    assert!(desc.carries_strength());
    assert!(desc.elements.iter().all(|element| {
        element.yield_strain_of() == 0.2
            && element.break_strain_of() == 0.4
            && element.plastic_flow_of() == 0.25
    }));
    assert!(
        desc.elements
            .iter()
            .any(|element| element.kind() == SoftElementKind::Area)
    );
    let elastic = SoftBodyDesc::cloth([3, 3], 1.0, SoftMaterial::rigid());
    assert!(!elastic.carries_strength());
    let overridden = desc.clone().yielding(0.05, 1.0).fracturing(0.1);
    assert!(
        overridden.elements.iter().all(|element| {
            element.yield_strain_of() == 0.05 && element.break_strain_of() == 0.1
        })
    );
    assert_eq!(
        SoftElementState::new(
            SoftElementKind::Distance,
            [3, 4, u32::MAX, u32::MAX],
            0.5,
            true
        )
        .participants()
        .collect::<Vec<_>>(),
        vec![3, 4]
    );
}

#[test]
fn a_soft_attachment_binds_one_particle_to_a_body() {
    let body = BodyHandle {
        id: 7,
        generation: 2,
    };
    let anchor = SoftAttachment::new(1, body, [0.5, 0.0, 0.0]);
    assert_eq!(anchor.particle(), 1);
    assert_eq!(anchor.body(), body);
    assert_eq!(anchor.local(), [0.5, 0.0, 0.0]);
    let net = SoftBodyDesc::net(vec![[0.0; 3], [0.0, 1.0, 0.0]], vec![[0, 1]]);
    let attached = net.clone().attach(anchor);
    assert_eq!(attached.attachments, vec![anchor]);
    assert!(attached.attachments[0].body() == body);
    assert!(
        catch_unwind(AssertUnwindSafe(|| attached.clone().attach(anchor))).is_err(),
        "a soft particle carries at most one attachment"
    );
    assert!(
        catch_unwind(AssertUnwindSafe(|| {
            net.clone().attach(SoftAttachment::new(2, body, [0.0; 3]))
        }))
        .is_err(),
        "a soft attachment must reference a live particle"
    );
}