use std::collections::BTreeMap;
use std::fmt;
use std::ptr::null;
use oxijolt_sys::*;
use crate::limits;
use crate::math::{is_finite_non_negative, is_finite_positive};
use crate::owned::{JoltObject, Owned};
use crate::world::ensure_initialized;
mod binary_state;
mod compound;
mod create;
mod geometry;
mod hull;
mod mesh;
mod mutable;
mod plane;
mod scaled;
mod static_only;
mod tapered;
use crate::material::height_field_materials;
use crate::{PhysicsMaterial, Quat, ShapeError, Vec3};
pub(crate) use compound::compound_sub_shape_of;
use compound::{build_compound, check_child_pose, sub_shape_ids, RawCompoundChild};
pub use mesh::{DroppedTriangles, MeshBuildQuality, MeshSettings};
pub use mutable::MutableCompound;
pub(crate) use static_only::static_only_leaves_are_meshes_of;
pub struct Shape(Owned<JPH_Shape>);
impl fmt::Debug for Shape {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Shape").finish_non_exhaustive()
}
}
unsafe impl Send for Shape {}
unsafe impl Sync for Shape {}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct SubShapeId(u32);
impl SubShapeId {
pub(crate) fn new(raw: u32) -> Self {
Self(raw)
}
pub fn to_raw(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy)]
pub struct CompoundChild<'a> {
pub shape: &'a Shape,
pub position: Vec3,
pub rotation: Quat,
pub user_data: u32,
}
impl fmt::Debug for CompoundChild<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompoundChild")
.field("position", &self.position)
.field("rotation", &self.rotation)
.field("user_data", &self.user_data)
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CompoundSubShape {
pub index: u32,
pub user_data: u32,
}
pub(crate) struct ShapeSettings(Owned<JPH_ShapeSettings>);
impl JoltObject for JPH_ShapeSettings {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_ShapeSettings_Destroy(ptr) };
}
}
impl ShapeSettings {
pub(crate) unsafe fn from_raw(ptr: *mut JPH_ShapeSettings) -> Result<Self, ShapeError> {
unsafe { Owned::from_raw(ptr) }
.map(Self)
.ok_or(ShapeError::AllocationFailed)
}
pub(crate) fn as_ptr<T>(&self) -> *mut T {
self.0.as_ptr().cast()
}
}
pub(crate) type HeightFieldMaterials<'a> = (&'a [u8], &'a [*const JPH_PhysicsMaterial]);
unsafe fn height_field_settings(
sample_count: u32,
samples: &[f32],
settings: &HeightFieldSettings,
materials: Option<HeightFieldMaterials<'_>>,
) -> Result<ShapeSettings, ShapeError> {
let offset = settings.offset.to_jph();
let scale = settings.scale.to_jph();
let jolt_settings = unsafe {
ShapeSettings::from_raw(
match materials {
None => JPH_HeightFieldShapeSettings_Create(
samples.as_ptr(),
&offset,
&scale,
sample_count,
null(),
),
Some((indices, list)) => JPH_HeightFieldShapeSettings_Create2(
samples.as_ptr(),
&offset,
&scale,
sample_count,
indices.as_ptr(),
list.as_ptr(),
list.len() as u32,
),
}
.cast(),
)
}?;
let ptr = jolt_settings.as_ptr();
unsafe {
JPH_HeightFieldShapeSettings_SetBlockSize(ptr, settings.block_size);
JPH_HeightFieldShapeSettings_SetBitsPerSample(ptr, settings.bits_per_sample);
JPH_HeightFieldShapeSettings_SetActiveEdgeCosThresholdAngle(
ptr,
settings.active_edge_cos_threshold_angle,
);
}
Ok(jolt_settings)
}
pub(crate) fn initialize() -> Result<(), ShapeError> {
if ensure_initialized() {
Ok(())
} else {
Err(ShapeError::InitFailed)
}
}
#[derive(Clone, Debug)]
pub struct HeightFieldSettings<'a> {
materials: Option<(&'a [&'a PhysicsMaterial], &'a [u8])>,
offset: Vec3,
scale: Vec3,
block_size: u32,
bits_per_sample: u32,
active_edge_cos_threshold_angle: f32,
}
impl PartialEq for HeightFieldSettings<'_> {
fn eq(&self, other: &Self) -> bool {
let same_materials = match (self.materials, other.materials) {
(None, None) => true,
(Some((list, indices)), Some((other_list, other_indices))) => {
indices == other_indices
&& list.len() == other_list.len()
&& list
.iter()
.zip(other_list)
.all(|(a, b)| std::ptr::eq(*a, *b))
}
_ => false,
};
same_materials
&& self.offset == other.offset
&& self.scale == other.scale
&& self.block_size == other.block_size
&& self.bits_per_sample == other.bits_per_sample
&& self.active_edge_cos_threshold_angle == other.active_edge_cos_threshold_angle
}
}
impl Default for HeightFieldSettings<'_> {
fn default() -> Self {
Self {
materials: None,
offset: Vec3::ZERO,
scale: Vec3::new(1.0, 1.0, 1.0),
block_size: 2,
bits_per_sample: 8,
active_edge_cos_threshold_angle: 0.996195,
}
}
}
impl<'a> HeightFieldSettings<'a> {
#[must_use]
pub fn materials(mut self, list: &'a [&'a PhysicsMaterial], indices: &'a [u8]) -> Self {
self.materials = Some((list, indices));
self
}
#[must_use]
pub fn offset(mut self, value: Vec3) -> Self {
self.offset = value;
self
}
#[must_use]
pub fn scale(mut self, value: Vec3) -> Self {
self.scale = value;
self
}
#[must_use]
pub fn block_size(mut self, value: u32) -> Self {
self.block_size = value;
self
}
#[must_use]
pub fn bits_per_sample(mut self, value: u32) -> Self {
self.bits_per_sample = value;
self
}
#[must_use]
pub fn active_edge_cos_threshold_angle(mut self, value: f32) -> Self {
self.active_edge_cos_threshold_angle = value;
self
}
fn validate_layout(&self, sample_count: u32) -> Result<u64, ShapeError> {
let invalid = |what| Err(ShapeError::InvalidValue(what));
if !(2..=8).contains(&self.block_size) {
return invalid("block_size must be between 2 and 8");
}
if !(1..=16).contains(&self.bits_per_sample) {
return invalid("bits_per_sample must be between 1 and 16");
}
let threshold = self.active_edge_cos_threshold_angle;
if !(threshold.is_finite() && (0.0..=1.0).contains(&threshold)) {
return invalid("active_edge_cos_threshold_angle must be between 0 and 1");
}
let padded = padded_sample_count(sample_count, self.block_size);
if padded / u64::from(self.block_size) < 2 {
return invalid("sample_count must be larger than block_size");
}
if padded > 32768 {
return invalid("sample_count is too large");
}
Ok(padded)
}
fn validate_extents(&self, padded: u64, heights: Option<(f32, f32)>) -> Result<(), ShapeError> {
if !self.offset.is_finite() {
return Err(ShapeError::InvalidValue(
"height field offset must be finite",
));
}
let scale = [self.scale.x, self.scale.y, self.scale.z];
if !scale.into_iter().all(is_finite_positive) {
return Err(ShapeError::InvalidValue(
"height field scale must be finite and positive",
));
}
let last = (padded - 1) as f32;
let far_x = self.offset.x + self.scale.x * last;
let far_z = self.offset.z + self.scale.z * last;
if !(far_x.is_finite() && far_z.is_finite()) {
return Err(ShapeError::InvalidValue(
"height field extent along x or z must be finite",
));
}
if let Some((min, max)) = heights {
let (offset, scale) = (self.offset.y, self.scale.y);
if !((offset + scale * min).is_finite() && (offset + scale * max).is_finite()) {
return Err(ShapeError::InvalidValue(
"height field extent along y must be finite",
));
}
}
Ok(())
}
}
const BEYOND_EXTENT: &str = "shape extends beyond limits::MAX_SHAPE_EXTENT";
fn within_extent(dimension: f32) -> bool {
dimension <= limits::MAX_SHAPE_EXTENT
}
fn padded_sample_count(sample_count: u32, block_size: u32) -> u64 {
u64::from(sample_count).div_ceil(u64::from(block_size)) * u64::from(block_size)
}
fn height_range(samples: &[f32]) -> Option<(f32, f32)> {
samples
.iter()
.filter(|&&sample| sample != f32::MAX)
.fold(None, |range, &sample| match range {
None => Some((sample, sample)),
Some((min, max)) => Some((sample.min(min), sample.max(max))),
})
}
pub(crate) fn validate_box(half_extent: Vec3, convex_radius: f32) -> Result<(), ShapeError> {
let components = [half_extent.x, half_extent.y, half_extent.z];
if !components.into_iter().all(is_finite_positive) {
return Err(ShapeError::InvalidValue(
"box half extents must be finite and positive",
));
}
if !components.into_iter().all(within_extent) {
return Err(ShapeError::InvalidValue(BEYOND_EXTENT));
}
validate_convex_radius(convex_radius)
}
fn validate_convex_radius(convex_radius: f32) -> Result<(), ShapeError> {
if is_finite_non_negative(convex_radius) {
Ok(())
} else {
Err(ShapeError::InvalidValue(
"convex radius must be finite and not negative",
))
}
}
pub(crate) fn validate_sphere(radius: f32) -> Result<(), ShapeError> {
if radius > limits::MAX_SHAPE_EXTENT {
return Err(ShapeError::InvalidValue(BEYOND_EXTENT));
}
validate_positive_sphere(radius)
}
fn validate_positive_sphere(radius: f32) -> Result<(), ShapeError> {
if is_finite_positive(radius) {
Ok(())
} else {
Err(ShapeError::InvalidValue(
"sphere radius must be finite and positive",
))
}
}
pub(crate) fn validate_capsule(
half_height_of_cylinder: f32,
radius: f32,
) -> Result<(), ShapeError> {
if half_height_of_cylinder + radius > limits::MAX_SHAPE_EXTENT {
return Err(ShapeError::InvalidValue(BEYOND_EXTENT));
}
validate_positive_capsule(half_height_of_cylinder, radius)
}
fn validate_positive_capsule(half_height_of_cylinder: f32, radius: f32) -> Result<(), ShapeError> {
if is_finite_positive(half_height_of_cylinder) && is_finite_positive(radius) {
Ok(())
} else {
Err(ShapeError::InvalidValue(
"capsule half height and radius must be finite and positive",
))
}
}
pub(crate) fn validate_cylinder(
half_height: f32,
radius: f32,
convex_radius: f32,
) -> Result<(), ShapeError> {
if !(is_finite_positive(half_height) && is_finite_positive(radius)) {
return Err(ShapeError::InvalidValue(
"cylinder half height and radius must be finite and positive",
));
}
if !(within_extent(half_height) && within_extent(radius)) {
return Err(ShapeError::InvalidValue(BEYOND_EXTENT));
}
validate_convex_radius(convex_radius)
}
pub(crate) fn validate_height_field(
sample_count: u32,
samples: &[f32],
settings: &HeightFieldSettings,
) -> Result<(), ShapeError> {
if sample_count < 2 {
return Err(ShapeError::InvalidValue("sample_count must be at least 2"));
}
let padded = settings.validate_layout(sample_count)?;
let count = sample_count as usize;
if count.checked_mul(count) != Some(samples.len()) {
return Err(ShapeError::InvalidValue(
"samples must hold sample_count^2 values",
));
}
if !samples.iter().all(|sample| sample.is_finite()) {
return Err(ShapeError::InvalidValue("height samples must be finite"));
}
let heights = height_range(samples);
if heights.is_some_and(|(min, max)| !(max - min).is_finite()) {
return Err(ShapeError::InvalidValue(
"height sample range must be finite",
));
}
settings.validate_extents(padded, heights)
}
impl Shape {
pub fn new_box(half_extent: Vec3) -> Result<Self, ShapeError> {
Self::new_box_with_convex_radius(half_extent, JPH_DEFAULT_CONVEX_RADIUS as f32)
}
pub fn new_box_with_convex_radius(
half_extent: Vec3,
convex_radius: f32,
) -> Result<Self, ShapeError> {
validate_box(half_extent, convex_radius)?;
initialize()?;
let half_extent = half_extent.to_jph();
unsafe { Self::from_raw(JPH_BoxShape_Create(&half_extent, convex_radius).cast()) }
}
pub fn new_sphere(radius: f32) -> Result<Self, ShapeError> {
validate_sphere(radius)?;
Self::sphere(radius)
}
fn sphere(radius: f32) -> Result<Self, ShapeError> {
validate_positive_sphere(radius)?;
initialize()?;
unsafe { Self::from_raw(JPH_SphereShape_Create(radius).cast()) }
}
pub fn new_cylinder(half_height: f32, radius: f32) -> Result<Self, ShapeError> {
Self::new_cylinder_with_convex_radius(half_height, radius, JPH_DEFAULT_CONVEX_RADIUS as f32)
}
pub fn new_cylinder_with_convex_radius(
half_height: f32,
radius: f32,
convex_radius: f32,
) -> Result<Self, ShapeError> {
validate_cylinder(half_height, radius, convex_radius)?;
initialize()?;
let settings = unsafe {
ShapeSettings::from_raw(
JPH_CylinderShapeSettings_Create(half_height, radius, convex_radius).cast(),
)
}?;
settings.create()
}
pub fn new_capsule(half_height_of_cylinder: f32, radius: f32) -> Result<Self, ShapeError> {
validate_capsule(half_height_of_cylinder, radius)?;
Self::capsule(half_height_of_cylinder, radius)
}
fn capsule(half_height_of_cylinder: f32, radius: f32) -> Result<Self, ShapeError> {
validate_positive_capsule(half_height_of_cylinder, radius)?;
initialize()?;
unsafe { Self::from_raw(JPH_CapsuleShape_Create(half_height_of_cylinder, radius).cast()) }
}
pub fn new_height_field(
sample_count: u32,
samples: &[f32],
settings: &HeightFieldSettings<'_>,
) -> Result<Self, ShapeError> {
validate_height_field(sample_count, samples, settings)?;
let Some((list, indices)) = settings.materials else {
return unsafe { Self::height_field(sample_count, samples, settings, None) };
};
let list = height_field_materials(sample_count, list, indices)?;
unsafe { Self::height_field(sample_count, samples, settings, Some((indices, &list))) }
}
pub(crate) unsafe fn height_field(
sample_count: u32,
samples: &[f32],
settings: &HeightFieldSettings,
materials: Option<HeightFieldMaterials<'_>>,
) -> Result<Self, ShapeError> {
initialize()?;
let jolt_settings =
unsafe { height_field_settings(sample_count, samples, settings, materials) }?;
jolt_settings.create()?.within_extent_bounds()
}
pub fn height_field_position(&self, x: u32, y: u32) -> Option<Vec3> {
if self.sub_type() != JPH_ShapeSubType_HeightField {
return None;
}
let shape: *const JPH_HeightFieldShape = self.as_ptr().cast();
let sample_count = unsafe { JPH_HeightFieldShape_GetSampleCount(shape) };
if x >= sample_count || y >= sample_count {
return None;
}
if unsafe { JPH_HeightFieldShape_IsNoCollision(shape, x, y) } {
return None;
}
let mut position = Vec3::ZERO.to_jph();
unsafe { JPH_HeightFieldShape_GetPosition(shape, x, y, &mut position) };
Some(Vec3::from_jph(position))
}
pub fn new_compound(children: &[CompoundChild<'_>]) -> Result<Self, ShapeError> {
if children.is_empty() {
return Err(ShapeError::InvalidValue(compound::EMPTY_COMPOUND_RULE));
}
if u32::try_from(children.len()).is_err() {
return Err(ShapeError::InvalidValue(
"a compound has at most u32::MAX children",
));
}
for child in children {
check_child_pose(child.position, child.rotation)?;
}
let mut memo = BTreeMap::new();
let raw: Vec<_> = children
.iter()
.map(|child| RawCompoundChild {
shape: child.shape.as_ptr(),
position: child.position,
rotation: child.rotation,
user_data: child.user_data,
ids: unsafe { sub_shape_ids(child.shape.as_ptr(), &mut memo) },
})
.collect();
unsafe { build_compound(&raw) }
}
pub fn new_offset_center_of_mass(shape: &Shape, offset: Vec3) -> Result<Self, ShapeError> {
if !limits::is_local_offset(offset) {
return Err(ShapeError::InvalidValue(
"centre of mass offset must be finite and within limits::MAX_SHAPE_EXTENT",
));
}
initialize()?;
let offset = offset.to_jph();
unsafe {
Self::from_raw(JPH_OffsetCenterOfMassShape_Create(&offset, shape.as_ptr()).cast())
}?
.within_extent_bounds()
}
pub(crate) fn within_extent_bounds(self) -> Result<Self, ShapeError> {
let mut bounds = JPH_AABox {
min: Vec3::ZERO.to_jph(),
max: Vec3::ZERO.to_jph(),
};
unsafe { JPH_Shape_GetLocalBounds(self.as_ptr(), &mut bounds) };
let (min, max) = (Vec3::from_jph(bounds.min), Vec3::from_jph(bounds.max));
let empty = min.x > max.x || min.y > max.y || min.z > max.z;
if empty || (limits::is_local_offset(min) && limits::is_local_offset(max)) {
Ok(self)
} else {
Err(ShapeError::InvalidValue(BEYOND_EXTENT))
}
}
pub fn compound_sub_shape(&self, id: SubShapeId) -> Option<CompoundSubShape> {
unsafe { compound_sub_shape_of(self.0.as_non_null(), id) }
}
unsafe fn from_raw(ptr: *mut JPH_Shape) -> Result<Self, ShapeError> {
unsafe { Owned::from_raw(ptr) }
.map(Self)
.ok_or(ShapeError::AllocationFailed)
}
pub(crate) fn as_ptr(&self) -> *const JPH_Shape {
self.0.as_ptr()
}
pub(crate) fn sub_type(&self) -> JPH_ShapeSubType {
unsafe { JPH_Shape_GetSubType(self.as_ptr()) }
}
pub(crate) fn must_be_static(&self) -> bool {
unsafe { JPH_Shape_MustBeStatic(self.as_ptr()) }
}
pub(crate) fn center_of_mass(&self) -> Vec3 {
let mut center = Vec3::ZERO.to_jph();
unsafe { JPH_Shape_GetCenterOfMass(self.as_ptr(), &mut center) };
Vec3::from_jph(center)
}
pub(crate) fn inflated(&self, by: f32) -> Result<Option<Shape>, ShapeError> {
let sub_type = self.sub_type();
if sub_type == JPH_ShapeSubType_Sphere {
let radius = unsafe { JPH_SphereShape_GetRadius(self.as_ptr().cast()) };
Shape::sphere(radius + by).map(Some)
} else if sub_type == JPH_ShapeSubType_Capsule {
let capsule: *const JPH_CapsuleShape = self.as_ptr().cast();
let (half_height, radius) = unsafe {
(
JPH_CapsuleShape_GetHalfHeightOfCylinder(capsule),
JPH_CapsuleShape_GetRadius(capsule),
)
};
Shape::capsule(half_height, radius + by).map(Some)
} else {
Ok(None)
}
}
}
impl JoltObject for JPH_Shape {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_Shape_Destroy(ptr) };
}
}
#[cfg(test)]
mod tests;