use oxijolt_sys::*;
use super::geometry::{cross, dot, length_sq, sub, v3};
use super::{initialize, validate_convex_radius, Shape, ShapeSettings};
use crate::{limits, ConvexHullError, PhysicsMaterial, ShapeError, Vec3};
const MIN_TRIANGLE_AREA_SQ: f64 = 1.0e-12;
const HULL_TOLERANCE: f64 = 1.0e-3;
const MIN_NEEDLE_LEVER: f64 = 0.25;
const MIN_SLAB_THICKNESS: f64 = 200.0;
impl Shape {
pub fn new_convex_hull(points: &[Vec3]) -> Result<Self, ShapeError> {
Self::new_convex_hull_with_convex_radius(points, JPH_DEFAULT_CONVEX_RADIUS as f32)
}
pub fn new_convex_hull_with_convex_radius(
points: &[Vec3],
convex_radius: f32,
) -> Result<Self, ShapeError> {
Self::convex_hull(points, convex_radius, None)
}
pub fn new_convex_hull_with_material(
points: &[Vec3],
convex_radius: f32,
material: &PhysicsMaterial,
) -> Result<Self, ShapeError> {
Self::convex_hull(points, convex_radius, Some(material))
}
fn convex_hull(
points: &[Vec3],
convex_radius: f32,
material: Option<&PhysicsMaterial>,
) -> Result<Self, ShapeError> {
validate_hull_points(points)?;
validate_convex_radius(convex_radius)?;
InitialSimplex::of(points).classify()?;
initialize()?;
let jolt_points: Vec<JPH_Vec3> = points.iter().map(|point| point.to_jph()).collect();
let settings = unsafe {
ShapeSettings::from_raw(
JPH_ConvexHullShapeSettings_Create(
jolt_points.as_ptr(),
jolt_points.len() as u32,
convex_radius,
)
.cast(),
)
}?;
if let Some(material) = material {
unsafe { Self::attach_material(&settings, material) };
}
let shape = settings.create()?;
if shape.is_flat_hull() {
return Err(ShapeError::ConvexHull(ConvexHullError::Coplanar));
}
shape.within_extent_bounds()
}
fn is_flat_hull(&self) -> bool {
if self.sub_type() != JPH_ShapeSubType_ConvexHull {
return false;
}
unsafe { JPH_ConvexHullShape_GetNumFaces(self.as_ptr().cast()) == 2 }
}
}
fn validate_hull_points(points: &[Vec3]) -> Result<(), ShapeError> {
if points.len() < 4 {
return Err(ShapeError::ConvexHull(ConvexHullError::TooFewPoints));
}
if points.len() > i32::MAX as usize {
return Err(ShapeError::InvalidValue(
"a convex hull has at most i32::MAX points",
));
}
if !points.iter().all(|&point| limits::is_local_offset(point)) {
return Err(ShapeError::InvalidValue(
"convex hull points must be finite and within limits::MAX_SHAPE_EXTENT",
));
}
Ok(())
}
struct InitialSimplex {
area_sq: f64,
length: f64,
width: f64,
thickness: f64,
coplanar_distance: f64,
}
impl InitialSimplex {
fn of(points: &[Vec3]) -> Self {
let points: Vec<[f64; 3]> = points.iter().map(|&point| v3(point)).collect();
let first = farthest(&points, None, length_sq);
let second = farthest(&points, Some(first), |p| length_sq(sub(p, points[first])));
let (a, b) = (points[first], points[second]);
let mut third = (first, -1.0);
for (index, &p) in points.iter().enumerate() {
let area_sq = length_sq(cross(sub(a, p), sub(b, p)));
if index != first && index != second && area_sq > third.1 {
third = (index, area_sq);
}
}
let (area_sq, c) = (third.1, points[third.0]);
let length = length_sq(sub(b, a)).sqrt();
let normal = cross(sub(b, a), sub(c, a));
let normal_length = length_sq(normal).sqrt();
let centroid = [0, 1, 2].map(|i| (a[i] + b[i] + c[i]) / 3.0);
let thickness = points
.iter()
.map(|&p| (dot(sub(p, centroid), normal) / normal_length).abs())
.fold(0.0, f64::max);
let largest = [0, 1, 2].map(|i| points.iter().map(|p| p[i].abs()).fold(0.0, f64::max));
Self {
area_sq,
length,
width: area_sq.sqrt() / length,
thickness,
coplanar_distance: 3.0
* f64::from(f32::EPSILON)
* (largest[0] + largest[1] + largest[2]),
}
}
fn classify(&self) -> Result<(), ShapeError> {
if self.area_sq < MIN_TRIANGLE_AREA_SQ {
return Err(ShapeError::ConvexHull(ConvexHullError::Degenerate));
}
let tolerance = HULL_TOLERANCE.max(self.coplanar_distance);
if self.width * tolerance < MIN_NEEDLE_LEVER * self.length * self.coplanar_distance {
return Err(ShapeError::ConvexHull(ConvexHullError::Degenerate));
}
if self.thickness < MIN_SLAB_THICKNESS * self.coplanar_distance {
return Err(ShapeError::ConvexHull(ConvexHullError::Coplanar));
}
Ok(())
}
}
fn farthest(points: &[[f64; 3]], skip: Option<usize>, measure: impl Fn([f64; 3]) -> f64) -> usize {
let mut best = (0, -1.0);
for (index, &point) in points.iter().enumerate() {
let value = measure(point);
if Some(index) != skip && value > best.1 {
best = (index, value);
}
}
best.0
}
#[cfg(test)]
mod tests;