use phyz_math::Vec3;
pub type ConeVec = Vec3;
pub fn project_cone(f: ConeVec, mu: f64) -> ConeVec {
let fn_ = f.x;
let ft = Vec3::new(0.0, f.y, f.z);
let ft_norm = (f.y * f.y + f.z * f.z).sqrt();
if mu <= 0.0 {
return Vec3::new(fn_.max(0.0), 0.0, 0.0);
}
if ft_norm <= mu * fn_ {
return f;
}
if ft_norm <= -fn_ / mu {
return Vec3::zeros();
}
let scale = (mu * ft_norm + fn_) / (mu * mu + 1.0);
let t_scale = if ft_norm > 0.0 {
mu * scale / ft_norm
} else {
0.0
};
Vec3::new(scale, ft.y * t_scale, ft.z * t_scale)
}
pub fn in_cone(f: ConeVec, mu: f64, tol: f64) -> bool {
let ft = (f.y * f.y + f.z * f.z).sqrt();
f.x >= -tol && ft <= mu * f.x + tol
}
pub fn in_cone_interior(f: ConeVec, mu: f64, tol: f64) -> bool {
let ft = (f.y * f.y + f.z * f.z).sqrt();
f.x > tol && ft < mu * f.x - tol
}
pub fn contact_frame(normal: &Vec3) -> (Vec3, Vec3, Vec3) {
let n = normal.normalize();
let a = if n.x.abs() <= n.y.abs() && n.x.abs() <= n.z.abs() {
Vec3::x()
} else if n.y.abs() <= n.z.abs() {
Vec3::y()
} else {
Vec3::z()
};
let u = n.cross(a).normalize();
let w = n.cross(u);
(n, u, w)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interior_point_is_unchanged() {
let f = Vec3::new(1.0, 0.1, 0.1);
let p = project_cone(f, 0.5);
assert!((p - f).norm() < 1e-12);
}
#[test]
fn polar_cone_projects_to_apex() {
let f = Vec3::new(-1.0, 0.01, 0.0);
let p = project_cone(f, 0.5);
assert!(p.norm() < 1e-12, "expected apex, got {p:?}");
}
#[test]
fn exterior_point_lands_on_the_boundary() {
let mu = 0.5;
let f = Vec3::new(1.0, 3.0, 0.0);
let p = project_cone(f, mu);
let ft = (p.y * p.y + p.z * p.z).sqrt();
assert!(
(ft - mu * p.x).abs() < 1e-12,
"not on boundary: ft={ft}, mu*fn={}",
mu * p.x
);
let p2 = project_cone(p, mu);
assert!((p2 - p).norm() < 1e-12);
}
#[test]
fn projection_is_nearest_point() {
let mu = 0.7;
let f = Vec3::new(0.3, 1.4, -0.9);
let p = project_cone(f, mu);
let d = (p - f).norm();
for i in 0..60 {
for j in 0..60 {
let fn_ = i as f64 * 0.05;
let theta = j as f64 * std::f64::consts::TAU / 60.0;
let r = mu * fn_;
let q = Vec3::new(fn_, r * theta.cos(), r * theta.sin());
assert!(
(q - f).norm() >= d - 1e-9,
"found closer feasible point {q:?}"
);
}
}
}
#[test]
fn zero_friction_clamps_normal_only() {
let p = project_cone(Vec3::new(2.0, 5.0, -5.0), 0.0);
assert!((p - Vec3::new(2.0, 0.0, 0.0)).norm() < 1e-12);
let p = project_cone(Vec3::new(-2.0, 5.0, -5.0), 0.0);
assert!(p.norm() < 1e-12);
}
#[test]
fn cone_is_isotropic_in_the_tangent_plane() {
let mu = 0.6;
let magnitude = 2.0;
let reference = project_cone(Vec3::new(0.5, magnitude, 0.0), mu);
let ref_ft = (reference.y.powi(2) + reference.z.powi(2)).sqrt();
for k in 1..16 {
let theta = k as f64 * std::f64::consts::TAU / 16.0;
let f = Vec3::new(0.5, magnitude * theta.cos(), magnitude * theta.sin());
let p = project_cone(f, mu);
let ft = (p.y.powi(2) + p.z.powi(2)).sqrt();
assert!(
(ft - ref_ft).abs() < 1e-12,
"anisotropy at {theta}: {ft} vs {ref_ft}"
);
assert!((p.x - reference.x).abs() < 1e-12);
}
}
#[test]
fn contact_frame_is_orthonormal() {
for n in [
Vec3::z(),
Vec3::x(),
Vec3::new(1.0, 1.0, 1.0),
Vec3::new(-0.3, 0.7, 0.2),
] {
let (n, u, w) = contact_frame(&n);
assert!((n.norm() - 1.0).abs() < 1e-12);
assert!((u.norm() - 1.0).abs() < 1e-12);
assert!((w.norm() - 1.0).abs() < 1e-12);
assert!(n.dot(u).abs() < 1e-12);
assert!(n.dot(w).abs() < 1e-12);
assert!(u.dot(w).abs() < 1e-12);
assert!((u.cross(w) - n).norm() < 1e-12);
}
}
}