mod coupling;
mod id;
mod lever;
mod linear;
mod path;
mod pulley;
mod rotational;
mod slider;
mod world;
use std::f32::consts::PI;
use oxijolt_sys::*;
pub use coupling::{GearConstraintSettings, RackAndPinionConstraintSettings};
pub use id::{AnyConstraintId, ConstraintId};
pub use linear::{
DistanceConstraintSettings, DistanceRange, FixedConstraintSettings, PointConstraintSettings,
};
pub use path::{HermitePath, HermitePathPoint, PathConstraintSettings, PathRotationConstraint};
pub use pulley::{PulleyConstraintSettings, PulleyLength};
pub use rotational::ConeConstraintSettings;
pub use slider::SliderConstraintSettings;
pub(crate) use world::ConstraintEntry;
pub use world::{
ConeConstraint, ConstraintKind, ConstraintMut, ConstraintRef, ConstraintSettings,
ConstraintType, DistanceConstraint, FixedConstraint, GearConstraint, HingeConstraint,
MotorState, PathConstraint, PointConstraint, PulleyConstraint, RackAndPinionConstraint,
SixDofConstraint, SliderConstraint, SwingTwistConstraint,
};
use crate::limits;
use crate::math::is_unit;
use crate::{RVec3, Vec3};
const PERPENDICULAR_TOLERANCE: f32 = 1.0e-4;
pub(crate) fn constraint_base() -> JPH_ConstraintSettings {
JPH_ConstraintSettings {
enabled: true,
constraintPriority: 0,
numVelocityStepsOverride: 0,
numPositionStepsOverride: 0,
drawConstraintSize: 1.0,
userData: 0,
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ConstraintSpace {
LocalToBodyCom,
#[default]
WorldSpace,
}
impl ConstraintSpace {
fn to_jph(self) -> JPH_ConstraintSpace {
match self {
Self::LocalToBodyCom => JPH_ConstraintSpace_LocalToBodyCOM,
Self::WorldSpace => JPH_ConstraintSpace_WorldSpace,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum SwingType {
#[default]
Cone,
Pyramid,
}
impl SwingType {
fn to_jph(self) -> JPH_SwingType {
match self {
Self::Cone => JPH_SwingType_Cone,
Self::Pyramid => JPH_SwingType_Pyramid,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SpringSettings {
FrequencyAndDamping {
frequency: f32,
damping: f32,
},
StiffnessAndDamping {
stiffness: f32,
damping: f32,
},
}
impl Default for SpringSettings {
fn default() -> Self {
Self::FrequencyAndDamping {
frequency: 0.0,
damping: 0.0,
}
}
}
impl SpringSettings {
fn values(&self) -> (f32, f32) {
match *self {
Self::FrequencyAndDamping { frequency, damping } => (frequency, damping),
Self::StiffnessAndDamping { stiffness, damping } => (stiffness, damping),
}
}
fn is_soft(&self) -> bool {
self.values().0 > 0.0
}
fn validate(&self) -> Result<(), &'static str> {
let (strength, damping) = self.values();
if !(non_negative(strength) && non_negative(damping)) {
return Err("spring frequency, stiffness and damping must be finite and not negative");
}
if let Self::StiffnessAndDamping { .. } = self {
if strength > limits::MAX_SPRING_COEFFICIENT || damping > limits::MAX_SPRING_COEFFICIENT
{
return Err(
"spring stiffness and damping must be at most limits::MAX_SPRING_COEFFICIENT",
);
}
}
Ok(())
}
pub(crate) fn fits_effective_mass(&self, effective_mass_bound: f64) -> bool {
let Self::FrequencyAndDamping { frequency, damping } = *self else {
return true;
};
if frequency == 0.0 {
return true;
}
let omega = 2.0 * std::f64::consts::PI * f64::from(frequency);
let bound = f64::from(limits::MAX_SPRING_COEFFICIENT);
effective_mass_bound * omega * omega <= bound
&& 2.0 * effective_mass_bound * f64::from(damping) * omega <= bound
}
fn from_jph(spring: JPH_SpringSettings) -> Self {
let (strength, damping) = (spring.frequencyOrStiffness, spring.damping);
if spring.mode == JPH_SpringMode_StiffnessAndDamping {
Self::StiffnessAndDamping {
stiffness: strength,
damping,
}
} else {
Self::FrequencyAndDamping {
frequency: strength,
damping,
}
}
}
pub(crate) fn to_jph(self) -> JPH_SpringSettings {
let (strength, damping) = self.values();
let mode = match self {
Self::FrequencyAndDamping { .. } => JPH_SpringMode_FrequencyAndDamping,
Self::StiffnessAndDamping { .. } => JPH_SpringMode_StiffnessAndDamping,
};
JPH_SpringSettings {
mode,
frequencyOrStiffness: strength,
damping,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MotorSettings {
spring: SpringSettings,
min_force_limit: f32,
max_force_limit: f32,
min_torque_limit: f32,
max_torque_limit: f32,
}
impl Default for MotorSettings {
fn default() -> Self {
Self {
spring: SpringSettings::FrequencyAndDamping {
frequency: 2.0,
damping: 1.0,
},
min_force_limit: -f32::MAX,
max_force_limit: f32::MAX,
min_torque_limit: -f32::MAX,
max_torque_limit: f32::MAX,
}
}
}
impl MotorSettings {
#[must_use]
pub fn spring(mut self, value: SpringSettings) -> Self {
self.spring = value;
self
}
#[must_use]
pub fn force_limits(mut self, min: f32, max: f32) -> Self {
self.min_force_limit = min;
self.max_force_limit = max;
self
}
#[must_use]
pub fn torque_limits(mut self, min: f32, max: f32) -> Self {
self.min_torque_limit = min;
self.max_torque_limit = max;
self
}
fn validate(&self) -> Result<(), &'static str> {
self.spring.validate()?;
let ordered = |min: f32, max: f32| min.is_finite() && max.is_finite() && min <= max;
if ordered(self.min_force_limit, self.max_force_limit)
&& ordered(self.min_torque_limit, self.max_torque_limit)
{
Ok(())
} else {
Err("motor force and torque limits must be finite with min <= max")
}
}
fn from_jph(motor: JPH_MotorSettings) -> Self {
Self {
spring: SpringSettings::from_jph(motor.springSettings),
min_force_limit: motor.minForceLimit,
max_force_limit: motor.maxForceLimit,
min_torque_limit: motor.minTorqueLimit,
max_torque_limit: motor.maxTorqueLimit,
}
}
fn to_jph(self) -> JPH_MotorSettings {
JPH_MotorSettings {
springSettings: self.spring.to_jph(),
minForceLimit: self.min_force_limit,
maxForceLimit: self.max_force_limit,
minTorqueLimit: self.min_torque_limit,
maxTorqueLimit: self.max_torque_limit,
}
}
}
const FRICTION_RULE: &str = "friction must be finite and not negative";
const FRICTION_TORQUE_RULE: &str = "friction torque must be finite and not negative";
fn non_negative(value: f32) -> bool {
value.is_finite() && value >= 0.0
}
const TARGET_VELOCITY_RULE: &str =
"target velocity must be finite and at most limits::MAX_LINEAR_VELOCITY";
fn within(value: f32, min: f32, max: f32) -> bool {
value.is_finite() && (min..=max).contains(&value)
}
fn validate_point(point: RVec3) -> Result<(), &'static str> {
if limits::is_in_frame(point) {
Ok(())
} else {
Err("constraint frame points must be finite and within limits::MAX_POSITION")
}
}
fn point_anchors(
space: ConstraintSpace,
point1: RVec3,
point2: RVec3,
) -> [world::sealed::Anchor; 2] {
[point1, point2].map(|point| match space {
ConstraintSpace::WorldSpace => world::sealed::Anchor::World(point),
#[allow(clippy::unnecessary_cast)]
ConstraintSpace::LocalToBodyCom => world::sealed::Anchor::CenterOfMass(Vec3::new(
point.x as f32,
point.y as f32,
point.z as f32,
)),
})
}
const FRAME_AXES_RULE: &str = "constraint frame axes must be unit vectors";
fn validate_frame(point: RVec3, axis_a: Vec3, axis_b: Vec3) -> Result<(), &'static str> {
validate_point(point)?;
if !(is_unit(axis_a) && is_unit(axis_b)) {
return Err(FRAME_AXES_RULE);
}
if axis_a.dot(axis_b).abs() > PERPENDICULAR_TOLERANCE {
return Err("the two axes of a constraint frame must be perpendicular");
}
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub struct SwingTwistConstraintSettings {
space: ConstraintSpace,
position1: RVec3,
twist_axis1: Vec3,
plane_axis1: Vec3,
position2: RVec3,
twist_axis2: Vec3,
plane_axis2: Vec3,
swing_type: SwingType,
normal_half_cone_angle: f32,
plane_half_cone_angle: f32,
twist_min_angle: f32,
twist_max_angle: f32,
max_friction_torque: f32,
swing_motor: MotorSettings,
twist_motor: MotorSettings,
}
impl Default for SwingTwistConstraintSettings {
fn default() -> Self {
Self::new(
RVec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
)
}
}
impl SwingTwistConstraintSettings {
pub fn new(position: RVec3, twist_axis: Vec3, plane_axis: Vec3) -> Self {
Self {
space: ConstraintSpace::WorldSpace,
position1: position,
twist_axis1: twist_axis,
plane_axis1: plane_axis,
position2: position,
twist_axis2: twist_axis,
plane_axis2: plane_axis,
swing_type: SwingType::Cone,
normal_half_cone_angle: 0.0,
plane_half_cone_angle: 0.0,
twist_min_angle: 0.0,
twist_max_angle: 0.0,
max_friction_torque: 0.0,
swing_motor: MotorSettings::default(),
twist_motor: MotorSettings::default(),
}
}
#[must_use]
pub fn space(mut self, value: ConstraintSpace) -> Self {
self.space = value;
self
}
#[must_use]
pub fn frame1(mut self, position: RVec3, twist_axis: Vec3, plane_axis: Vec3) -> Self {
self.position1 = position;
self.twist_axis1 = twist_axis;
self.plane_axis1 = plane_axis;
self
}
#[must_use]
pub fn frame2(mut self, position: RVec3, twist_axis: Vec3, plane_axis: Vec3) -> Self {
self.position2 = position;
self.twist_axis2 = twist_axis;
self.plane_axis2 = plane_axis;
self
}
#[must_use]
pub fn swing_type(mut self, value: SwingType) -> Self {
self.swing_type = value;
self
}
#[must_use]
pub fn half_cone_angles(mut self, normal: f32, plane: f32) -> Self {
self.normal_half_cone_angle = normal;
self.plane_half_cone_angle = plane;
self
}
#[must_use]
pub fn twist_limits(mut self, min: f32, max: f32) -> Self {
self.twist_min_angle = min;
self.twist_max_angle = max;
self
}
#[must_use]
pub fn max_friction_torque(mut self, value: f32) -> Self {
self.max_friction_torque = value;
self
}
#[must_use]
pub fn swing_motor(mut self, value: MotorSettings) -> Self {
self.swing_motor = value;
self
}
#[must_use]
pub fn twist_motor(mut self, value: MotorSettings) -> Self {
self.twist_motor = value;
self
}
pub(crate) fn validate(&self) -> Result<(), &'static str> {
validate_frame(self.position1, self.twist_axis1, self.plane_axis1)?;
validate_frame(self.position2, self.twist_axis2, self.plane_axis2)?;
if !(within(self.normal_half_cone_angle, 0.0, PI)
&& within(self.plane_half_cone_angle, 0.0, PI))
{
return Err("swing-twist half cone angles must be between 0 and pi");
}
if !(within(self.twist_min_angle, -PI, PI)
&& within(self.twist_max_angle, -PI, PI)
&& self.twist_min_angle <= self.twist_max_angle)
{
return Err("swing-twist twist limits must be between -pi and pi with min <= max");
}
if !non_negative(self.max_friction_torque) {
return Err(FRICTION_TORQUE_RULE);
}
self.swing_motor.validate()?;
self.twist_motor.validate()
}
pub(crate) fn springs(&self) -> impl Iterator<Item = SpringSettings> {
[self.swing_motor.spring, self.twist_motor.spring].into_iter()
}
pub(crate) fn to_jph(&self) -> JPH_SwingTwistConstraintSettings {
JPH_SwingTwistConstraintSettings {
base: constraint_base(),
space: self.space.to_jph(),
position1: self.position1.to_jph(),
twistAxis1: self.twist_axis1.to_jph(),
planeAxis1: self.plane_axis1.to_jph(),
position2: self.position2.to_jph(),
twistAxis2: self.twist_axis2.to_jph(),
planeAxis2: self.plane_axis2.to_jph(),
swingType: self.swing_type.to_jph(),
normalHalfConeAngle: self.normal_half_cone_angle,
planeHalfConeAngle: self.plane_half_cone_angle,
twistMinAngle: self.twist_min_angle,
twistMaxAngle: self.twist_max_angle,
maxFrictionTorque: self.max_friction_torque,
swingMotorSettings: self.swing_motor.to_jph(),
twistMotorSettings: self.twist_motor.to_jph(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct HingeConstraintSettings {
space: ConstraintSpace,
point1: RVec3,
hinge_axis1: Vec3,
normal_axis1: Vec3,
point2: RVec3,
hinge_axis2: Vec3,
normal_axis2: Vec3,
limits_min: f32,
limits_max: f32,
limits_spring: SpringSettings,
max_friction_torque: f32,
motor: MotorSettings,
}
impl Default for HingeConstraintSettings {
fn default() -> Self {
Self::new(
RVec3::ZERO,
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
)
}
}
impl HingeConstraintSettings {
pub fn new(point: RVec3, hinge_axis: Vec3, normal_axis: Vec3) -> Self {
Self {
space: ConstraintSpace::WorldSpace,
point1: point,
hinge_axis1: hinge_axis,
normal_axis1: normal_axis,
point2: point,
hinge_axis2: hinge_axis,
normal_axis2: normal_axis,
limits_min: -PI,
limits_max: PI,
limits_spring: SpringSettings::default(),
max_friction_torque: 0.0,
motor: MotorSettings::default(),
}
}
#[must_use]
pub fn space(mut self, value: ConstraintSpace) -> Self {
self.space = value;
self
}
#[must_use]
pub fn frame1(mut self, point: RVec3, hinge_axis: Vec3, normal_axis: Vec3) -> Self {
self.point1 = point;
self.hinge_axis1 = hinge_axis;
self.normal_axis1 = normal_axis;
self
}
#[must_use]
pub fn frame2(mut self, point: RVec3, hinge_axis: Vec3, normal_axis: Vec3) -> Self {
self.point2 = point;
self.hinge_axis2 = hinge_axis;
self.normal_axis2 = normal_axis;
self
}
#[must_use]
pub fn limits(mut self, min: f32, max: f32) -> Self {
self.limits_min = min;
self.limits_max = max;
self
}
#[must_use]
pub fn limits_spring(mut self, value: SpringSettings) -> Self {
self.limits_spring = value;
self
}
#[must_use]
pub fn max_friction_torque(mut self, value: f32) -> Self {
self.max_friction_torque = value;
self
}
#[must_use]
pub fn motor(mut self, value: MotorSettings) -> Self {
self.motor = value;
self
}
pub(crate) fn validate(&self) -> Result<(), &'static str> {
validate_frame(self.point1, self.hinge_axis1, self.normal_axis1)?;
validate_frame(self.point2, self.hinge_axis2, self.normal_axis2)?;
self.limits_spring.validate()?;
validate_hinge_limits(self.limits_min, self.limits_max, self.limits_spring)?;
if !non_negative(self.max_friction_torque) {
return Err(FRICTION_TORQUE_RULE);
}
self.motor.validate()
}
pub(crate) fn springs(&self) -> impl Iterator<Item = SpringSettings> {
[self.limits_spring, self.motor.spring].into_iter()
}
pub(crate) fn to_jph(&self) -> JPH_HingeConstraintSettings {
JPH_HingeConstraintSettings {
base: constraint_base(),
space: self.space.to_jph(),
point1: self.point1.to_jph(),
hingeAxis1: self.hinge_axis1.to_jph(),
normalAxis1: self.normal_axis1.to_jph(),
point2: self.point2.to_jph(),
hingeAxis2: self.hinge_axis2.to_jph(),
normalAxis2: self.normal_axis2.to_jph(),
limitsMin: self.limits_min,
limitsMax: self.limits_max,
limitsSpringSettings: self.limits_spring.to_jph(),
maxFrictionTorque: self.max_friction_torque,
motorSettings: self.motor.to_jph(),
}
}
}
fn validate_hinge_limits(
min: f32,
max: f32,
limits_spring: SpringSettings,
) -> Result<(), &'static str> {
if !(within(min, -PI, 0.0) && within(max, 0.0, PI)) {
return Err("hinge limits must be min in [-pi, 0] and max in [0, pi]");
}
if min == max && !limits_spring.is_soft() {
return Err("hinge limits with min == max need a soft limits spring");
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SixDofConstraintAxis {
TranslationX,
TranslationY,
TranslationZ,
RotationX,
RotationY,
RotationZ,
}
impl SixDofConstraintAxis {
pub(crate) const ALL: [Self; 6] = [
Self::TranslationX,
Self::TranslationY,
Self::TranslationZ,
Self::RotationX,
Self::RotationY,
Self::RotationZ,
];
pub(crate) const ROTATIONS: [Self; 3] = [Self::RotationX, Self::RotationY, Self::RotationZ];
pub(crate) fn index(self) -> usize {
self as usize
}
fn is_translation(self) -> bool {
self.index() < 3
}
pub(crate) fn to_jph(self) -> JPH_SixDOFConstraintAxis {
match self {
Self::TranslationX => JPH_SixDOFConstraintAxis_TranslationX,
Self::TranslationY => JPH_SixDOFConstraintAxis_TranslationY,
Self::TranslationZ => JPH_SixDOFConstraintAxis_TranslationZ,
Self::RotationX => JPH_SixDOFConstraintAxis_RotationX,
Self::RotationY => JPH_SixDOFConstraintAxis_RotationY,
Self::RotationZ => JPH_SixDOFConstraintAxis_RotationZ,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum SixDofAxis {
#[default]
Free,
Fixed,
Limited {
min: f32,
max: f32,
},
}
impl SixDofAxis {
fn limits(self) -> (f32, f32) {
match self {
Self::Free => (-f32::MAX, f32::MAX),
Self::Fixed => (f32::MAX, -f32::MAX),
Self::Limited { min, max } => (min, max),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SixDofConstraintSettings {
space: ConstraintSpace,
position1: RVec3,
axis_x1: Vec3,
axis_y1: Vec3,
position2: RVec3,
axis_x2: Vec3,
axis_y2: Vec3,
swing_type: SwingType,
axes: [SixDofAxis; 6],
max_friction: [f32; 6],
limits_springs: [SpringSettings; 6],
motors: [MotorSettings; 6],
}
impl Default for SixDofConstraintSettings {
fn default() -> Self {
Self::new(
RVec3::ZERO,
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
)
}
}
impl SixDofConstraintSettings {
pub fn new(position: RVec3, axis_x: Vec3, axis_y: Vec3) -> Self {
Self {
space: ConstraintSpace::WorldSpace,
position1: position,
axis_x1: axis_x,
axis_y1: axis_y,
position2: position,
axis_x2: axis_x,
axis_y2: axis_y,
swing_type: SwingType::Cone,
axes: [SixDofAxis::Free; 6],
max_friction: [0.0; 6],
limits_springs: [SpringSettings::default(); 6],
motors: [MotorSettings::default(); 6],
}
}
#[must_use]
pub fn space(mut self, value: ConstraintSpace) -> Self {
self.space = value;
self
}
#[must_use]
pub fn frame1(mut self, position: RVec3, axis_x: Vec3, axis_y: Vec3) -> Self {
self.position1 = position;
self.axis_x1 = axis_x;
self.axis_y1 = axis_y;
self
}
#[must_use]
pub fn frame2(mut self, position: RVec3, axis_x: Vec3, axis_y: Vec3) -> Self {
self.position2 = position;
self.axis_x2 = axis_x;
self.axis_y2 = axis_y;
self
}
#[must_use]
pub fn swing_type(mut self, value: SwingType) -> Self {
self.swing_type = value;
self
}
#[must_use]
pub fn axis(mut self, which: SixDofConstraintAxis, value: SixDofAxis) -> Self {
self.axes[which.index()] = value;
self
}
#[must_use]
pub fn max_friction(mut self, which: SixDofConstraintAxis, value: f32) -> Self {
self.max_friction[which.index()] = value;
self
}
#[must_use]
pub fn limits_spring(mut self, which: SixDofConstraintAxis, value: SpringSettings) -> Self {
self.limits_springs[which.index()] = value;
self
}
#[must_use]
pub fn motor(mut self, which: SixDofConstraintAxis, value: MotorSettings) -> Self {
self.motors[which.index()] = value;
self
}
pub(crate) fn validate(&self) -> Result<(), &'static str> {
validate_frame(self.position1, self.axis_x1, self.axis_y1)?;
validate_frame(self.position2, self.axis_x2, self.axis_y2)?;
for which in SixDofConstraintAxis::ALL {
let i = which.index();
if let SixDofAxis::Limited { min, max } = self.axes[i] {
if !(min.is_finite() && max.is_finite() && min < max) {
return Err(
"limited axes need finite limits with min < max; use Fixed for min == max",
);
}
if which.is_translation() {
let extent = limits::MAX_SHAPE_EXTENT;
if !(within(min, -extent, extent) && within(max, -extent, extent)) {
return Err("translation limits must be within limits::MAX_SHAPE_EXTENT");
}
} else if !(within(min, -PI, PI) && within(max, -PI, PI)) {
return Err("rotation limits must be between -pi and pi");
}
}
if !non_negative(self.max_friction[i]) {
return Err(FRICTION_RULE);
}
let spring = self.limits_springs[i];
spring.validate()?;
if spring.is_soft() {
if !which.is_translation() {
return Err("limit springs exist only for translation axes");
}
if self.axes[i] == SixDofAxis::Free {
return Err("a limit spring needs a limited or fixed translation axis");
}
}
self.motors[i].validate()?;
}
if self.swing_type == SwingType::Cone {
for which in [
SixDofConstraintAxis::RotationY,
SixDofConstraintAxis::RotationZ,
] {
if let SixDofAxis::Limited { min, max } = self.axes[which.index()] {
if min != -max {
return Err(
"cone swing limits must be symmetric; use SwingType::Pyramid for asymmetric ones",
);
}
}
}
}
Ok(())
}
pub(crate) fn springs(&self) -> impl Iterator<Item = SpringSettings> + '_ {
self.limits_springs[..3]
.iter()
.copied()
.chain(self.motors.iter().map(|motor| motor.spring))
}
pub(crate) fn to_jph(&self) -> JPH_SixDOFConstraintSettings {
let limits = self.axes.map(SixDofAxis::limits);
JPH_SixDOFConstraintSettings {
base: constraint_base(),
space: self.space.to_jph(),
position1: self.position1.to_jph(),
axisX1: self.axis_x1.to_jph(),
axisY1: self.axis_y1.to_jph(),
position2: self.position2.to_jph(),
axisX2: self.axis_x2.to_jph(),
axisY2: self.axis_y2.to_jph(),
maxFriction: self.max_friction,
swingType: self.swing_type.to_jph(),
limitMin: limits.map(|(min, _)| min),
limitMax: limits.map(|(_, max)| max),
limitsSpringSettings: [0, 1, 2].map(|i| self.limits_springs[i].to_jph()),
motorSettings: self.motors.map(MotorSettings::to_jph),
}
}
}
#[cfg(test)]
mod tests;