Skip to content

Commit e3ee365

Browse files
authored
Merge pull request #48 from Andlon/aabb_closest_point
Implement AABB::closest_point_to and related methods
2 parents 6c6347c + 99f1cb5 commit e3ee365

File tree

2 files changed

+154
-12
lines changed

2 files changed

+154
-12
lines changed

fenris-geometry/src/lib.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ where
151151
D: DimName,
152152
DefaultAllocator: Allocator<T, D>,
153153
{
154-
/// Computes the minimal bounding box which encloses both `this` and `other`.
154+
/// Computes the minimal bounding box which encloses both `self` and `other`.
155155
pub fn enclose(&self, other: &AxisAlignedBoundingBox<T, D>) -> Self {
156156
let min = self
157157
.min
@@ -266,16 +266,33 @@ where
266266
})
267267
}
268268

269+
/// Computes the point in the bounding box closest to the given point.
270+
pub fn closest_point_to(&self, point: &OPoint<T, D>) -> OPoint<T, D> {
271+
point
272+
.coords
273+
.zip_zip_map(&self.min.coords, &self.max.coords, |p_i, a_i, b_i| {
274+
if p_i <= a_i {
275+
a_i
276+
} else if p_i >= b_i {
277+
b_i
278+
} else {
279+
p_i
280+
}
281+
})
282+
.into()
283+
}
284+
285+
/// Computes the distance between the bounding box and the given point.
286+
pub fn dist_to(&self, point: &OPoint<T, D>) -> T {
287+
self.dist2_to(point).sqrt()
288+
}
289+
290+
/// Computes the squared distance between the bounding box and the given point.
291+
pub fn dist2_to(&self, point: &OPoint<T, D>) -> T {
292+
(self.closest_point_to(point) - point).norm_squared()
293+
}
294+
269295
/// Compute the point in the bounding box furthest away from the given point.
270-
///
271-
/// # Panics
272-
///
273-
/// Panics if two distances cannot be ordered. This typically only happens if
274-
/// one of the numbers is not a number (NaN) or the comparison is not sensible, such as
275-
/// comparing two infinities. Since given finite coordinates no distance should be infinite,
276-
/// this method will realistically only panic in cases where one of the points
277-
/// --- either of the bounding box or the query point --- has components that are not
278-
/// finite numbers.
279296
#[replace_float_literals(T::from_f64(literal).unwrap())]
280297
pub fn furthest_point_to(&self, point: &OPoint<T, D>) -> OPoint<T, D>
281298
where

fenris-geometry/tests/unit_tests/aabb.rs

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use fenris::allocators::DimAllocator;
22
use fenris_geometry::proptest::{aabb2, aabb3, point2, point3};
3-
use fenris_geometry::AxisAlignedBoundingBox;
3+
use fenris_geometry::{AxisAlignedBoundingBox, AxisAlignedBoundingBox2d, AxisAlignedBoundingBox3d};
44
use matrixcompare::assert_scalar_eq;
55
use nalgebra::allocator::Allocator;
6-
use nalgebra::distance_squared;
6+
use nalgebra::proptest::vector;
7+
use nalgebra::{distance, distance_squared, Const, Point};
78
use nalgebra::{point, DefaultAllocator, DimName, OPoint, U2};
9+
use proptest::collection::vec;
810
use proptest::prelude::*;
911

1012
#[test]
@@ -74,6 +76,21 @@ macro_rules! assert_unordered_eq {
7476
}};
7577
}
7678

79+
fn point_in_aabb<const D: usize>(aabb: AxisAlignedBoundingBox<f64, Const<D>>) -> impl Strategy<Value = Point<f64, D>> {
80+
// Bias generation 0.0 and 1.0 values to ensure that we generate values also on the boundary
81+
// of the box
82+
let values = prop_oneof![
83+
1 => Just(0.0),
84+
1 => Just(1.0),
85+
5 => 0.0 ..= 1.0];
86+
vector(values, Const::<D>)
87+
.prop_map(move |v| {
88+
// Coordinates are in [0, 1] interval, transform to [a_i, b_i]
89+
v.zip_zip_map(&aabb.min().coords, &aabb.max().coords, |p, a, b| (1.0 - p) * a + p * b)
90+
})
91+
.prop_map(Point::from)
92+
}
93+
7794
#[test]
7895
fn test_aabb_corners_iter() {
7996
// 1D
@@ -132,6 +149,57 @@ fn test_furthest_point_2d() {
132149
}
133150
}
134151

152+
#[test]
153+
fn test_closest_point() {
154+
// Helper macro for succinct checks
155+
macro_rules! assert_closest_point {
156+
($aabb:expr, $p:expr => $expected:expr) => {{
157+
let aabb = $aabb;
158+
let p = $p;
159+
let q = aabb.closest_point_to(&p);
160+
assert_eq!(&q, &$expected);
161+
assert_eq!(distance(&q, &p), aabb.dist_to(&p));
162+
assert_eq!(distance_squared(&q, &p), aabb.dist2_to(&p));
163+
}};
164+
}
165+
166+
// 2D
167+
{
168+
let a = point![2.0, 3.0];
169+
let b = point![3.0, 5.0];
170+
let aabb = AxisAlignedBoundingBox2d::new(a, b);
171+
// Outside points
172+
assert_closest_point!(aabb, point![1.0, 1.0] => point![2.0, 3.0]);
173+
assert_closest_point!(aabb, point![2.0, 2.0] => point![2.0, 3.0]);
174+
assert_closest_point!(aabb, point![1.0, 4.0] => point![2.0, 4.0]);
175+
assert_closest_point!(aabb, point![1.0, 5.0] => point![2.0, 5.0]);
176+
assert_closest_point!(aabb, point![-1.0, 6.0] => point![2.0, 5.0]);
177+
assert_closest_point!(aabb, point![2.5, 7.0] => point![2.5, 5.0]);
178+
assert_closest_point!(aabb, point![4.0, 6.0] => point![3.0, 5.0]);
179+
assert_closest_point!(aabb, point![6.0, 4.0] => point![3.0, 4.0]);
180+
assert_closest_point!(aabb, point![5.0, 2.0] => point![3.0, 3.0]);
181+
182+
// Inside points
183+
assert_closest_point!(aabb, point![2.5, 4.0] => point![2.5, 4.0]);
184+
assert_closest_point!(aabb, point![2.3, 4.6] => point![2.3, 4.6]);
185+
}
186+
187+
// 3D. We only test a few points since the impl is the same as in 2D and
188+
// we have proptests that should cover things quite extensively
189+
{
190+
let a = point![2.0, 3.0, 1.0];
191+
let b = point![3.0, 5.0, 6.0];
192+
let aabb = AxisAlignedBoundingBox3d::new(a, b);
193+
// Outside points
194+
assert_closest_point!(aabb, point![1.0, 1.0, 1.0] => point![2.0, 3.0, 1.0]);
195+
assert_closest_point!(aabb, point![4.0, 6.0, 8.0] => point![3.0, 5.0, 6.0]);
196+
assert_closest_point!(aabb, point![1.0, 4.0, 5.0] => point![2.0, 4.0, 5.0]);
197+
198+
// Inside points
199+
assert_closest_point!(aabb, point![2.5, 4.0, 3.0] => point![2.5, 4.0, 3.0]);
200+
}
201+
}
202+
135203
proptest! {
136204

137205
#[test]
@@ -186,4 +254,61 @@ proptest! {
186254
prop_assert!(aabb.contains_point(&q));
187255
}
188256

257+
#[test]
258+
fn aabb_dists_agree_with_closest_point_2d(point in point2(), aabb in aabb2()) {
259+
let q = aabb.closest_point_to(&point);
260+
let dist2 = distance_squared(&q, &point);
261+
prop_assert_eq!(aabb.dist2_to(&point), dist2);
262+
prop_assert_eq!(aabb.dist_to(&point), dist2.sqrt());
263+
}
264+
265+
#[test]
266+
fn aabb_dists_agree_with_closest_point_3d(point in point3(), aabb in aabb3()) {
267+
let q = aabb.closest_point_to(&point);
268+
let dist2 = distance_squared(&q, &point);
269+
prop_assert_eq!(aabb.dist2_to(&point), dist2);
270+
prop_assert_eq!(aabb.dist_to(&point), dist2.sqrt());
271+
}
272+
273+
#[test]
274+
fn aabb_closest_point_2d_closer_than_other_points(
275+
p in point2(),
276+
(aabb, test_points) in aabb2()
277+
.prop_flat_map(|aabb| (Just(aabb), vec(point_in_aabb(aabb), 0 .. 50)))
278+
) {
279+
let q = aabb.closest_point_to(&p);
280+
prop_assert!(aabb.contains_point(&q));
281+
for test_point in test_points {
282+
assert!(aabb.contains_point(&test_point));
283+
prop_assert!(distance(&q, &p) <= distance(&p, &test_point));
284+
}
285+
}
286+
287+
#[test]
288+
fn aabb_closest_point_3d_closer_than_other_points(
289+
p in point3(),
290+
(aabb, test_points) in aabb3()
291+
.prop_flat_map(|aabb| (Just(aabb), vec(point_in_aabb(aabb), 0 .. 50)))
292+
) {
293+
let q = aabb.closest_point_to(&p);
294+
prop_assert!(aabb.contains_point(&q));
295+
for test_point in test_points {
296+
assert!(aabb.contains_point(&test_point));
297+
prop_assert!(distance(&q, &p) <= distance(&p, &test_point));
298+
}
299+
}
300+
301+
#[test]
302+
fn aabb_closest_point_of_internal_point_2d(
303+
(aabb, p) in aabb2().prop_flat_map(|aabb| (Just(aabb), point_in_aabb(aabb)))
304+
) {
305+
prop_assert_eq!(aabb.closest_point_to(&p), p);
306+
}
307+
308+
#[test]
309+
fn aabb_closest_point_of_internal_point_3d(
310+
(aabb, p) in aabb3().prop_flat_map(|aabb| (Just(aabb), point_in_aabb(aabb)))
311+
) {
312+
prop_assert_eq!(aabb.closest_point_to(&p), p);
313+
}
189314
}

0 commit comments

Comments
 (0)