use std::ptr::NonNull;
use oxijolt_sys::*;
use super::rotational::{check_friction, check_motor};
use super::world::{sealed, ConstraintSettings};
use super::{
constraint_base, non_negative, within, MotorSettings, SpringSettings, FRICTION_RULE,
TARGET_VELOCITY_RULE,
};
use crate::limits;
use crate::math::is_unit;
use crate::owned::{JoltObject, Owned};
use crate::{
ConstraintError, ConstraintMut, ConstraintRef, MotorState, PathConstraint, Quat, Vec3,
};
impl JoltObject for JPH_PathConstraintPath {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_PathConstraintPath_Destroy(ptr) };
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct HermitePathPoint {
pub position: Vec3,
pub tangent: Vec3,
}
const MIN_CHORD: f64 = 1.0e-3;
const SEGMENT_RULE: &str =
"each path segment needs a chord of at least 1 mm and tangents that follow it";
#[derive(Clone, Debug, PartialEq)]
pub struct HermitePath {
normal: Vec3,
points: Vec<HermitePathPoint>,
looping: bool,
}
impl HermitePath {
pub const MAX_POINTS: usize = 4096;
pub fn new(
normal: Vec3,
points: Vec<HermitePathPoint>,
looping: bool,
) -> Result<Self, ConstraintError> {
let invalid = |what| Err(ConstraintError::InvalidValue(what));
if !(2..=Self::MAX_POINTS).contains(&points.len()) {
return invalid("a path needs between 2 and HermitePath::MAX_POINTS points");
}
if !is_unit(normal) {
return invalid("the path normal must be a unit vector");
}
for point in &points {
if !(limits::is_local_offset(point.position) && limits::is_local_offset(point.tangent))
{
return invalid(
"path positions and tangents must be within limits::MAX_SHAPE_EXTENT per axis",
);
}
}
let segments = if looping {
points.len()
} else {
points.len() - 1
};
for i in 0..segments {
let next = (i + 1) % points.len();
if !segment_is_valid(normal, points[i], points[next]) {
return invalid(SEGMENT_RULE);
}
}
Ok(Self {
normal,
points,
looping,
})
}
pub fn max_fraction(&self) -> f32 {
if self.looping {
self.points.len() as f32
} else {
(self.points.len() - 1) as f32
}
}
pub fn normal(&self) -> Vec3 {
self.normal
}
pub fn points(&self) -> &[HermitePathPoint] {
&self.points
}
pub fn is_looping(&self) -> bool {
self.looping
}
fn point_at(&self, fraction: f32) -> Vec3 {
let count = self.points.len();
let mut index = fraction.trunc() as usize;
let mut t = fraction - index as f32;
if self.looping {
index %= count;
} else if index >= count - 1 {
index = count - 2;
t = 1.0;
}
let (p1, p2) = (self.points[index], self.points[(index + 1) % count]);
let (t2, t3) = (t * t, t * t * t);
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h10 = t3 - 2.0 * t2 + t;
let h01 = -2.0 * t3 + 3.0 * t2;
let h11 = t3 - t2;
let blend = |a: f32, b: f32, c: f32, d: f32| h00 * a + h10 * b + h01 * c + h11 * d;
Vec3::new(
blend(p1.position.x, p1.tangent.x, p2.position.x, p2.tangent.x),
blend(p1.position.y, p1.tangent.y, p2.position.y, p2.tangent.y),
blend(p1.position.z, p1.tangent.z, p2.position.z, p2.tangent.z),
)
}
fn reach(&self) -> f64 {
self.points
.iter()
.map(|point| {
limits::f64_length(point.position) + limits::f64_length(point.tangent) / 3.0
})
.fold(0.0, f64::max)
}
fn create(&self) -> Owned<JPH_PathConstraintPath> {
let path = unsafe { Owned::from_raw(JPH_PathConstraintPathHermite_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the path"));
let normal = self.normal.to_jph();
for point in &self.points {
let position = point.position.to_jph();
let tangent = point.tangent.to_jph();
unsafe {
JPH_PathConstraintPathHermite_AddPoint(path.as_ptr(), &position, &tangent, &normal)
};
}
unsafe { JPH_PathConstraintPath_SetIsLooping(path.as_ptr(), self.looping) };
path
}
}
fn to_f64(v: Vec3) -> [f64; 3] {
[v.x, v.y, v.z].map(f64::from)
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn length(a: [f64; 3]) -> f64 {
dot(a, a).sqrt()
}
fn segment_is_valid(normal: Vec3, p1: HermitePathPoint, p2: HermitePathPoint) -> bool {
let n = to_f64(normal);
let (a1, a2) = (to_f64(p1.position), to_f64(p2.position));
let (m1, m2) = (to_f64(p1.tangent), to_f64(p2.tangent));
let chord = [a2[0] - a1[0], a2[1] - a1[1], a2[2] - a1[2]];
let chord_length = length(chord);
if chord_length < MIN_CHORD {
return false;
}
let unit_chord = chord.map(|c| c / chord_length);
let (a, b) = (dot(m1, unit_chord), dot(m2, unit_chord));
let quadratic = -6.0 * chord_length + 3.0 * a + 3.0 * b;
let linear = 6.0 * chord_length - 4.0 * a - 2.0 * b;
let mut minimum = a.min(b);
if quadratic != 0.0 {
let vertex = -linear / (2.0 * quadratic);
if vertex > 0.0 && vertex < 1.0 {
let at_vertex = (quadratic * vertex + linear) * vertex + a;
minimum = minimum.min(at_vertex);
}
}
let off_plane = 1.5 * dot(chord, n).abs() + dot(m1, n).abs() + dot(m2, n).abs();
let scale = length(a1) + length(a2) + length(m1) + length(m2);
let required = (1.0e-3 * chord_length)
.max(1.0e-5 * scale)
.max(2.0 * off_plane);
minimum >= required
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PathRotationConstraint {
#[default]
Free,
ConstrainAroundTangent,
ConstrainAroundNormal,
ConstrainAroundBinormal,
ConstrainToPath,
FullyConstrained,
}
impl PathRotationConstraint {
fn to_jph(self) -> JPH_PathRotationConstraintType {
match self {
Self::Free => JPH_PathRotationConstraintType_Free,
Self::ConstrainAroundTangent => JPH_PathRotationConstraintType_ConstrainAroundTangent,
Self::ConstrainAroundNormal => JPH_PathRotationConstraintType_ConstrainAroundNormal,
Self::ConstrainAroundBinormal => JPH_PathRotationConstraintType_ConstrainAroundBinormal,
Self::ConstrainToPath => JPH_PathRotationConstraintType_ConstrainToPath,
Self::FullyConstrained => JPH_PathRotationConstraintType_FullyConstrained,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PathConstraintSettings {
path: HermitePath,
path_position: Vec3,
path_rotation: Quat,
path_fraction: f32,
max_friction_force: f32,
rotation_constraint: PathRotationConstraint,
position_motor: MotorSettings,
}
impl PathConstraintSettings {
pub fn new(path: HermitePath) -> Self {
Self {
path,
path_position: Vec3::ZERO,
path_rotation: Quat::IDENTITY,
path_fraction: 0.0,
max_friction_force: 0.0,
rotation_constraint: PathRotationConstraint::Free,
position_motor: MotorSettings::default(),
}
}
#[must_use]
pub fn path_position(mut self, value: Vec3) -> Self {
self.path_position = value;
self
}
#[must_use]
pub fn path_rotation(mut self, value: Quat) -> Self {
self.path_rotation = value;
self
}
#[must_use]
pub fn path_fraction(mut self, value: f32) -> Self {
self.path_fraction = value;
self
}
#[must_use]
pub fn max_friction_force(mut self, value: f32) -> Self {
self.max_friction_force = value;
self
}
#[must_use]
pub fn rotation_constraint(mut self, value: PathRotationConstraint) -> Self {
self.rotation_constraint = value;
self
}
#[must_use]
pub fn position_motor(mut self, value: MotorSettings) -> Self {
self.position_motor = value;
self
}
}
impl PathConstraintSettings {
fn to_jph(&self, path: *const JPH_PathConstraintPath) -> JPH_PathConstraintSettings {
JPH_PathConstraintSettings {
base: constraint_base(),
path,
pathPosition: self.path_position.to_jph(),
pathRotation: self.path_rotation.to_jph(),
pathFraction: self.path_fraction,
maxFrictionForce: self.max_friction_force,
rotationConstraintType: self.rotation_constraint.to_jph(),
positionMotorSettings: self.position_motor.to_jph(),
}
}
}
impl sealed::Settings for PathConstraintSettings {
fn validate(&self) -> Result<(), &'static str> {
if !limits::is_local_offset(self.path_position) {
return Err("path position must be within limits::MAX_SHAPE_EXTENT per axis");
}
if !self.path_rotation.is_valid_rotation() {
return Err("path rotation must be a finite unit quaternion");
}
if !within(self.path_fraction, 0.0, self.path.max_fraction()) {
return Err("path fraction must be between 0 and the path's max fraction");
}
if !non_negative(self.max_friction_force) {
return Err(FRICTION_RULE);
}
self.position_motor.validate()
}
fn springs(&self) -> Vec<SpringSettings> {
vec![self.position_motor.spring]
}
fn anchors(&self) -> Option<[sealed::Anchor; 2]> {
let attached = self
.path_rotation
.rotate(self.path.point_at(self.path_fraction));
Some([
sealed::Anchor::OnBody1 {
offset: self.path_position,
radius: self.path.reach(),
},
sealed::Anchor::OnBody1 {
offset: Vec3::new(
self.path_position.x + attached.x,
self.path_position.y + attached.y,
self.path_position.z + attached.z,
),
radius: 0.0,
},
])
}
unsafe fn create(
&self,
body1: NonNull<JPH_Body>,
body2: NonNull<JPH_Body>,
) -> *mut JPH_Constraint {
let path = self.path.create();
let settings = self.to_jph(path.as_ptr());
unsafe { JPH_PathConstraint_Create(&settings, body1.as_ptr(), body2.as_ptr()) }.cast()
}
}
impl ConstraintSettings for PathConstraintSettings {
type Kind = PathConstraint;
}
impl ConstraintRef<'_, PathConstraint> {
fn path(&self) -> *const JPH_PathConstraintPath {
unsafe { JPH_PathConstraint_GetPath(self.ptr()) }
}
pub fn max_fraction(&self) -> f32 {
unsafe { JPH_PathConstraintPath_GetPathMaxFraction(self.path()) }
}
pub fn is_looping(&self) -> bool {
unsafe { JPH_PathConstraintPath_IsLooping(self.path()) }
}
pub fn path_fraction(&self) -> f32 {
unsafe { JPH_PathConstraint_GetPathFraction(self.ptr()) }
}
pub fn closest_fraction(&self, point: Vec3, hint: f32) -> Result<f32, ConstraintError> {
if !(limits::is_local_offset(point) && hint.is_finite()) {
return Err(ConstraintError::InvalidValue(
"the point must be within limits::MAX_SHAPE_EXTENT per axis and the hint finite",
));
}
let point = point.to_jph();
Ok(unsafe { JPH_PathConstraintPath_GetClosestPoint(self.path(), &point, hint) })
}
pub fn motor_state(&self) -> MotorState {
MotorState::from_jph(unsafe { JPH_PathConstraint_GetPositionMotorState(self.ptr()) })
}
pub fn target_path_fraction(&self) -> f32 {
unsafe { JPH_PathConstraint_GetTargetPathFraction(self.ptr()) }
}
pub fn target_velocity(&self) -> f32 {
unsafe { JPH_PathConstraint_GetTargetVelocity(self.ptr()) }
}
pub fn max_friction_force(&self) -> f32 {
unsafe { JPH_PathConstraint_GetMaxFrictionForce(self.ptr()) }
}
pub fn total_lambda_position(&self) -> [f32; 2] {
let mut value = [0.0; 2];
unsafe { JPH_PathConstraint_GetTotalLambdaPosition(self.ptr(), value.as_mut_ptr()) };
value
}
pub fn total_lambda_position_limits(&self) -> f32 {
unsafe { JPH_PathConstraint_GetTotalLambdaPositionLimits(self.ptr()) }
}
pub fn total_lambda_motor(&self) -> f32 {
unsafe { JPH_PathConstraint_GetTotalLambdaMotor(self.ptr()) }
}
pub fn position_motor_settings(&self) -> MotorSettings {
let mut motor: JPH_MotorSettings = unsafe { std::mem::zeroed() };
unsafe { JPH_PathConstraint_GetPositionMotorSettings(self.ptr(), &mut motor) };
MotorSettings::from_jph(motor)
}
pub fn total_lambda_rotation_hinge(&self) -> [f32; 2] {
let mut value = [0.0; 2];
unsafe { JPH_PathConstraint_GetTotalLambdaRotationHinge(self.ptr(), value.as_mut_ptr()) };
value
}
pub fn total_lambda_rotation(&self) -> Vec3 {
let mut value = Vec3::ZERO.to_jph();
unsafe { JPH_PathConstraint_GetTotalLambdaRotation(self.ptr(), &mut value) };
Vec3::from_jph(value)
}
}
impl ConstraintMut<'_, PathConstraint> {
fn max_fraction(&self) -> f32 {
unsafe { JPH_PathConstraintPath_GetPathMaxFraction(JPH_PathConstraint_GetPath(self.ptr())) }
}
pub fn set_motor_state(&mut self, state: MotorState) {
unsafe { JPH_PathConstraint_SetPositionMotorState(self.ptr(), state.to_jph()) };
self.wake_bodies();
}
pub fn set_target_path_fraction(&mut self, fraction: f32) -> Result<(), ConstraintError> {
if !within(fraction, 0.0, self.max_fraction()) {
return Err(ConstraintError::InvalidValue(
"target path fraction must be between 0 and the path's max fraction",
));
}
unsafe { JPH_PathConstraint_SetTargetPathFraction(self.ptr(), fraction) };
self.wake_bodies();
Ok(())
}
pub fn set_target_velocity(&mut self, velocity: f32) -> Result<(), ConstraintError> {
let speed = limits::MAX_LINEAR_VELOCITY;
if !within(velocity, -speed, speed) {
return Err(ConstraintError::InvalidValue(TARGET_VELOCITY_RULE));
}
unsafe { JPH_PathConstraint_SetTargetVelocity(self.ptr(), velocity) };
self.wake_bodies();
Ok(())
}
pub fn set_position_motor_settings(
&mut self,
motor: MotorSettings,
) -> Result<(), ConstraintError> {
check_motor(motor, self.effective_mass_bound())?;
let motor = motor.to_jph();
unsafe { JPH_PathConstraint_SetPositionMotorSettings(self.ptr(), &motor) };
self.wake_bodies();
Ok(())
}
pub fn set_max_friction_force(&mut self, force: f32) -> Result<(), ConstraintError> {
check_friction(force)?;
unsafe { JPH_PathConstraint_SetMaxFrictionForce(self.ptr(), force) };
self.wake_bodies();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world::ensure_initialized;
#[test]
fn path_defaults_are_jolts() {
assert!(ensure_initialized());
let mut jolt: JPH_PathConstraintSettings = unsafe { std::mem::zeroed() };
unsafe { JPH_PathConstraintSettings_Init(&mut jolt) };
let x = Vec3::new(1.0, 0.0, 0.0);
let point = |position| HermitePathPoint {
position,
tangent: x,
};
let path = HermitePath::new(
Vec3::new(0.0, 1.0, 0.0),
vec![point(Vec3::ZERO), point(x)],
false,
)
.unwrap();
let ours = PathConstraintSettings::new(path).to_jph(std::ptr::null());
assert!(jolt.path.is_null());
assert_eq!(ours.base.enabled, jolt.base.enabled);
assert_eq!(
Vec3::from_jph(ours.pathPosition),
Vec3::from_jph(jolt.pathPosition)
);
assert_eq!(
Quat::from_jph(ours.pathRotation),
Quat::from_jph(jolt.pathRotation)
);
assert_eq!(ours.pathFraction.to_bits(), jolt.pathFraction.to_bits());
assert_eq!(
ours.maxFrictionForce.to_bits(),
jolt.maxFrictionForce.to_bits()
);
assert_eq!(ours.rotationConstraintType, jolt.rotationConstraintType);
assert_eq!(
MotorSettings::from_jph(ours.positionMotorSettings),
MotorSettings::from_jph(jolt.positionMotorSettings)
);
}
}