use std::ffi::c_void;
use std::fmt;
use std::mem::MaybeUninit;
use std::ptr::null;
use oxijolt_sys::*;
use super::{offset_from, rotation_translation, validate_pose, ResultSlot};
use crate::filter::with_query_filters;
use crate::limits;
use crate::math::is_finite_non_negative;
use crate::{
BodyId, CompoundSubShape, ObjectLayer, PhysicsWorld, Quat, QueryError, QueryFilter, RVec3,
Shape, SubShapeId, Vec3,
};
#[derive(Clone, Copy)]
pub struct ShapeCast<'a> {
shape: &'a Shape,
position: RVec3,
rotation: Quat,
direction: Vec3,
target_distance: f32,
return_deepest_point: bool,
back_faces: bool,
}
impl fmt::Debug for ShapeCast<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ShapeCast")
.field("position", &self.position)
.field("rotation", &self.rotation)
.field("direction", &self.direction)
.field("target_distance", &self.target_distance)
.field("return_deepest_point", &self.return_deepest_point)
.field("back_faces", &self.back_faces)
.finish_non_exhaustive()
}
}
impl<'a> ShapeCast<'a> {
pub const MAX_TARGET_DISTANCE: f32 = 1000.0;
pub fn new(shape: &'a Shape, position: RVec3, rotation: Quat, direction: Vec3) -> Self {
Self {
shape,
position,
rotation,
direction,
target_distance: 0.0,
return_deepest_point: false,
back_faces: false,
}
}
#[must_use]
pub fn target_distance(mut self, metres: f32) -> Self {
self.target_distance = metres;
self
}
#[must_use]
pub fn return_deepest_point(mut self, value: bool) -> Self {
self.return_deepest_point = value;
self
}
#[must_use]
pub fn collide_with_back_faces(mut self, value: bool) -> Self {
self.back_faces = value;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct ShapeCastHit {
pub body: BodyId,
pub sub_shape_id: SubShapeId,
pub object_layer: ObjectLayer,
pub compound_child: Option<CompoundSubShape>,
pub fraction: f32,
pub distance: f32,
pub point: RVec3,
pub normal: Vec3,
pub penetration_depth: f32,
}
unsafe extern "C" fn store_shape_cast_hit(
user_data: *mut c_void,
result: *const JPH_ShapeCastResult,
) {
unsafe { ResultSlot::<JPH_ShapeCastResult>::store(user_data, result) }
}
fn default_cast_settings() -> JPH_ShapeCastSettings {
let mut settings = MaybeUninit::<JPH_ShapeCastSettings>::uninit();
unsafe {
JPH_ShapeCastSettings_Init(settings.as_mut_ptr());
settings.assume_init()
}
}
impl PhysicsWorld {
pub fn cast_shape(
&self,
cast: &ShapeCast<'_>,
filter: &QueryFilter<'_>,
) -> Result<Option<ShapeCastHit>, QueryError> {
validate_pose(cast.shape, cast.position, cast.rotation)?;
if !(limits::is_frame_span(cast.direction) && cast.direction != Vec3::ZERO) {
return Err(QueryError::InvalidValue(
"cast direction must be finite, not zero and at most 2 * limits::MAX_POSITION per axis",
));
}
let target_distance = cast.target_distance;
if !is_finite_non_negative(target_distance) {
return Err(QueryError::InvalidValue(
"target distance must be finite and not negative",
));
}
if target_distance > ShapeCast::MAX_TARGET_DISTANCE {
return Err(QueryError::InvalidValue(
"target distance must be at most ShapeCast::MAX_TARGET_DISTANCE",
));
}
let inflated = if target_distance > 0.0 {
let inflated = cast
.shape
.inflated(target_distance)
.map_err(|_| QueryError::InvalidValue("target distance is too large"))?;
Some(inflated.ok_or(QueryError::InvalidValue(
"target distance needs a sphere or capsule query shape",
))?)
} else {
None
};
filter.validate(self)?;
let query_shape = inflated.as_ref().unwrap_or(cast.shape);
let mut settings = default_cast_settings();
settings.returnDeepestPoint = cast.return_deepest_point;
let back_face_mode = if cast.back_faces {
JPH_BackFaceMode_CollideWithBackFaces
} else {
JPH_BackFaceMode_IgnoreBackFaces
};
settings.backFaceModeTriangles = back_face_mode;
settings.backFaceModeConvex = back_face_mode;
let transform = rotation_translation(cast.rotation, cast.position);
let direction = cast.direction.to_jph();
let mut base_offset = cast.position.to_jph();
let hit = with_query_filters(self, filter, |raw, state| {
let slot = ResultSlot::<JPH_ShapeCastResult>::new(state);
unsafe {
JPH_NarrowPhaseQuery_CastShape2(
self.narrow_phase_query.as_ptr(),
query_shape.as_ptr(),
&transform,
&direction,
&settings,
&mut base_offset,
JPH_CollisionCollectorType_ClosestHit,
Some(store_shape_cast_hit),
slot.as_user_data(),
null(),
raw.object_layer,
raw.body,
raw.shape,
)
};
slot.hit.get()
})?;
let Some(hit) = hit else {
return Ok(None);
};
let body = BodyId::new(hit.bodyID2, self.tag);
let sub_shape_id = SubShapeId::new(hit.subShapeID2);
Ok(Some(ShapeCastHit {
body,
sub_shape_id,
object_layer: self.object_layer_of(body),
compound_child: self.compound_sub_shape(body, sub_shape_id).ok().flatten(),
fraction: hit.fraction,
distance: hit.fraction * cast.direction.length(),
point: offset_from(cast.position, hit.contactPointOn2),
normal: Vec3::from_jph(hit.penetrationAxis)
.normalized_or_zero()
.scale(-1.0),
penetration_depth: hit.penetrationDepth - target_distance,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn joltc_cast_settings_init_gives_jolts_defaults() {
let settings = default_cast_settings();
assert_eq!(settings.base.collectFacesMode, JPH_CollectFacesMode_NoFaces);
assert_eq!(
settings.base.activeEdgeMode,
JPH_ActiveEdgeMode_CollideOnlyWithActive
);
assert_eq!(settings.base.collisionTolerance, 1.0e-4);
assert_eq!(settings.base.penetrationTolerance, 1.0e-4);
assert_eq!(
settings.backFaceModeTriangles,
JPH_BackFaceMode_IgnoreBackFaces
);
assert_eq!(
settings.backFaceModeConvex,
JPH_BackFaceMode_IgnoreBackFaces
);
assert!(!settings.useShrunkenShapeAndConvexRadius);
assert!(!settings.returnDeepestPoint);
}
}