use std::cmp::Ordering;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use crate::{
BoundingSphereRadius, Error, IntersectsAt, IntersectsAtGlobal, IsPointInside, SupportMapping,
Volume, shape::ConvexPolytope,
};
use hoomd_utility::valid::PositiveReal;
use hoomd_vector::{Cartesian, InnerProduct, Metric, Rotate, Rotation, RotationMatrix};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ConvexSurfaceMesh2d {
vertices: Vec<Cartesian<2>>,
bounding_radius: PositiveReal,
}
#[inline]
fn find_lowest_leftmost(vertices: &[Cartesian<2>]) -> Option<usize> {
vertices.iter().position_min_by(|a, b| {
a[1].total_cmp(&b[1]).then(a[0].total_cmp(&b[0]))
})
}
#[inline]
fn get_graham_key(p: Cartesian<2>, anchor: Cartesian<2>) -> (f64, f64) {
let diff = p - anchor;
(f64::atan2(diff[1], diff[0]), diff.dot(&diff))
}
#[inline]
fn predicate_orient2d((p, q): (Cartesian<2>, Cartesian<2>), test: Cartesian<2>) -> i64 {
let orientation = (p[0] * q[1] - p[1] * q[0])
+ (q[0] * test[1] - q[1] * test[0])
+ (test[0] * p[1] - test[1] * p[0]);
match orientation.total_cmp(&0.0) {
Ordering::Greater => 1,
Ordering::Less => -1,
Ordering::Equal => 0,
}
}
impl ConvexSurfaceMesh2d {
#[inline]
pub fn from_point_set<I>(points: I) -> Result<Self, Error>
where
I: IntoIterator<Item = Cartesian<2>>,
{
let vertices = Self::construct_convex_hull(points.into_iter().collect())?;
Ok(Self {
bounding_radius: ConvexPolytope::<2>::bounding_radius(&vertices),
vertices,
})
}
#[inline]
pub fn construct_convex_hull(
mut points: Vec<Cartesian<2>>,
) -> Result<Vec<Cartesian<2>>, Error> {
if points.len() < 3 {
return Err(Error::DegeneratePolytope);
}
let anchor_idx = find_lowest_leftmost(&points).ok_or(Error::DegeneratePolytope)?;
points.swap(0, anchor_idx);
let anchor = points[0];
points[1..].sort_unstable_by(|&a, &b| {
let (a0, a1) = get_graham_key(a, anchor);
let (b0, b1) = get_graham_key(b, anchor);
a0.total_cmp(&b0).then(a1.total_cmp(&b1))
});
let mut n_vertices_on_hull = 2;
let mut next_candidate = 2;
while next_candidate < points.len() {
let c = points[next_candidate];
while n_vertices_on_hull >= 2 {
let p = points[n_vertices_on_hull - 2];
let n = points[n_vertices_on_hull - 1];
if predicate_orient2d((p, n), c) <= 0 {
n_vertices_on_hull -= 1;
} else {
break;
}
}
points.swap(next_candidate, n_vertices_on_hull);
n_vertices_on_hull += 1;
next_candidate += 1;
}
points.truncate(n_vertices_on_hull);
if points.len() >= 3 {
Ok(points)
} else {
Err(Error::DegeneratePolytope)
}
}
#[inline]
#[must_use]
pub fn vertices(&self) -> &[Cartesian<2>] {
&self.vertices
}
}
impl SupportMapping<Cartesian<2>> for ConvexSurfaceMesh2d {
#[inline]
fn support_mapping(&self, n: &Cartesian<2>) -> Cartesian<2> {
*self
.vertices
.iter()
.max_by(|a, b| {
a.dot(n)
.partial_cmp(&b.dot(n))
.unwrap_or(std::cmp::Ordering::Equal)
})
.expect("there should be at least 3 vertices")
}
}
impl BoundingSphereRadius for ConvexSurfaceMesh2d {
#[inline]
fn bounding_sphere_radius(&self) -> PositiveReal {
self.bounding_radius
}
}
impl Volume for ConvexSurfaceMesh2d {
#[inline]
fn volume(&self) -> f64 {
0.5 * self
.vertices
.iter()
.circular_tuple_windows()
.fold(0.0, |total, (a, b)| total + a[0] * b[1] - b[0] * a[1])
}
}
impl IsPointInside<Cartesian<2>> for ConvexSurfaceMesh2d {
#[inline]
fn is_point_inside(&self, point: &Cartesian<2>) -> bool {
for (a, b) in self.vertices.iter().circular_tuple_windows() {
let edge = *b - *a;
let n = -edge.perpendicular();
let v = *point - *a;
if v.dot(&n) > 0.0 {
return false;
}
}
true
}
}
impl<R> IntersectsAtGlobal<Self, Cartesian<2>, R> for ConvexSurfaceMesh2d
where
R: Rotation + Rotate<Cartesian<2>>,
RotationMatrix<2>: From<R>,
{
#[inline]
fn intersects_at_global(
&self,
other: &Self,
r_self: &Cartesian<2>,
o_self: &R,
r_other: &Cartesian<2>,
o_other: &R,
) -> bool {
let max_separation =
self.bounding_sphere_radius().get() + other.bounding_sphere_radius().get();
if r_self.distance_squared(r_other) >= max_separation.powi(2) {
return false;
}
let (v_ij, o_ij) = hoomd_vector::pair_system_to_local(r_self, o_self, r_other, o_other);
self.intersects_at(other, &v_ij, &o_ij)
}
}
impl<R> IntersectsAt<Self, Cartesian<2>, R> for ConvexSurfaceMesh2d
where
RotationMatrix<2>: From<R>,
R: Copy,
{
#[inline]
fn intersects_at(&self, other: &Self, v_ij: &Cartesian<2>, o_ij: &R) -> bool {
assert!(
self.vertices.len() > 2 && other.vertices.len() > 2,
"A convex polygon must have at least 3 vertices."
);
let o_j = RotationMatrix::from(*o_ij);
if b_edge_separates(self, other, v_ij, &o_j) {
return false;
}
let o_j_inverted = o_j.inverted();
let v_ji = o_j_inverted.rotate(&-*v_ij);
if b_edge_separates(other, self, &v_ji, &o_j_inverted) {
return false;
}
true
}
}
#[inline]
fn b_edge_separates(
a: &ConvexSurfaceMesh2d,
b: &ConvexSurfaceMesh2d,
v_ab: &Cartesian<2>,
o_b: &RotationMatrix<2>,
) -> bool {
let mut previous = b.vertices.len() - 1;
for current in 0..b.vertices.len() {
let p = b.vertices[current];
let edge = p - b.vertices[previous];
let n = -edge.perpendicular();
let p_in_frame_a = o_b.rotate(&p) + *v_ab;
let n_in_frame_a = o_b.rotate(&n);
if is_separating(a, &p_in_frame_a, &n_in_frame_a) {
return true;
}
previous = current;
}
false
}
#[inline]
fn is_separating(a: &ConvexSurfaceMesh2d, p: &Cartesian<2>, n: &Cartesian<2>) -> bool {
let n_dot_p = n.dot(p);
for v in &a.vertices {
if n.dot(v) - n_dot_p <= 0.0 {
return false;
}
}
true
}
impl<const MAX_VERTICES: usize> TryFrom<ConvexPolytope<2, MAX_VERTICES>> for ConvexSurfaceMesh2d {
type Error = Error;
#[inline]
fn try_from(v: ConvexPolytope<2, MAX_VERTICES>) -> Result<ConvexSurfaceMesh2d, Error> {
Self::from_point_set(v.vertices().iter().copied())
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
use approxim::assert_relative_eq;
use assert2::check;
use hoomd_vector::Angle;
use rand::{RngExt, SeedableRng, rngs::StdRng};
use rstest::*;
#[rstest]
#[case::single_point(vec![[1.0, 2.0]], 0)]
#[case::two_points_first_lower(vec![[1.0, 1.0], [2.0, 3.0]], 0)]
#[case::two_points_second_lower(vec![[1.0, 3.0], [2.0, 1.0]], 1)]
#[case::same_y_leftmost_wins(vec![[3.0, 1.0], [1.0, 1.0], [2.0, 1.0]], 1)]
#[case::same_y_negative(vec![[0.0, -5.0], [-3.0, -5.0], [2.0, -5.0]], 1)]
#[case::multiple_points(vec![[3.0, 5.0], [1.0, 1.0], [4.0, 2.0], [2.0, 3.0]], 1)]
#[case::negative_coords(vec![[0.0, 0.0], [-1.0, -1.0], [1.0, -1.0]], 1)]
#[case::same_y_all_negative_x(vec![[5.0, 0.0], [-10.0, 0.0], [-5.0, 0.0]], 1)]
#[case::lowest_is_only_point(vec![[100.0, -100.0]], 0)]
#[case::diagonal_tiebreak(vec![[5.0, 5.0], [4.0, 4.0], [3.0, 3.0], [2.0, 2.0], [1.0, 1.0]], 4)]
fn test_find_lowest_leftmost(#[case] vertices: Vec<[f64; 2]>, #[case] expected_idx: usize) {
let vertices: Vec<Cartesian<2>> = vertices.into_iter().map(Cartesian::from).collect();
let idx = find_lowest_leftmost(&vertices).expect("returned None for non-empty input");
assert_eq!(idx, expected_idx);
}
#[rstest]
fn test_find_lowest_leftmost_empty() {
let vertices: Vec<Cartesian<2>> = vec![];
assert_eq!(find_lowest_leftmost(&vertices), None);
}
#[rstest]
fn test_single_point_various_coords(
#[values([0.0, 0.0], [-1.0, -1.0], [100.0, -50.0], [f64::MIN_POSITIVE, f64::MIN_POSITIVE])]
coords: [f64; 2],
) {
let vertices = vec![Cartesian::from(coords)];
let idx = find_lowest_leftmost(&vertices).expect("returned None for non-empty input");
assert_eq!(idx, 0);
}
#[rstest]
fn test_square_corners() {
let points: Vec<Cartesian<2>> = vec![[1.0, 1.0], [1.0, 0.0], [0.0, 0.0], [0.0, 1.0]]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[0.0, 0.0].into(),
[1.0, 0.0].into(),
[1.0, 1.0].into(),
[0.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_square_corners_big() {
let points: Vec<Cartesian<2>> = vec![
[101.0, 101.0],
[101.0, 100.0],
[100.0, 101.0],
[100.0, 100.0],
]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[100.0, 100.0].into(),
[101.0, 100.0].into(),
[101.0, 101.0].into(),
[100.0, 101.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_square_with_edge_points() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0],
[0.5, 0.0],
[1.0, 0.0], [1.0, 0.5],
[1.0, 1.0], [0.5, 1.0],
[0.0, 1.0], [0.0, 0.5], ]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[0.0, 0.0].into(),
[1.0, 0.0].into(),
[1.0, 1.0].into(),
[0.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_square_dense_boundary() {
let mut pts: Vec<[f64; 2]> = Vec::new();
for i in 0..20 {
pts.push([f64::from(i) / 19.0, 0.0]);
}
for i in 0..20 {
pts.push([1.0, f64::from(i) / 19.0]);
}
for i in 0..20 {
pts.push([f64::from(i) / 19.0, 1.0]);
}
for i in 0..20 {
pts.push([0.0, f64::from(i) / 19.0]);
}
let points: Vec<Cartesian<2>> = pts.into_iter().map(Cartesian::from).collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[0.0, 0.0].into(),
[1.0, 0.0].into(),
[1.0, 1.0].into(),
[0.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_circle_uniform() {
let n = 20;
let points: Vec<Cartesian<2>> = (0..n)
.map(|i| {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
Cartesian::from([angle.cos(), angle.sin()])
})
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), n);
}
#[rstest]
fn test_circle_with_interior_points() {
let n_boundary = 12;
let mut points: Vec<Cartesian<2>> = (0..n_boundary)
.map(|i| {
let angle = 2.0 * std::f64::consts::PI * i as f64 / n_boundary as f64;
Cartesian::from([angle.cos(), angle.sin()])
})
.collect();
points.extend([[0.0, 0.0], [0.3, 0.3], [-0.2, 0.1], [0.1, -0.4]].map(Cartesian::from));
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), n_boundary);
}
#[rstest]
fn test_circle_partial_arc() {
let points: Vec<Cartesian<2>> = (0..10)
.map(|i| {
let angle = -std::f64::consts::FRAC_PI_4
+ (std::f64::consts::FRAC_PI_2 * f64::from(i) / 9.0);
Cartesian::from([angle.cos(), angle.sin()])
})
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 10);
}
#[rstest]
fn test_random_unit_square(#[values(0, 42, 64, 100_000)] seed: u64) {
let mut rng = StdRng::seed_from_u64(seed);
let original: Vec<Cartesian<2>> = (0..50)
.map(|_| Cartesian::from([rng.random::<f64>(), rng.random::<f64>()]))
.collect();
let points = original.clone();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
for h in &vertices {
assert!(original.iter().any(|v| (*v - *h).norm() < 1e-10));
}
}
#[rstest]
fn test_random_gaussian() {
let mut rng = StdRng::seed_from_u64(42);
let points: Vec<Cartesian<2>> = (0..100)
.map(|_| {
Cartesian::from([
rng.random::<f64>() * 2.0 - 1.0,
rng.random::<f64>() * 2.0 - 1.0,
])
})
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
}
#[rstest]
fn test_random_deterministic_output() {
for _ in 0..3 {
let mut rng = StdRng::seed_from_u64(123);
let points1: Vec<Cartesian<2>> = (0..30)
.map(|_| Cartesian::from([rng.random::<f64>(), rng.random::<f64>()]))
.collect();
let vertices1 = ConvexSurfaceMesh2d::construct_convex_hull(points1)
.expect("hard-coded points should lie on a convex hull");
let mut rng = StdRng::seed_from_u64(123);
let points2: Vec<Cartesian<2>> = (0..30)
.map(|_| Cartesian::from([rng.random::<f64>(), rng.random::<f64>()]))
.collect();
let vertices2 = ConvexSurfaceMesh2d::construct_convex_hull(points2)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices1.len(), vertices2.len());
}
}
#[rstest]
fn test_duplicate_lowest_points() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0],
[0.0, 0.0],
[0.0, 0.0], [1.0, 0.0],
[1.0, 1.0],
[0.0, 1.0],
]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
let hull = [
[0.0, 0.0].into(),
[1.0, 0.0].into(),
[1.0, 1.0].into(),
[0.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_many_duplicates_few_unique() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0],
[0.0, 0.0],
[0.0, 0.0],
[2.0, 0.0],
[2.0, 0.0],
[1.0, 2.0],
[1.0, 2.0],
[1.0, 2.0],
]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
let hull = [[0.0, 0.0].into(), [2.0, 0.0].into(), [1.0, 2.0].into()];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_collinear_bottom_edge() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0],
[0.5, 0.0],
[1.0, 0.0],
[1.5, 0.0], [0.75, 1.0], ]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
let hull = [[0.0, 0.0].into(), [1.5, 0.0].into(), [0.75, 1.0].into()];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_leftmost_selected() {
let points: Vec<Cartesian<2>> = vec![
[0.5, 0.0],
[1.0, 0.0],
[0.0, 0.0], [0.5, 1.0],
]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
let hull = [[0.0, 0.0].into(), [1.0, 0.0].into(), [0.5, 1.0].into()];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_many_bottom_points() {
let mut points: Vec<[f64; 2]> = (0..10).map(|i| [f64::from(i) * 2.0 / 9.0, 0.0]).collect();
points.push([1.0, 1.0]); let points: Vec<Cartesian<2>> = points.into_iter().map(Cartesian::from).collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
}
#[rstest]
fn test_collinear_from_anchor() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0], [1.0, 1.0],
[2.0, 2.0],
[3.0, 3.0], [1.0, 0.0],
[0.0, 1.0], ]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
let hull = [
[0.0, 0.0].into(),
[1.0, 0.0].into(),
[3.0, 3.0].into(),
[0.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_radial_collinear_multiple_directions() {
let points: Vec<Cartesian<2>> = vec![
[0.0, 0.0], [1.0, 0.0],
[2.0, 0.0],
[3.0, 0.0], [0.0, 1.0],
[0.0, 2.0],
[0.0, 3.0], [1.0, 1.0],
[2.0, 2.0], [-1.0, 0.0],
[-2.0, 0.0], ]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 3);
}
#[rstest]
fn test_star_pattern() {
let mut points: Vec<Cartesian<2>> = vec![Cartesian::from([0.0, 0.0])]; for i in 0..8 {
let angle = 2.0 * std::f64::consts::PI * f64::from(i) / 8.0;
for r in [0.5, 1.0, 1.5] {
points.push(Cartesian::from([r * angle.cos(), r * angle.sin()]));
}
}
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert!(vertices.len() >= 8); }
#[rstest]
fn test_minimum_triangle() {
let points: Vec<Cartesian<2>> = vec![[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 3);
let hull = [[0.0, 0.0].into(), [1.0, 0.0].into(), [0.5, 1.0].into()];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_negative_coordinates() {
let points: Vec<Cartesian<2>> = vec![[1.0, 1.0], [1.0, -1.0], [-1.0, 1.0], [-1.0, -1.0]]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[-1.0, -1.0].into(),
[1.0, -1.0].into(),
[1.0, 1.0].into(),
[-1.0, 1.0].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_large_coordinates() {
let points: Vec<Cartesian<2>> = vec![[0.0, 0.0], [1e6, 0.0], [1e6, 1e6], [0.0, 1e6]]
.into_iter()
.map(Cartesian::from)
.collect();
let vertices = ConvexSurfaceMesh2d::construct_convex_hull(points)
.expect("hard-coded points should lie on a convex hull");
assert_eq!(vertices.len(), 4);
let hull = [
[0.0, 0.0].into(),
[1e6, 0.0].into(),
[1e6, 1e6].into(),
[0.0, 1e6].into(),
];
itertools::assert_equal(&vertices, &hull);
}
#[rstest]
fn test_degenerate() {
let points: Vec<Cartesian<2>> = vec![[0.0, 0.0], [0.5, 0.5], [0.25, 0.25], [1.0, 1.0]]
.into_iter()
.map(Cartesian::from)
.collect();
let result = ConvexSurfaceMesh2d::construct_convex_hull(points);
check!(result == Err(Error::DegeneratePolytope));
}
#[test]
fn support_mapping_2d() {
let cuboid = ConvexSurfaceMesh2d::from_point_set([
[-1.0, -2.0].into(),
[1.0, -2.0].into(),
[1.0, 2.0].into(),
[-1.0, 2.0].into(),
])
.expect("hard-coded vertices form a polygon");
assert_relative_eq!(
cuboid.support_mapping(&Cartesian::from([1.0, 0.1])),
[1.0, 2.0].into()
);
assert_relative_eq!(
cuboid.support_mapping(&Cartesian::from([1.0, -0.1])),
[1.0, -2.0].into()
);
assert_relative_eq!(
cuboid.support_mapping(&Cartesian::from([-0.1, 1.0])),
[-1.0, 2.0].into()
);
assert_relative_eq!(
cuboid.support_mapping(&Cartesian::from([-0.1, -1.0])),
[-1.0, -2.0].into()
);
}
#[fixture]
fn square() -> ConvexSurfaceMesh2d {
ConvexSurfaceMesh2d::from_point_set([
[-0.5, -0.5].into(),
[0.5, -0.5].into(),
[0.5, 0.5].into(),
[-0.5, 0.5].into(),
])
.expect("hard-coded vertices form a valid polygon")
}
#[fixture]
fn triangle() -> ConvexSurfaceMesh2d {
ConvexSurfaceMesh2d::from_point_set([
[-0.5, -0.5].into(),
[0.5, -0.5].into(),
[0.5, 0.5].into(),
])
.expect("hard-coded vertices form a valid polygon")
}
#[rstest]
fn square_no_rot(square: ConvexSurfaceMesh2d) {
let a = Angle::identity();
assert!(!square.intersects_at(&square, &[10.0, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[-10.0, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[1.1, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[-1.1, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[0.0, 1.1].into(), &a));
assert!(!square.intersects_at(&square, &[0.0, -1.1].into(), &a));
assert!(square.intersects_at(&square, &[0.9, 0.2].into(), &a));
assert!(square.intersects_at(&square, &[-0.9, 0.2].into(), &a));
assert!(square.intersects_at(&square, &[-0.2, 0.9].into(), &a));
assert!(square.intersects_at(&square, &[-0.2, -0.9].into(), &a));
assert!(square.intersects_at(&square, &[1.0, 0.2].into(), &a));
}
#[rstest]
fn square_rot(square: ConvexSurfaceMesh2d) {
let a = Angle::from(PI / 4.0);
assert!(!square.intersects_at(&square, &[10.0, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[-10.0, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[1.3, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[-1.3, 0.0].into(), &a));
assert!(!square.intersects_at(&square, &[0.0, 1.3].into(), &a));
assert!(!square.intersects_at(&square, &[0.0, -1.3].into(), &a));
assert!(!square.intersects_at(&square, &[1.3, 0.2].into(), &a));
assert!(!square.intersects_at(&square, &[-1.3, 0.2].into(), &a));
assert!(!square.intersects_at(&square, &[-0.2, 1.3].into(), &a));
assert!(!square.intersects_at(&square, &[-0.2, -1.3].into(), &a));
assert!(square.intersects_at(&square, &[1.2, 0.2].into(), &a));
assert!(square.intersects_at(&square, &[-1.2, 0.2].into(), &a));
assert!(square.intersects_at(&square, &[-0.2, 1.2].into(), &a));
assert!(square.intersects_at(&square, &[-0.2, -1.2].into(), &a));
}
fn test_overlap<A, B, R, const N: usize>(
r_ab: Cartesian<N>,
a: &A,
b: &B,
o_a: R,
o_b: &R,
) -> bool
where
R: Rotation + Rotate<Cartesian<N>>,
A: IntersectsAt<B, Cartesian<N>, R>,
{
let r_a_inverted = o_a.inverted();
let v_ij = r_a_inverted.rotate(&r_ab);
let o_ij = o_b.combine(&r_a_inverted);
a.intersects_at(b, &v_ij, &o_ij)
}
fn assert_symmetric_overlap<A, B, R, const N: usize>(
r_ab: Cartesian<N>,
a: &A,
b: &B,
r_a: R,
r_b: R,
expected: bool,
) where
R: Rotation + Rotate<Cartesian<N>>,
A: IntersectsAt<B, Cartesian<N>, R>,
B: IntersectsAt<A, Cartesian<N>, R>,
{
assert_eq!(test_overlap(r_ab, a, b, r_a, &r_b), expected);
assert_eq!(test_overlap(-r_ab, b, a, r_b, &r_a), expected);
}
#[rstest]
fn square_triangle(square: ConvexSurfaceMesh2d, triangle: ConvexSurfaceMesh2d) {
let r_square = Angle::from(-PI / 4.0);
let r_triangle = Angle::from(PI);
assert_symmetric_overlap(
[10.0, 0.0].into(),
&square,
&triangle,
r_square,
r_triangle,
false,
);
assert_symmetric_overlap(
[1.3, 0.0].into(),
&square,
&triangle,
r_square,
r_triangle,
false,
);
assert_symmetric_overlap(
[-1.3, 0.0].into(),
&square,
&triangle,
r_square,
r_triangle,
false,
);
assert_symmetric_overlap(
[0.0, 1.3].into(),
&square,
&triangle,
r_square,
r_triangle,
false,
);
assert_symmetric_overlap(
[0.0, -1.3].into(),
&square,
&triangle,
r_square,
r_triangle,
false,
);
assert_symmetric_overlap(
[1.2, 0.2].into(),
&square,
&triangle,
r_square,
r_triangle,
true,
);
assert_symmetric_overlap(
[-0.7, -0.2].into(),
&square,
&triangle,
r_square,
r_triangle,
true,
);
assert_symmetric_overlap(
[0.4, 1.1].into(),
&square,
&triangle,
r_square,
r_triangle,
true,
);
assert_symmetric_overlap(
[-0.2, -1.2].into(),
&square,
&triangle,
r_square,
r_triangle,
true,
);
}
}