use crate::prelude::*;
const DOT_EPSILON: Scalar = 0.005;
#[must_use]
pub fn project_velocity_bruteforce(v: Vector, normals: &[Dir]) -> Vector {
if normals.is_empty() {
return v;
}
if normals
.iter()
.all(|normal| normal.adjust_precision().dot(v) >= -DOT_EPSILON)
{
return v;
}
let mut best_projection = Vector::ZERO;
let mut best_distance_sq = Scalar::INFINITY;
let is_valid = |projection: Vector| {
normals
.iter()
.all(|n| projection.dot(n.adjust_precision()) >= -DOT_EPSILON)
};
for n in normals {
let n = n.adjust_precision();
let n_dot_v = n.dot(v);
if n_dot_v < -DOT_EPSILON {
let projection = v - n_dot_v * n;
let distance_sq = v.distance_squared(projection);
if distance_sq < best_distance_sq && is_valid(projection) {
best_distance_sq = distance_sq;
best_projection = projection;
}
}
}
#[cfg(feature = "3d")]
{
let n = normals.len();
for i in 0..n {
let ni = normals[i].adjust_precision();
for nj in normals
.iter()
.take(n)
.skip(i + 1)
.map(|n| n.adjust_precision())
{
let e = ni.cross(nj);
let e_length_sq = e.length_squared();
if e_length_sq < DOT_EPSILON {
continue;
}
let projection = e * (v.dot(e) / e_length_sq);
let distance_sq = v.distance_squared(projection);
if distance_sq < best_distance_sq && is_valid(projection) {
best_distance_sq = distance_sq;
best_projection = projection;
}
}
}
}
if best_distance_sq.is_infinite() {
Vector::ZERO
} else {
best_projection
}
}
#[must_use]
pub fn project_velocity(v: Vector, normals: &[Dir]) -> Vector {
-project_onto_conical_hull(-v, normals)
}
#[cfg_attr(
feature = "3d",
doc = " - Tetrahedron: a \"solid wedge\" spanning the volume between three rays"
)]
#[derive(Debug)]
enum SimplicialCone {
Origin,
Ray(#[cfg(feature = "3d")] Dir),
#[cfg(feature = "3d")]
Wedge(Dir, Dir),
}
#[must_use]
fn project_onto_conical_hull(x0: Vector, normals: &[Dir]) -> Vector {
let mut maybe_cone = Some(SimplicialCone::Origin);
let mut search_vector = x0;
let mut n_iters = 0;
while let Some(cone) = maybe_cone {
if search_vector.length_squared() < DOT_EPSILON * DOT_EPSILON {
break;
}
let n_dots = normals
.iter()
.map(|n| n.adjust_precision().dot(search_vector));
let Some((best_idx, best_dot)) = n_dots
.enumerate()
.max_by(|(_, dot1), (_, dot2)| Scalar::total_cmp(dot1, dot2))
else {
break;
};
if best_dot <= DOT_EPSILON {
break;
}
let best_normal = normals[best_idx];
(maybe_cone, search_vector) = cone.project_point(x0, best_normal);
n_iters += 1;
if n_iters >= 10 {
break;
}
}
search_vector
}
impl SimplicialCone {
fn project_point(self, x0: Vector, new_direction: Dir) -> (Option<SimplicialCone>, Vector) {
let new_direction_vec = new_direction.adjust_precision();
match self {
SimplicialCone::Origin => {
let dot = new_direction_vec.dot(x0);
#[cfg(feature = "2d")]
let ray = Self::Ray();
#[cfg(feature = "3d")]
let ray = Self::Ray(new_direction);
(Some(ray), x0 - dot * new_direction_vec)
}
#[cfg(feature = "2d")]
SimplicialCone::Ray() => (None, Vector::ZERO),
#[cfg(feature = "3d")]
SimplicialCone::Ray(previous_direction) => {
let cross = new_direction_vec.cross(previous_direction.adjust_precision());
let dot = x0.dot(cross);
let new_search_vector = dot * cross / cross.length_squared();
let new_cone = if dot > 0.0 {
Self::Wedge(new_direction, previous_direction)
} else {
Self::Wedge(previous_direction, new_direction)
};
(Some(new_cone), new_search_vector)
}
#[cfg(feature = "3d")]
SimplicialCone::Wedge(n1, n2) => {
let cross1 = n1.adjust_precision().cross(new_direction_vec);
let cross1_sq = cross1.length_squared();
let dot1 = x0.dot(cross1);
let inside1 = dot1 <= 0.0;
let cross2 = new_direction_vec.cross(n2.adjust_precision());
let cross2_sq = cross2.length_squared();
let dot2 = x0.dot(cross2);
let inside2 = dot2 <= 0.0;
if inside1 & inside2 {
(None, Vector::ZERO)
} else if dot1 * dot1.abs() * cross2_sq > dot2 * dot2.abs() * cross1_sq {
(
Some(Self::Wedge(n1, new_direction)),
dot1 * cross1 / cross1_sq,
)
} else {
(
Some(Self::Wedge(new_direction, n2)),
dot2 * cross2 / cross2_sq,
)
}
}
}
}
}
#[cfg(test)]
pub mod test {
use super::DOT_EPSILON;
use crate::prelude::*;
#[test]
fn check_agreement() {
#[cfg(feature = "2d")]
let normals = &[
Dir::Y,
Dir::from_xy(2.0, 1.0).unwrap(),
Dir::from_xy(-2.0, 1.0).unwrap(),
Dir::from_xy(1.0, 1.0).unwrap(),
Dir::from_xy(-1.0, 1.0).unwrap(),
];
#[cfg(feature = "3d")]
let normals = &[
Dir::Z,
Dir::from_xyz(2.0, 0.0, 1.0).unwrap(),
Dir::from_xyz(-2.0, 0.0, 1.0).unwrap(),
Dir::from_xyz(0.0, 2.0, 1.0).unwrap(),
Dir::from_xyz(0.0, -2.0, 1.0).unwrap(),
Dir::from_xyz(1.5, 1.5, 1.0).unwrap(),
Dir::from_xyz(1.5, -1.5, 1.0).unwrap(),
Dir::from_xyz(-1.5, 1.5, 1.0).unwrap(),
Dir::from_xyz(-1.5, -1.5, 1.0).unwrap(),
Dir::from_xyz(1.0, 1.75, 1.0).unwrap(),
Dir::from_xyz(1.0, -1.75, 1.0).unwrap(),
Dir::from_xyz(-1.0, 1.75, 1.0).unwrap(),
Dir::from_xyz(-1.0, -1.75, 1.0).unwrap(),
Dir::from_xyz(1.75, 1.0, 1.0).unwrap(),
Dir::from_xyz(1.75, -1.0, 1.0).unwrap(),
Dir::from_xyz(-1.75, 1.0, 1.0).unwrap(),
Dir::from_xyz(-1.75, -1.0, 1.0).unwrap(),
];
for n in 1..=normals.len() {
let selected_normals = &normals[..n];
let velocities = QuasiRandomDirection::default();
let mut worst_result = (Scalar::NEG_INFINITY, Vector::ZERO);
for vel in velocities.take(1000) {
let new_result = super::project_velocity(vel, selected_normals);
for (k, normal) in selected_normals.iter().enumerate() {
let intrusion = -new_result.dot(normal.adjust_precision());
assert!(
intrusion <= DOT_EPSILON,
"velocity still points into constraint plane after projection: \
input {vel} against {n} normals has intrusion \
{intrusion} against normal {k}"
);
}
let old_result = super::project_velocity_bruteforce(vel, selected_normals);
let amount_worse = (new_result - vel).length() - (old_result - vel).length();
assert!(
amount_worse <= DOT_EPSILON,
"velocity projection result was {amount_worse} > {DOT_EPSILON} worse than \
result from brute-force method when projecting input {vel} against {n} normals"
);
if amount_worse >= worst_result.0 {
worst_result = (amount_worse, vel);
}
}
let (worst_error, bad_input) = worst_result;
eprintln!(
"For {n} normals, worst case new method is {worst_error} worse than brute-force for input {bad_input}",
);
}
}
#[derive(Default)]
pub struct QuasiRandomDirection {
#[cfg(feature = "3d")]
i: Scalar,
j: Scalar,
}
#[cfg(feature = "3d")]
impl QuasiRandomDirection {
#[allow(clippy::excessive_precision)]
const PLASTIC: Scalar = 1.32471795724475;
const INV_PLASTIC: Scalar = 1.0 / Self::PLASTIC;
const INV_PLASTIC_SQ: Scalar = Self::INV_PLASTIC * Self::INV_PLASTIC;
}
#[cfg(feature = "2d")]
impl QuasiRandomDirection {
#[allow(clippy::excessive_precision)]
const GOLDEN: Scalar = 1.61803398875;
const INV_GOLDEN: Scalar = 1.0 / Self::GOLDEN;
}
impl QuasiRandomDirection {
pub fn reset(&mut self) {
*self = Self::default();
}
}
impl Iterator for QuasiRandomDirection {
type Item = Vector;
fn next(&mut self) -> Option<Self::Item> {
let phi = 2.0 * PI * self.j;
let x = phi.cos();
let y = phi.sin();
#[cfg(feature = "3d")]
{
let z = 2.0 * self.i - 1.0;
let rho = (1.0 - z * z).sqrt();
self.i = (self.i + Self::INV_PLASTIC) % 1.0;
self.j = (self.j + Self::INV_PLASTIC_SQ) % 1.0;
Some(Vector::new(rho * x, rho * y, z))
}
#[cfg(feature = "2d")]
{
self.j = (self.j + Self::INV_GOLDEN) % 1.0;
Some(Vector::new(x, y))
}
}
}
}