use crate::coords;
use crate::error::{Error, Result};
use crate::geom;
#[derive(Default)]
struct Parts {
points: Vec<[f64; 3]>,
segments: Vec<([f64; 3], [f64; 3])>,
faces: Vec<Vec<[f64; 3]>>,
}
impl Parts {
fn vertices(&self) -> impl Iterator<Item = [f64; 3]> + '_ {
self.points
.iter()
.copied()
.chain(self.segments.iter().flat_map(|(a, b)| [*a, *b]))
.chain(self.faces.iter().flatten().copied())
}
fn triangles(&self) -> impl Iterator<Item = ([f64; 3], [f64; 3], [f64; 3])> + '_ {
self.faces.iter().flat_map(|ring| {
let n = ring.len();
(1..n.saturating_sub(1)).map(move |i| (ring[0], ring[i], ring[i + 1]))
})
}
}
fn parts(bytes: &[u8], func: &'static str) -> Result<Parts> {
let mut parts = Parts::default();
let mut current: Vec<[f64; 3]> = Vec::new();
let mut current_base = 0u32;
let flush = |base: u32, run: &mut Vec<[f64; 3]>, parts: &mut Parts| {
if run.is_empty() {
return;
}
match base {
coords::base::POINT => parts.points.extend(run.iter().copied()),
coords::base::LINESTRING => {
for pair in run.windows(2) {
parts.segments.push((pair[0], pair[1]));
}
}
coords::base::POLYGON | coords::base::TRIANGLE => {
parts.faces.push(std::mem::take(run));
}
_ => {}
}
run.clear();
};
coords::for_each_coord_typed(bytes, &mut |c, first, base| {
if first {
flush(current_base, &mut current, &mut parts);
current_base = base;
}
current.push([c.x, c.y, c.z.unwrap_or(0.0)]);
})?;
flush(current_base, &mut current, &mut parts);
let _ = func;
Ok(parts)
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn lerp(a: [f64; 3], b: [f64; 3], t: f64) -> [f64; 3] {
[
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
]
}
fn norm(v: [f64; 3]) -> f64 {
dot(v, v).sqrt()
}
#[derive(Debug, Clone, Copy)]
struct Witness {
d: f64,
a: [f64; 3],
b: [f64; 3],
}
impl Witness {
fn between(a: [f64; 3], b: [f64; 3]) -> Self {
Self {
d: norm(sub(a, b)),
a,
b,
}
}
fn min(self, other: Self) -> Self {
if other.d < self.d { other } else { self }
}
fn flipped(self) -> Self {
Self {
d: self.d,
a: self.b,
b: self.a,
}
}
}
fn pt_seg(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> Witness {
let ab = sub(b, a);
let len_sq = dot(ab, ab);
let t = if len_sq == 0.0 {
0.0
} else {
(dot(sub(p, a), ab) / len_sq).clamp(0.0, 1.0)
};
Witness::between(p, lerp(a, b, t))
}
fn seg_seg(p1: [f64; 3], q1: [f64; 3], p2: [f64; 3], q2: [f64; 3]) -> Witness {
let (d1, d2, r) = (sub(q1, p1), sub(q2, p2), sub(p1, p2));
let (a, e, f) = (dot(d1, d1), dot(d2, d2), dot(d2, r));
if a <= f64::EPSILON && e <= f64::EPSILON {
return Witness::between(p1, p2);
}
if a <= f64::EPSILON {
return pt_seg(p1, p2, q2).flipped().flipped();
}
if e <= f64::EPSILON {
return pt_seg(p2, p1, q1).flipped();
}
let c = dot(d1, r);
let b = dot(d1, d2);
let denom = a * e - b * b;
let mut s = if denom != 0.0 {
((b * f - c * e) / denom).clamp(0.0, 1.0)
} else {
0.0 };
let mut t = (b * s + f) / e;
if t < 0.0 {
t = 0.0;
s = (-c / a).clamp(0.0, 1.0);
} else if t > 1.0 {
t = 1.0;
s = ((b - c) / a).clamp(0.0, 1.0);
}
Witness::between(lerp(p1, q1, s), lerp(p2, q2, t))
}
fn pt_tri(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> Witness {
let n = cross(sub(b, a), sub(c, a));
let n_sq = dot(n, n);
if n_sq > 0.0 {
let foot = {
let dist = dot(sub(p, a), n) / n_sq;
[p[0] - dist * n[0], p[1] - dist * n[1], p[2] - dist * n[2]]
};
let inside = [(a, b), (b, c), (c, a)]
.iter()
.all(|(u, v)| dot(cross(sub(*v, *u), sub(foot, *u)), n) >= 0.0);
if inside {
return Witness::between(p, foot);
}
}
pt_seg(p, a, b).min(pt_seg(p, b, c)).min(pt_seg(p, c, a))
}
fn tri_tri(t1: ([f64; 3], [f64; 3], [f64; 3]), t2: ([f64; 3], [f64; 3], [f64; 3])) -> Witness {
let e1 = [(t1.0, t1.1), (t1.1, t1.2), (t1.2, t1.0)];
let e2 = [(t2.0, t2.1), (t2.1, t2.2), (t2.2, t2.0)];
let mut best = Witness {
d: f64::INFINITY,
a: t1.0,
b: t2.0,
};
for (a1, b1) in e1 {
for (a2, b2) in e2 {
best = best.min(seg_seg(a1, b1, a2, b2));
if best.d == 0.0 {
return best;
}
}
}
for v in [t1.0, t1.1, t1.2] {
best = best.min(pt_tri(v, t2.0, t2.1, t2.2));
}
for v in [t2.0, t2.1, t2.2] {
best = best.min(pt_tri(v, t1.0, t1.1, t1.2).flipped());
}
best
}
#[derive(Clone, Copy)]
struct Aabb {
lo: [f64; 3],
hi: [f64; 3],
}
const SLACK: f64 = 8.0 * f64::EPSILON;
impl Aabb {
fn point(p: [f64; 3]) -> Self {
Self { lo: p, hi: p }
}
fn of(points: impl IntoIterator<Item = [f64; 3]>) -> Option<Self> {
let mut it = points.into_iter();
let mut b = Self::point(it.next()?);
for p in it {
b.add(p);
}
Some(b)
}
fn add(&mut self, p: [f64; 3]) {
for (i, c) in p.iter().enumerate() {
self.lo[i] = self.lo[i].min(*c);
self.hi[i] = self.hi[i].max(*c);
}
}
fn union(self, other: Self) -> Self {
let mut b = self;
b.add(other.lo);
b.add(other.hi);
b
}
fn min_dist_sq(&self, other: &Self) -> f64 {
let mut sum = 0.0;
for i in 0..3 {
let gap = (self.lo[i] - other.hi[i])
.max(other.lo[i] - self.hi[i])
.max(0.0);
sum += gap * gap;
}
sum
}
fn max_dist_sq(&self, other: &Self) -> f64 {
let mut sum = 0.0;
for i in 0..3 {
let span = (self.hi[i] - other.lo[i])
.abs()
.max((other.hi[i] - self.lo[i]).abs());
sum += span * span;
}
sum
}
}
struct Boxed<'a> {
parts: &'a Parts,
segments: Vec<Aabb>,
triangles: Vec<([f64; 3], [f64; 3], [f64; 3])>,
triangle_boxes: Vec<Aabb>,
all: Option<Aabb>,
}
impl<'a> Boxed<'a> {
fn new(parts: &'a Parts) -> Self {
let segments: Vec<Aabb> = parts
.segments
.iter()
.map(|(a, b)| Aabb::point(*a).union(Aabb::point(*b)))
.collect();
let triangles: Vec<_> = parts.triangles().collect();
let triangle_boxes: Vec<Aabb> = triangles
.iter()
.map(|t| {
Aabb::point(t.0)
.union(Aabb::point(t.1))
.union(Aabb::point(t.2))
})
.collect();
let all = parts
.points
.iter()
.map(|p| Aabb::point(*p))
.chain(segments.iter().copied())
.chain(triangle_boxes.iter().copied())
.reduce(Aabb::union);
Self {
parts,
segments,
triangles,
triangle_boxes,
all,
}
}
}
fn no_closer(best: Option<Witness>, lb_sq: f64) -> bool {
best.is_some_and(|w| lb_sq * (1.0 - SLACK) >= w.d * w.d)
}
fn closest(a: &Parts, b: &Parts, stop_at_zero: bool) -> Option<Witness> {
closest_inner(a, b, stop_at_zero, true)
}
fn closest_inner(a: &Parts, b: &Parts, stop_at_zero: bool, prefilter: bool) -> Option<Witness> {
let (a, b) = (Boxed::new(a), Boxed::new(b));
let (Some(_), Some(b_all)) = (a.all, b.all) else {
return None; };
let mut best: Option<Witness> = None;
macro_rules! skip {
($outer:expr, $inner:expr) => {
prefilter && no_closer(best, $outer.min_dist_sq($inner))
};
}
macro_rules! offer {
($w:expr) => {{
let w = $w;
best = Some(match best {
None => w,
Some(seen) => seen.min(w),
});
if stop_at_zero && best.is_some_and(|x| x.d == 0.0) {
return best;
}
}};
}
for &p in &a.parts.points {
let pb = Aabb::point(p);
if skip!(pb, &b_all) {
continue;
}
for &q in &b.parts.points {
if skip!(pb, &Aabb::point(q)) {
continue;
}
offer!(Witness::between(p, q));
}
for (&(s, e), sb) in b.parts.segments.iter().zip(&b.segments) {
if skip!(pb, sb) {
continue;
}
offer!(pt_seg(p, s, e));
}
for (t, tb) in b.triangles.iter().zip(&b.triangle_boxes) {
if skip!(pb, tb) {
continue;
}
offer!(pt_tri(p, t.0, t.1, t.2));
}
}
for (&(s, e), ab) in a.parts.segments.iter().zip(&a.segments) {
if skip!(ab, &b_all) {
continue;
}
for &q in &b.parts.points {
if skip!(ab, &Aabb::point(q)) {
continue;
}
offer!(pt_seg(q, s, e).flipped());
}
for (&(s2, e2), sb) in b.parts.segments.iter().zip(&b.segments) {
if skip!(ab, sb) {
continue;
}
offer!(seg_seg(s, e, s2, e2));
}
for (t, tb) in b.triangles.iter().zip(&b.triangle_boxes) {
if skip!(ab, tb) {
continue;
}
offer!(pt_tri(s, t.0, t.1, t.2));
offer!(pt_tri(e, t.0, t.1, t.2));
for (u, v) in [(t.0, t.1), (t.1, t.2), (t.2, t.0)] {
offer!(seg_seg(s, e, u, v));
}
}
}
for (&t1, ab) in a.triangles.iter().zip(&a.triangle_boxes) {
if skip!(ab, &b_all) {
continue;
}
for &q in &b.parts.points {
if skip!(ab, &Aabb::point(q)) {
continue;
}
offer!(pt_tri(q, t1.0, t1.1, t1.2).flipped());
}
for (&(s2, e2), sb) in b.parts.segments.iter().zip(&b.segments) {
if skip!(ab, sb) {
continue;
}
offer!(pt_tri(s2, t1.0, t1.1, t1.2).flipped());
offer!(pt_tri(e2, t1.0, t1.1, t1.2).flipped());
for (u, v) in [(t1.0, t1.1), (t1.1, t1.2), (t1.2, t1.0)] {
offer!(seg_seg(u, v, s2, e2));
}
}
for (&t2, tb) in b.triangles.iter().zip(&b.triangle_boxes) {
if skip!(ab, tb) {
continue;
}
offer!(tri_tri(t1, t2));
}
}
best
}
fn both(a: &[u8], b: &[u8], func: &'static str) -> Result<Option<(Parts, Parts)>> {
if !geom::has_z_encoded(a)? || !geom::has_z_encoded(b)? {
return Ok(None);
}
Ok(Some((parts(a, func)?, parts(b, func)?)))
}
pub fn st_3d_distance(a: &[u8], b: &[u8]) -> Result<Option<f64>> {
const FUNC: &str = "ST_3DDistance";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_distance(a, b);
};
Ok(closest(&pa, &pb, false).map(|w| w.d))
}
pub fn st_3d_dwithin(a: &[u8], b: &[u8], d: f64) -> Result<bool> {
const FUNC: &str = "ST_3DDWithin";
if d < 0.0 {
return Err(Error::Unsupported {
func: FUNC,
reason: "tolerance cannot be less than zero".into(),
});
}
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_dwithin(a, b, d);
};
Ok(closest(&pa, &pb, false).is_some_and(|w| w.d <= d))
}
pub fn st_3d_intersects(a: &[u8], b: &[u8]) -> Result<bool> {
const FUNC: &str = "ST_3DIntersects";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::predicates::st_intersects(a, b);
};
Ok(closest(&pa, &pb, true).is_some_and(|w| w.d == 0.0))
}
pub fn st_3d_max_distance(a: &[u8], b: &[u8]) -> Result<Option<f64>> {
const FUNC: &str = "ST_3DMaxDistance";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_max_distance(a, b);
};
Ok(farthest(&pa, &pb).map(|w| w.d))
}
fn farthest(a: &Parts, b: &Parts) -> Option<Witness> {
farthest_inner(a, b, true)
}
fn farthest_inner(a: &Parts, b: &Parts, prefilter: bool) -> Option<Witness> {
let b_all = Aabb::of(b.vertices())?;
let mut best: Option<Witness> = None;
for p in a.vertices() {
if prefilter
&& best.is_some_and(|w| Aabb::point(p).max_dist_sq(&b_all) * (1.0 + SLACK) <= w.d * w.d)
{
continue;
}
for q in b.vertices() {
let w = Witness::between(p, q);
best = Some(match best {
None => w,
Some(seen) if w.d > seen.d => w,
Some(seen) => seen,
});
}
}
best
}
pub fn st_3d_dfully_within(a: &[u8], b: &[u8], d: f64) -> Result<bool> {
if d < 0.0 {
return Err(Error::Unsupported {
func: "ST_3DDFullyWithin",
reason: "tolerance cannot be less than zero".into(),
});
}
Ok(st_3d_max_distance(a, b)?.is_some_and(|max| max <= d))
}
pub fn st_3d_closest_point(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DClosestPoint";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::measures::st_closest_point(a, b);
};
let Some(w) = closest(&pa, &pb, false) else {
return Ok(None);
};
point_z(w.a, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_shortest_line(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DShortestLine";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_shortest_line(a, b);
};
let Some(w) = closest(&pa, &pb, false) else {
return Ok(None);
};
line_z(w.a, w.b, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_longest_line(a: &[u8], b: &[u8]) -> Result<Option<Vec<u8>>> {
const FUNC: &str = "ST_3DLongestLine";
let Some((pa, pb)) = both(a, b, FUNC)? else {
return crate::functions::linear::st_longest_line(a, b);
};
let Some(w) = farthest(&pa, &pb) else {
return Ok(None);
};
line_z(w.a, w.b, geom::srid_of(a)?, FUNC).map(Some)
}
pub fn st_3d_line_interpolate_point(bytes: &[u8], fraction: f64) -> Result<Vec<u8>> {
const FUNC: &str = "ST_3DLineInterpolatePoint";
if !(0.0..=1.0).contains(&fraction) {
return Err(Error::Unsupported {
func: FUNC,
reason: "fraction must be between 0 and 1".into(),
});
}
let p = parts(bytes, FUNC)?;
if !p.points.is_empty() || !p.faces.is_empty() || p.segments.is_empty() {
return Err(Error::Unsupported {
func: FUNC,
reason: "the first argument must be a LINESTRING".into(),
});
}
let total: f64 = p.segments.iter().map(|(a, b)| norm(sub(*b, *a))).sum();
if total == 0.0 {
return point_z(p.segments[0].0, geom::srid_of(bytes)?, FUNC);
}
let target = fraction * total;
let mut walked = 0.0;
for (a, b) in &p.segments {
let len = norm(sub(*b, *a));
if walked + len >= target || (a, b) == p.segments.last().map(|(a, b)| (a, b)).unwrap() {
let t = if len == 0.0 {
0.0
} else {
((target - walked) / len).clamp(0.0, 1.0)
};
return point_z(lerp(*a, *b, t), geom::srid_of(bytes)?, FUNC);
}
walked += len;
}
unreachable!("the loop always returns on its last iteration")
}
fn point_z(p: [f64; 3], srid: i32, func: &'static str) -> Result<Vec<u8>> {
let index = coords::ZIndex::at(p[0], p[1], p[2]);
let g = geo_types::Geometry::Point(geo_types::Point::new(p[0], p[1]));
let wkb = coords::write_wkb_z(&g, &index, func)?;
Ok(crate::gpb::write_gpb(&wkb, srid, None, false))
}
fn line_z(a: [f64; 3], b: [f64; 3], srid: i32, func: &'static str) -> Result<Vec<u8>> {
let mut wkb = vec![0x01u8];
wkb.extend_from_slice(&1002u32.to_le_bytes());
wkb.extend_from_slice(&2u32.to_le_bytes());
for p in [a, b] {
for o in p {
wkb.extend_from_slice(&o.to_le_bytes());
}
}
let _ = func;
Ok(crate::gpb::write_gpb(&wkb, srid, None, false))
}
#[cfg(test)]
mod tests {
use super::*;
fn blob(ty: u32, counts: &[usize], coords: &[[f64; 3]]) -> Vec<u8> {
let mut v = vec![0x01u8];
v.extend_from_slice(&(1000 + ty).to_le_bytes());
for c in counts {
v.extend_from_slice(&(*c as u32).to_le_bytes());
}
for c in coords {
for o in c {
v.extend_from_slice(&o.to_le_bytes());
}
}
v
}
fn pt(x: f64, y: f64, z: f64) -> Vec<u8> {
blob(1, &[], &[[x, y, z]])
}
fn line(cs: &[[f64; 3]]) -> Vec<u8> {
blob(2, &[cs.len()], cs)
}
fn poly(cs: &[[f64; 3]]) -> Vec<u8> {
blob(3, &[1, cs.len()], cs)
}
fn square() -> Vec<u8> {
poly(&[
[0., 0., 0.],
[10., 0., 0.],
[10., 10., 0.],
[0., 10., 0.],
[0., 0., 0.],
])
}
fn near(got: Option<f64>, want: f64, what: &str) {
let g = got.unwrap_or(f64::NAN);
assert!((g - want).abs() < 1e-9, "{what}: got {g}, want {want}");
}
#[test]
fn distances_match_the_reference() {
near(
st_3d_distance(&pt(0., 0., 0.), &pt(1., 1., 1.)).unwrap(),
3f64.sqrt(),
"pt/pt",
);
near(
st_3d_distance(&pt(0., 0., 10.), &line(&[[0., 0., 0.], [10., 0., 0.]])).unwrap(),
10.0,
"pt/line",
);
near(
st_3d_distance(
&line(&[[0., 0., 0.], [10., 0., 0.]]),
&line(&[[5., -5., 4.], [5., 5., 4.]]),
)
.unwrap(),
4.0,
"line/line skew",
);
near(
st_3d_distance(&pt(5., 5., 10.), &square()).unwrap(),
10.0,
"pt/face interior",
);
near(
st_3d_distance(
&poly(&[[0., 0., 0.], [1., 0., 0.], [1., 1., 0.], [0., 0., 0.]]),
&poly(&[[0., 0., 5.], [1., 0., 5.], [1., 1., 5.], [0., 0., 5.]]),
)
.unwrap(),
5.0,
"face/face parallel",
);
}
#[test]
fn a_closed_shell_has_no_interior() {
let cube = crate::functions::surface::fixtures::cube(6);
assert!(
!st_3d_intersects(&cube, &pt(0.5, 0.5, 0.5)).unwrap(),
"a point at the centre of a closed cube must not intersect it"
);
assert!(st_3d_intersects(&cube, &pt(0.5, 0.5, 0.0)).unwrap());
near(
st_3d_distance(&cube, &pt(0.5, 0.5, 3.0)).unwrap(),
2.0,
"cube/pt above",
);
}
#[test]
fn the_third_dimension_actually_separates() {
let (a, b) = (
line(&[[0., 0., 0.], [10., 0., 0.]]),
line(&[[5., -5., 4.], [5., 5., 4.]]),
);
assert!(!st_3d_intersects(&a, &b).unwrap());
assert!(crate::functions::predicates::st_intersects(&a, &b).unwrap());
assert!(st_3d_dwithin(&a, &b, 4.0).unwrap());
assert!(!st_3d_dwithin(&a, &b, 3.9).unwrap());
}
#[test]
fn max_distance_is_vertex_to_vertex() {
near(
st_3d_max_distance(&square(), &pt(0., 0., 0.)).unwrap(),
200f64.sqrt(),
"maxdist",
);
assert!(st_3d_dfully_within(&square(), &pt(0., 0., 0.), 14.15).unwrap());
assert!(!st_3d_dfully_within(&square(), &pt(0., 0., 0.), 14.14).unwrap());
}
#[test]
fn the_witnesses_are_the_measured_ones() {
use crate::functions::{rtree, threed};
let l = st_3d_shortest_line(
&line(&[[0., 0., 0.], [10., 0., 0.]]),
&line(&[[5., -5., 4.], [5., 5., 4.]]),
)
.unwrap()
.unwrap();
assert_eq!(threed::st_zmin(&l).unwrap(), Some(0.0));
assert_eq!(threed::st_zmax(&l).unwrap(), Some(4.0));
assert_eq!(rtree::st_min_x(&l).unwrap(), Some(5.0));
let p = st_3d_closest_point(&line(&[[0., 0., 0.], [10., 0., 0.]]), &pt(5., 0., 9.))
.unwrap()
.unwrap();
assert_eq!(threed::st_z(&p).unwrap(), Some(0.0));
assert_eq!(rtree::st_min_x(&p).unwrap(), Some(5.0));
}
#[test]
fn line_interpolate_takes_the_fraction_by_3d_length() {
use crate::functions::{rtree, threed};
let l = line(&[[0., 0., 0.], [10., 0., 10.], [20., 0., 30.]]);
let p = st_3d_line_interpolate_point(&l, 0.5).unwrap();
near(
rtree::st_min_x(&p).unwrap(),
11.837_722_339_831_622,
"3d lip x",
);
near(
threed::st_z(&p).unwrap(),
13.675_444_679_663_242,
"3d lip z",
);
}
#[test]
fn a_missing_z_delegates_to_the_2d_functions() {
let flat = crate::functions::io::st_geom_from_text("POINT(0 0)", None).unwrap();
near(
st_3d_distance(&pt(0., 0., 10.), &flat).unwrap(),
0.0,
"z vs 2d",
);
let two = crate::functions::io::st_geom_from_text("POINT(3 4)", None).unwrap();
near(st_3d_distance(&flat, &two).unwrap(), 5.0, "both 2d");
}
fn parts_of(b: &[u8]) -> Parts {
parts(b, "test").unwrap()
}
fn agrees(a: &Parts, b: &Parts, what: &str) -> Option<Witness> {
let naive = closest_inner(a, b, false, false);
let filtered = closest_inner(a, b, false, true);
match (naive, filtered) {
(None, None) => None,
(Some(n), Some(f)) => {
assert_eq!(
(n.d, n.a, n.b),
(f.d, f.a, f.b),
"{what}: the prefilter changed the winning pair"
);
Some(f)
}
(n, f) => panic!("{what}: naive {n:?} against filtered {f:?}"),
}
}
#[test]
fn nested_boxes_prune_nothing_and_agree() {
let outer = crate::functions::surface::fixtures::cube(6);
let inner = poly(&[
[0.4, 0.4, 0.5],
[0.6, 0.4, 0.5],
[0.6, 0.6, 0.5],
[0.4, 0.6, 0.5],
[0.4, 0.4, 0.5],
]);
let (po, pi) = (parts_of(&outer), parts_of(&inner));
let w = agrees(&po, &pi, "nested").unwrap();
assert!((w.d - 0.4).abs() < 1e-9, "nested distance {}", w.d);
}
#[test]
fn touching_boxes_still_find_the_zero() {
let a = square();
let b = poly(&[
[10., 0., 0.],
[20., 0., 0.],
[20., 10., 0.],
[10., 10., 0.],
[10., 0., 0.],
]);
let (pa, pb) = (parts_of(&a), parts_of(&b));
let w = agrees(&pa, &pb, "touching").unwrap();
assert_eq!(w.d, 0.0);
assert!(st_3d_intersects(&a, &b).unwrap(), "stop_at_zero path");
}
#[test]
fn a_tie_keeps_the_same_witness() {
let p = pt(0., 0., 0.);
let l = line(&[[-5., 5., 0.], [5., 5., 0.], [5., -5., 0.]]);
let (pa, pb) = (parts_of(&p), parts_of(&l));
let w = agrees(&pa, &pb, "tie").unwrap();
assert_eq!(w.d, 5.0);
assert_eq!(w.b, [0., 5., 0.], "the first segment must keep the tie");
let cp = st_3d_closest_point(&l, &p).unwrap().unwrap();
assert_eq!(crate::functions::rtree::st_min_x(&cp).unwrap(), Some(0.0));
let sl = st_3d_shortest_line(&p, &l).unwrap().unwrap();
assert_eq!(crate::functions::rtree::st_max_y(&sl).unwrap(), Some(5.0));
}
#[test]
fn the_max_path_agrees_too() {
let a = crate::functions::surface::fixtures::cube(6);
let b = line(&[[7., 7., 7.], [9., 1., 3.], [2., 8., 6.]]);
let (pa, pb) = (parts_of(&a), parts_of(&b));
let (naive, filtered) = (
farthest_inner(&pa, &pb, false),
farthest_inner(&pa, &pb, true),
);
let (n, f) = (naive.unwrap(), filtered.unwrap());
assert_eq!((n.d, n.a, n.b), (f.d, f.a, f.b), "max witness moved");
let (sq, p) = (parts_of(&square()), parts_of(&pt(5., 5., 0.)));
let (n, f) = (
farthest_inner(&sq, &p, false).unwrap(),
farthest_inner(&sq, &p, true).unwrap(),
);
assert_eq!((n.d, n.a, n.b), (f.d, f.a, f.b), "max tie moved");
}
fn grid(n: usize, dx: f64, dy: f64, dz: f64) -> Parts {
let mut p = Parts::default();
for i in 0..n {
for j in 0..n {
let (x, y) = (i as f64 + dx, j as f64 + dy);
p.faces.push(vec![
[x, y, dz],
[x + 1., y, dz],
[x + 1., y + 1., dz],
[x, y + 1., dz],
[x, y, dz],
]);
}
}
p
}
#[test]
fn two_meshes_are_much_cheaper_with_the_prefilter() {
let (a, b) = (grid(14, 0., 0., 0.), grid(14, 40., 40., 10.));
let t0 = std::time::Instant::now();
let naive = closest_inner(&a, &b, false, false).unwrap();
let naive_ms = t0.elapsed();
let t1 = std::time::Instant::now();
let filtered = closest_inner(&a, &b, false, true).unwrap();
let filtered_ms = t1.elapsed();
assert_eq!(
(naive.d, naive.a, naive.b),
(filtered.d, filtered.a, filtered.b),
"the mesh answer moved"
);
println!("closest: naive {naive_ms:?}, prefiltered {filtered_ms:?}");
assert!(
filtered_ms <= naive_ms,
"the prefilter should not cost more: naive {naive_ms:?}, filtered {filtered_ms:?}"
);
}
#[test]
fn a_vertical_wall_is_not_ambiguous_here() {
let wall = poly(&[
[0., 0., 0.],
[0., 0., 10.],
[10., 0., 10.],
[10., 0., 0.],
[0., 0., 0.],
]);
near(
st_3d_distance(&wall, &pt(0., 5., 5.)).unwrap(),
5.0,
"wall/pt",
);
}
}