use crate::math::{ComplexField, Pose, Real, Vector};
use crate::shape::SupportMap;
use crate::shape::{PolygonalFeature, Segment};
use crate::utils;
use core::mem;
use num::Zero;
#[cfg(feature = "dim3")]
use crate::shape::FeatureId;
#[cfg(feature = "dim2")]
use crate::shape::PackedFeatureId;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
#[cfg_attr(feature = "encase", derive(encase::ShaderType))]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)
)]
#[derive(PartialEq, Debug, Copy, Clone, Default)]
#[repr(C)]
pub struct Triangle {
pub a: Vector,
pub b: Vector,
pub c: Vector,
}
#[derive(Copy, Clone, Debug)]
pub enum TrianglePointLocation {
OnVertex(u32),
OnEdge(u32, [Real; 2]),
OnFace(u32, [Real; 3]),
OnSolid,
}
impl TrianglePointLocation {
pub fn barycentric_coordinates(&self) -> Option<[Real; 3]> {
let mut bcoords = [0.0; 3];
match self {
TrianglePointLocation::OnVertex(i) => bcoords[*i as usize] = 1.0,
TrianglePointLocation::OnEdge(i, uv) => {
let idx = match i {
0 => (0, 1),
1 => (1, 2),
2 => (0, 2),
_ => unreachable!(),
};
bcoords[idx.0] = uv[0];
bcoords[idx.1] = uv[1];
}
TrianglePointLocation::OnFace(_, uvw) => {
bcoords[0] = uvw[0];
bcoords[1] = uvw[1];
bcoords[2] = uvw[2];
}
TrianglePointLocation::OnSolid => {
return None;
}
}
Some(bcoords)
}
pub fn is_on_face(&self) -> bool {
matches!(*self, TrianglePointLocation::OnFace(..))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TriangleOrientation {
Clockwise,
CounterClockwise,
Degenerate,
}
impl From<[Vector; 3]> for Triangle {
fn from(arr: [Vector; 3]) -> Self {
*Self::from_array(&arr)
}
}
impl Triangle {
#[inline]
pub fn new(a: Vector, b: Vector, c: Vector) -> Triangle {
Triangle { a, b, c }
}
pub fn from_array(arr: &[Vector; 3]) -> &Triangle {
unsafe { mem::transmute(arr) }
}
#[inline]
pub fn vertices(&self) -> &[Vector; 3] {
unsafe { mem::transmute(self) }
}
#[inline]
#[cfg(feature = "dim3")]
pub fn normal(&self) -> Option<Vector> {
self.scaled_normal().try_normalize()
}
#[inline]
pub fn edges(&self) -> [Segment; 3] {
[
Segment::new(self.a, self.b),
Segment::new(self.b, self.c),
Segment::new(self.c, self.a),
]
}
pub fn scaled(self, scale: Vector) -> Self {
Self::new(self.a * scale, self.b * scale, self.c * scale)
}
#[inline]
pub fn transformed(&self, m: &Pose) -> Self {
Triangle::new(m * self.a, m * self.b, m * self.c)
}
#[inline]
pub fn edges_scaled_directions(&self) -> [Vector; 3] {
[self.b - self.a, self.c - self.b, self.a - self.c]
}
pub fn local_support_edge_segment(&self, dir: Vector) -> Segment {
let dots = [dir.dot(self.a), dir.dot(self.b), dir.dot(self.c)];
let imin = if dots[0] <= dots[1] && dots[0] <= dots[2] {
0
} else if dots[1] <= dots[2] {
1
} else {
2
};
match imin {
0 => Segment::new(self.b, self.c),
1 => Segment::new(self.c, self.a),
_ => Segment::new(self.a, self.b),
}
}
#[cfg(feature = "dim3")]
pub fn support_face(&self, _dir: Vector) -> PolygonalFeature {
PolygonalFeature::from(*self)
}
#[cfg(feature = "dim2")]
pub fn support_face(&self, dir: Vector) -> PolygonalFeature {
let mut best = 0;
let mut best_dot = -Real::MAX;
for (i, tangent) in self.edges_scaled_directions().iter().enumerate() {
let normal = Vector::new(tangent.y, -tangent.x);
if let Some(normal) = normal.try_normalize() {
let dot = normal.dot(dir);
if normal.dot(dir) > best_dot {
best = i;
best_dot = dot;
}
}
}
let pts = self.vertices();
let i1 = best;
let i2 = (best + 1) % 3;
PolygonalFeature {
vertices: [pts[i1], pts[i2]],
vids: PackedFeatureId::vertices([i1 as u32, i2 as u32]),
fid: PackedFeatureId::face(i1 as u32),
num_vertices: 2,
}
}
#[inline]
#[cfg(feature = "dim3")]
pub fn scaled_normal(&self) -> Vector {
let ab = self.b - self.a;
let ac = self.c - self.a;
ab.cross(ac)
}
#[inline]
#[cfg(feature = "dim3")]
pub fn robust_scaled_normal(&self) -> Vector {
let pts = self.vertices();
let best_vertex = self.angle_closest_to_90();
let d1 = pts[(best_vertex + 2) % 3] - pts[(best_vertex + 1) % 3];
let d2 = pts[best_vertex] - pts[(best_vertex + 1) % 3];
d1.cross(d2)
}
#[inline]
#[cfg(feature = "dim3")]
pub fn robust_normal(&self) -> Vector {
self.robust_scaled_normal().normalize()
}
#[inline]
pub fn extents_on_dir(&self, dir: Vector) -> (Real, Real) {
let a = self.a.dot(dir);
let b = self.b.dot(dir);
let c = self.c.dot(dir);
if a > b {
if b > c {
(c, a)
} else if a > c {
(b, a)
} else {
(b, c)
}
} else {
if a > c {
(c, b)
} else if b > c {
(a, b)
} else {
(a, c)
}
}
}
#[inline]
pub fn area(&self) -> Real {
let a = self.b.distance(self.a);
let b = self.c.distance(self.b);
let c = self.a.distance(self.c);
let (c, b, a) = utils::sort3(&a, &b, &c);
let a = *a;
let b = *b;
let c = *c;
let sqr = (a + (b + c)) * (c - (a - b)) * (c + (a - b)) * (a + (b - c));
<Real as ComplexField>::sqrt(sqr.max(0.0)) * 0.25
}
#[cfg(feature = "dim2")]
pub fn unit_angular_inertia(&self) -> Real {
let factor = 1.0 / 6.0;
let e1 = self.b - self.a;
let e2 = self.c - self.a;
let intx2 = e1.x * e1.x + e2.x * e1.x + e2.x * e2.x;
let inty2 = e1.y * e1.y + e2.y * e1.y + e2.y * e2.y;
factor * (intx2 + inty2)
}
#[inline]
pub fn center(&self) -> Vector {
utils::center(&[self.a, self.b, self.c])
}
#[inline]
pub fn perimeter(&self) -> Real {
self.b.distance(self.a) + self.c.distance(self.b) + self.a.distance(self.c)
}
pub fn circumcircle(&self) -> (Vector, Real) {
let a = self.a - self.c;
let b = self.b - self.c;
let na = a.length_squared();
let nb = b.length_squared();
let dab = a.dot(b);
let denom = 2.0 * (na * nb - dab * dab);
if denom.is_zero() {
let c = self.a - self.b;
let nc = c.length_squared();
if nc >= na && nc >= nb {
(
self.a.midpoint(self.b),
<Real as ComplexField>::sqrt(nc) / 2.0,
)
} else if na >= nb && na >= nc {
(
self.a.midpoint(self.c),
<Real as ComplexField>::sqrt(na) / 2.0,
)
} else {
(
self.b.midpoint(self.c),
<Real as ComplexField>::sqrt(nb) / 2.0,
)
}
} else {
let k = b * na - a * nb;
let center = self.c + (a * k.dot(b) - b * k.dot(a)) / denom;
let radius = center.distance(self.a);
(center, radius)
}
}
#[cfg(feature = "dim3")]
pub fn is_affinely_dependent(&self) -> bool {
const EPS: Real = crate::math::DEFAULT_EPSILON * 100.0;
let p1p2 = self.b - self.a;
let p1p3 = self.c - self.a;
relative_eq!(p1p2.cross(p1p3).length_squared(), 0.0, epsilon = EPS * EPS)
}
#[cfg(feature = "dim3")]
pub fn is_affinely_dependent_eps(&self, eps: Real) -> bool {
let p1p2 = self.b - self.a;
let p1p3 = self.c - self.a;
relative_eq!(
p1p2.cross(p1p3).length(),
0.0,
epsilon = eps * p1p2.length().max(p1p3.length())
)
}
#[cfg(feature = "dim2")]
pub fn contains_point(&self, p: Vector) -> bool {
let ab = self.b - self.a;
let bc = self.c - self.b;
let ca = self.a - self.c;
let sgn1 = ab.perp_dot(p - self.a);
let sgn2 = bc.perp_dot(p - self.b);
let sgn3 = ca.perp_dot(p - self.c);
sgn1.signum() * sgn2.signum() >= 0.0
&& sgn1.signum() * sgn3.signum() >= 0.0
&& sgn2.signum() * sgn3.signum() >= 0.0
}
#[cfg(feature = "dim3")]
pub fn contains_point(&self, p: Vector) -> bool {
const EPS: Real = crate::math::DEFAULT_EPSILON;
let vb = self.b - self.a;
let vc = self.c - self.a;
let vp = p - self.a;
let n = vc.cross(vb);
let n_norm = n.length_squared();
if n_norm < EPS || vp.dot(n).abs() > EPS * n_norm {
return false;
}
let nb = vb.cross(n);
let nc = vc.cross(n);
let signed_blim = vb.dot(nc);
let b = vp.dot(nc) * signed_blim.signum();
let blim = signed_blim.abs();
let signed_clim = vc.dot(nb);
let c = vp.dot(nb) * signed_clim.signum();
let clim = signed_clim.abs();
c >= 0.0 && c <= clim && b >= 0.0 && b <= blim && c * blim + b * clim <= blim * clim
}
#[cfg(feature = "dim3")]
pub fn feature_normal(&self, _: FeatureId) -> Option<Vector> {
self.normal()
}
#[cfg(feature = "dim2")]
pub fn orientation(&self, epsilon: Real) -> TriangleOrientation {
let area2 = (self.b - self.a).perp_dot(self.c - self.a);
if area2 > epsilon {
TriangleOrientation::CounterClockwise
} else if area2 < -epsilon {
TriangleOrientation::Clockwise
} else {
TriangleOrientation::Degenerate
}
}
pub fn orientation2d(
a: crate::math::Vector2,
b: crate::math::Vector2,
c: crate::math::Vector2,
epsilon: Real,
) -> TriangleOrientation {
let area2 = (b - a).perp_dot(c - a);
if area2 > epsilon {
TriangleOrientation::CounterClockwise
} else if area2 < -epsilon {
TriangleOrientation::Clockwise
} else {
TriangleOrientation::Degenerate
}
}
pub fn angle_closest_to_90(&self) -> usize {
let points = self.vertices();
let mut best_cos = 2.0;
let mut selected_i = 0;
for i in 0..3 {
let d1 = (points[i] - points[(i + 1) % 3]).normalize();
let d2 = (points[(i + 2) % 3] - points[(i + 1) % 3]).normalize();
let cos_abs = d1.dot(d2).abs();
if cos_abs < best_cos {
best_cos = cos_abs;
selected_i = i;
}
}
selected_i
}
pub fn reverse(&mut self) {
mem::swap(&mut self.b, &mut self.c);
}
}
impl SupportMap for Triangle {
#[inline]
fn local_support_point(&self, dir: Vector) -> Vector {
let d1 = self.a.dot(dir);
let d2 = self.b.dot(dir);
let d3 = self.c.dot(dir);
if d1 > d2 {
if d1 > d3 {
self.a
} else {
self.c
}
} else if d2 > d3 {
self.b
} else {
self.c
}
}
}
#[cfg(feature = "dim2")]
#[cfg(test)]
mod test {
use crate::math::Vector;
use crate::shape::Triangle;
#[test]
fn test_triangle_area() {
let pa = Vector::new(5.0, 0.0);
let pb = Vector::ZERO;
let pc = Vector::new(0.0, 4.0);
assert!(relative_eq!(Triangle::new(pa, pb, pc).area(), 10.0));
}
#[test]
fn test_triangle_contains_point() {
let tri = Triangle::new(Vector::new(5.0, 0.0), Vector::ZERO, Vector::new(0.0, 4.0));
assert!(tri.contains_point(Vector::new(1.0, 1.0)));
assert!(!tri.contains_point(Vector::new(-1.0, 1.0)));
}
#[test]
fn test_obtuse_triangle_contains_point() {
let tri = Triangle::new(
Vector::new(-10.0, 10.0),
Vector::ZERO,
Vector::new(20.0, 0.0),
);
assert!(tri.contains_point(Vector::new(-3.0, 5.0)));
assert!(tri.contains_point(Vector::new(5.0, 1.0)));
assert!(!tri.contains_point(Vector::new(0.0, -1.0)));
}
}
#[cfg(feature = "dim3")]
#[cfg(test)]
mod test {
use crate::math::{Real, Vector};
use crate::shape::Triangle;
#[test]
fn test_triangle_area() {
let pa = Vector::new(0.0, 5.0, 0.0);
let pb = Vector::ZERO;
let pc = Vector::new(0.0, 0.0, 4.0);
assert!(relative_eq!(Triangle::new(pa, pb, pc).area(), 10.0));
}
#[test]
fn test_triangle_contains_point() {
let tri = Triangle::new(
Vector::new(0.0, 5.0, 0.0),
Vector::ZERO,
Vector::new(0.0, 0.0, 4.0),
);
assert!(tri.contains_point(Vector::new(0.0, 1.0, 1.0)));
assert!(!tri.contains_point(Vector::new(0.0, -1.0, 1.0)));
}
#[test]
fn test_obtuse_triangle_contains_point() {
let tri = Triangle::new(
Vector::new(-10.0, 10.0, 0.0),
Vector::ZERO,
Vector::new(20.0, 0.0, 0.0),
);
assert!(tri.contains_point(Vector::new(-3.0, 5.0, 0.0)));
assert!(tri.contains_point(Vector::new(5.0, 1.0, 0.0)));
assert!(!tri.contains_point(Vector::new(0.0, -1.0, 0.0)));
}
#[test]
fn test_3dtriangle_contains_point() {
let o = Vector::ZERO;
let pa = Vector::new(1.2, 1.4, 5.6);
let pb = Vector::new(1.5, 6.7, 1.9);
let pc = Vector::new(5.0, 2.1, 1.3);
let tri = Triangle::new(pa, pb, pc);
let va = pa - o;
let vb = pb - o;
let vc = pc - o;
let n = (pa - pb).cross(pb - pc);
let contained_p = o + (va * 0.2 + vb * 0.3 + vc * 0.5);
let not_contained_coplanar_p = o + (va * -0.5 + vb * 0.8 + vc * 0.7);
let not_coplanar_p = o + (va * 0.2 + vb * 0.3 + vc * 0.5) + n * 0.1;
let not_coplanar_p2 = o + (va * -0.5 + vb * 0.8 + vc * 0.7) + n * 0.1;
assert!(tri.contains_point(contained_p));
assert!(!tri.contains_point(not_contained_coplanar_p));
assert!(!tri.contains_point(not_coplanar_p));
assert!(!tri.contains_point(not_coplanar_p2));
for i in -50i16..150 {
let a = 0.15;
let b = 0.01 * Real::from(i); let c = 1.0 - a - b;
let p = o + (va * a + vb * b + vc * c);
match i {
ii if ii < 0 || ii > 85 => assert!(
!tri.contains_point(p),
"Should not contain: i = {}, b = {}",
i,
b
),
ii if ii > 0 && ii < 85 => assert!(
tri.contains_point(p),
"Should contain: i = {}, b = {}",
i,
b
),
_ => (), }
}
}
}