use core::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use axiolid_contracts::Sign;
use axiolid_core::Point2;
use axiolid_overlay::Polygon;
use crate::map::{admissible, free_triangles, meets_triangle, outside};
use crate::{
crosses, side, validate_region, DistanceMap, FarthestError, LengthInterval, RouteError,
MAX_CELLS,
};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ForcedWalk {
pub length: LengthInterval,
pub shortest: f64,
pub witness: Option<Point2>,
pub converged: bool,
pub cells: usize,
}
pub fn forced_walk(
from: &DistanceMap,
to: &DistanceMap,
through: &Polygon,
tolerance: f64,
) -> Result<ForcedWalk, FarthestError> {
forced_walk_within(from, to, through, tolerance, MAX_CELLS)
}
#[derive(Debug, Clone, Copy)]
struct Cell {
corners: [Point2; 3],
root: usize,
anchor: (Point2, f64),
lower: f64,
depth: u32,
order: usize,
}
impl PartialEq for Cell {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Cell {}
impl PartialOrd for Cell {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Cell {
fn cmp(&self, other: &Self) -> Ordering {
other
.lower
.total_cmp(&self.lower)
.then(other.order.cmp(&self.order))
}
}
pub fn forced_walk_within(
from: &DistanceMap,
to: &DistanceMap,
through: &Polygon,
tolerance: f64,
max_cells: usize,
) -> Result<ForcedWalk, FarthestError> {
if !(tolerance.is_finite() && tolerance >= 0.0) {
return Err(FarthestError::InvalidTolerance);
}
if !from.same_space(to) {
return Err(FarthestError::MismatchedMaps);
}
validate_region(core::slice::from_ref(through), &[])?;
let roots = free_triangles(from)?;
let relative = (from.hops() as f64 + to.hops() as f64 + 16.0) * 2.0 * f64::EPSILON;
let scale = from
.nodes()
.iter()
.chain(through.outer.points.iter())
.fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
let slack = |depth: u32, radius: f64| {
2.0 * (f64::from(depth + 4) * 4.0 * f64::EPSILON * scale + 4.0 * f64::EPSILON * radius)
};
let mut shortest = f64::INFINITY;
for (&origin, &weight) in from.sites().iter().zip(from.weights()) {
if let Some(d) = to.at(origin)? {
shortest = shortest.min(weight + d);
}
}
let floor = shortest * (1.0 - relative);
let vertices = |map: &DistanceMap| -> Result<Vec<Vertex>, RouteError> {
map.nodes()
.iter()
.copied()
.zip(map.vertex_distances())
.filter(|(_, d)| d.is_finite())
.map(|(at, distance)| {
Ok(Vertex {
at,
distance,
solid: solid_corner(map.region(), map.obstacles(), at)?,
})
})
.collect()
};
let mut search = Search {
from,
to,
through,
from_vertices: vertices(from)?,
to_vertices: vertices(to)?,
roots: &roots,
evaluated: HashMap::new(),
upper: None,
};
let mut heap = BinaryHeap::new();
let mut cells = 0usize;
let mut met = false;
for (root, corners) in roots.iter().enumerate() {
if outside(through, corners)? {
continue;
}
met = true;
cells += 1;
match search.cell(*corners, root, None, 0, cells, floor, &slack)? {
Anchored::Cell(cell) => heap.push(cell),
Anchored::Unreached => {}
Anchored::None => return Err(FarthestError::Triangulation),
}
}
if !met {
return Err(FarthestError::Empty);
}
let best = |search: &Search| search.upper.map_or(f64::INFINITY, |(d, _)| d);
while let Some(top) = heap.peek() {
if top.lower >= best(&search) - tolerance || cells >= max_cells {
break;
}
let cell = heap.pop().expect("peeked");
let [a, b, c] = cell.corners;
let mid = |p: Point2, q: Point2| Point2::new(0.5 * p.x + 0.5 * q.x, 0.5 * p.y + 0.5 * q.y);
let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
for corners in [[a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]] {
if outside(through, &corners)? {
continue;
}
cells += 1;
let child = search.cell(
corners,
cell.root,
Some(cell.anchor),
cell.depth + 1,
cells,
floor,
&slack,
)?;
if let Anchored::Cell(child) = child {
if child.lower < best(&search) {
heap.push(child);
}
}
}
}
let Some((upper, witness)) = search.upper else {
if heap.is_empty() {
return Ok(ForcedWalk {
length: LengthInterval {
lower: f64::INFINITY,
upper: f64::INFINITY,
},
shortest,
witness: None,
converged: true,
cells,
});
}
let lower = heap.peek().map_or(floor, |c| c.lower.max(floor));
return Ok(ForcedWalk {
length: LengthInterval {
lower: lower * (1.0 - relative),
upper: f64::INFINITY,
},
shortest,
witness: None,
converged: false,
cells,
});
};
let lower = heap.peek().map_or(upper, |c| c.lower.min(upper)).max(floor);
Ok(ForcedWalk {
length: LengthInterval {
lower: (lower * (1.0 - relative)).min(upper),
upper: upper * (1.0 + relative),
},
shortest,
witness: Some(witness),
converged: upper - lower <= tolerance,
cells,
})
}
enum Anchored {
Cell(Cell),
Unreached,
None,
}
struct Search<'a> {
from: &'a DistanceMap,
to: &'a DistanceMap,
through: &'a Polygon,
from_vertices: Vec<Vertex>,
to_vertices: Vec<Vertex>,
roots: &'a [[Point2; 3]],
evaluated: HashMap<(u64, u64), Option<f64>>,
upper: Option<(f64, Point2)>,
}
impl Search<'_> {
fn pair_bound(&self, t: &[Point2; 3]) -> Result<f64, RouteError> {
let origins = candidates(&self.from_vertices, t, self.from.obstacles())?;
let targets = candidates(&self.to_vertices, t, self.to.obstacles())?;
let Some(&(least, ..)) = targets.first() else {
return Ok(f64::INFINITY);
};
let mut best = f64::INFINITY;
for &(s, u, du, gu) in &origins {
if s + least >= best {
break;
}
for &(r, v, dv, gv) in &targets {
if s + r >= best {
break;
}
best = best.min(du + dv + (u - v).length().max(gu + gv));
}
}
Ok(best)
}
fn walk(&mut self, p: Point2) -> Result<Option<f64>, FarthestError> {
let key = (p.x.to_bits(), p.y.to_bits());
if let Some(d) = self.evaluated.get(&key) {
return Ok(*d);
}
let d = match (self.from.at(p)?, self.to.at(p)?) {
(Some(a), Some(b)) => Some(a + b),
_ => None,
};
self.evaluated.insert(key, d);
Ok(d)
}
#[allow(clippy::too_many_arguments)]
fn cell(
&mut self,
corners: [Point2; 3],
root: usize,
inherited: Option<(Point2, f64)>,
depth: u32,
order: usize,
floor: f64,
slack: &dyn Fn(u32, f64) -> f64,
) -> Result<Anchored, FarthestError> {
let centroid = Point2::new(
(corners[0].x + corners[1].x + corners[2].x) / 3.0,
(corners[0].y + corners[1].y + corners[2].y) / 3.0,
);
let radius = |a: Point2| {
corners
.iter()
.map(|c| (*c - a).length())
.fold(0.0, f64::max)
};
let mut best: Option<(f64, (Point2, f64))> =
inherited.map(|(a, f)| (lipschitz_lower(f, radius(a)), (a, f)));
for a in [corners[0], corners[1], corners[2], centroid] {
if !admissible(self.from, &self.roots[root], a)? {
continue;
}
let Some(f) = self.walk(a)? else {
return Ok(Anchored::Unreached);
};
if crate::map::in_polygon(self.through, a)? && self.upper.is_none_or(|(u, _)| f < u) {
self.upper = Some((f, a));
}
let bound = lipschitz_lower(f, radius(a));
if best.is_none_or(|(b, _)| bound > b) {
best = Some((bound, (a, f)));
}
}
let pairs = self.pair_bound(&corners)?;
Ok(match best {
Some((bound, anchor)) => Anchored::Cell(Cell {
corners,
root,
anchor,
lower: (bound.max(pairs) - slack(depth, radius(anchor.0))).max(floor),
depth,
order,
}),
None => Anchored::None,
})
}
}
fn candidates(
vertices: &[Vertex],
t: &[Point2; 3],
obstacles: &[(Point2, Point2)],
) -> Result<Vec<(f64, Point2, f64, f64)>, RouteError> {
let mut out = Vec::new();
'vertex: for vertex in vertices {
let (v, d) = (vertex.at, vertex.distance);
if let Some((a, b)) = vertex.solid {
let inside = |c: Point2| -> Result<bool, RouteError> {
let (ab, ac) = (side(v, a, b)?, side(v, a, c)?);
let (ba, bc) = (side(v, b, a)?, side(v, b, c)?);
Ok(ac != Sign::Zero && ac == ab && bc != Sign::Zero && bc == ba)
};
if inside(t[0])? && inside(t[1])? && inside(t[2])? {
continue 'vertex;
}
}
for &(p, q) in obstacles {
if crosses(v, t[0], p, q)? && crosses(v, t[1], p, q)? && crosses(v, t[2], p, q)? {
continue 'vertex;
}
}
let g = triangle_distance(t, v)?;
out.push((g + d, v, d, g));
}
out.sort_by(|a, b| a.0.total_cmp(&b.0));
Ok(out)
}
fn lipschitz_lower(f: f64, radius: f64) -> f64 {
f - 2.0 * radius
}
struct Vertex {
at: Point2,
distance: f64,
solid: Option<(Point2, Point2)>,
}
fn solid_corner(
region: &[Polygon],
obstacles: &[(Point2, Point2)],
v: Point2,
) -> Result<Option<(Point2, Point2)>, RouteError> {
if obstacles.iter().filter(|(p, q)| *p == v || *q == v).count() != 2 {
return Ok(None);
}
for polygon in region {
for (hole, ring) in
core::iter::once((false, &polygon.outer)).chain(polygon.holes.iter().map(|h| (true, h)))
{
let n = ring.points.len();
let Some(i) = ring.points.iter().position(|p| *p == v) else {
continue;
};
let (prev, next) = (ring.points[(i + n - 1) % n], ring.points[(i + 1) % n]);
let turn = side(prev, v, next)?;
if turn == Sign::Zero {
return Ok(None);
}
let k = (0..n)
.min_by(|&a, &b| {
let (p, q) = (ring.points[a], ring.points[b]);
p.y.total_cmp(&q.y).then(p.x.total_cmp(&q.x))
})
.expect("a ring has corners");
let orientation = side(
ring.points[(k + n - 1) % n],
ring.points[k],
ring.points[(k + 1) % n],
)?;
let small_is_inside = turn == orientation;
return Ok((small_is_inside == hole).then_some((prev, next)));
}
}
Ok(None)
}
fn triangle_distance(t: &[Point2; 3], v: Point2) -> Result<f64, RouteError> {
if meets_triangle(v, v, t)? {
return Ok(0.0);
}
Ok((0..3)
.map(|i| segment_distance(t[i], t[(i + 1) % 3], v))
.fold(f64::INFINITY, f64::min))
}
fn segment_distance(a: Point2, b: Point2, v: Point2) -> f64 {
let d = b - a;
let length2 = d.dot(d);
let s = if length2 > 0.0 {
((v - a).dot(d) / length2).clamp(0.0, 1.0)
} else {
0.0
};
(v - (a + d * s)).length()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::distance_map;
use axiolid_overlay::Ring;
fn p(x: f64, y: f64) -> Point2 {
Point2::new(x, y)
}
fn ring(points: &[(f64, f64)]) -> Ring {
Ring {
points: points.iter().map(|(x, y)| p(*x, *y)).collect(),
}
}
fn two_holes() -> Polygon {
Polygon {
outer: ring(&[(0.0, 0.0), (12.0, 0.0), (12.0, 8.0), (0.0, 8.0)]),
holes: vec![
ring(&[(2.0, 2.0), (5.0, 2.0), (5.0, 6.0), (2.0, 6.0)]),
ring(&[(7.0, 2.0), (10.0, 2.0), (10.0, 6.0), (7.0, 6.0)]),
],
}
}
fn vertices(map: &DistanceMap) -> Vec<Vertex> {
map.nodes()
.iter()
.copied()
.zip(map.vertex_distances())
.filter(|(_, d)| d.is_finite())
.map(|(at, distance)| Vertex {
at,
distance,
solid: solid_corner(map.region(), map.obstacles(), at).unwrap(),
})
.collect()
}
#[test]
fn the_lipschitz_bound_allows_both_distances_to_fall_together() {
let room = [Polygon {
outer: ring(&[(-10.0, -10.0), (10.0, -10.0), (10.0, 10.0), (-10.0, 10.0)]),
holes: Vec::new(),
}];
let map = distance_map(&room, &[], &[p(0.0, 0.0)]).unwrap();
let a = p(5.0, 0.0);
let f = 2.0 * map.at(a).unwrap().unwrap();
let y = p(4.0, 0.0);
let least = 2.0 * map.at(y).unwrap().unwrap();
assert!(lipschitz_lower(f, 1.0) <= least);
assert_eq!(lipschitz_lower(f, 1.0), least);
}
#[test]
fn the_pair_bound_is_the_least_over_every_pair() {
let region = [two_holes()];
let from = distance_map(®ion, &[], &[p(1.0, 1.0), p(6.0, 7.5)]).unwrap();
let to = distance_map(®ion, &[], &[p(11.0, 7.0), p(6.0, 0.5)]).unwrap();
let search = Search {
from: &from,
to: &to,
through: ®ion[0],
from_vertices: vertices(&from),
to_vertices: vertices(&to),
roots: &[],
evaluated: HashMap::new(),
upper: None,
};
let mut checked = 0;
for i in 0..24 {
for j in 0..16 {
let (x, y) = (0.25 + 0.5 * f64::from(i), 0.25 + 0.5 * f64::from(j));
let t = [p(x, y), p(x + 0.4, y), p(x, y + 0.4)];
let origins = candidates(&search.from_vertices, &t, from.obstacles()).unwrap();
let targets = candidates(&search.to_vertices, &t, to.obstacles()).unwrap();
let mut brute = f64::INFINITY;
for &(_, u, du, gu) in &origins {
for &(_, v, dv, gv) in &targets {
brute = brute.min(du + dv + (u - v).length().max(gu + gv));
}
}
assert_eq!(search.pair_bound(&t).unwrap(), brute, "{t:?}");
checked += 1;
}
}
assert_eq!(checked, 384);
}
#[test]
fn a_vertex_is_hidden_only_from_the_whole_cell() {
let region = [two_holes()];
let map = distance_map(®ion, &[], &[p(1.0, 1.0)]).unwrap();
let find = |at: Point2| {
vertices(&map)
.into_iter()
.find(|v| v.at == at)
.expect("a vertex")
};
let hidden = |v: &Vertex, t: [Point2; 3]| {
!candidates(core::slice::from_ref(v), &t, map.obstacles())
.unwrap()
.iter()
.any(|c| c.1 == v.at)
};
let origin = find(p(1.0, 1.0));
let behind = [p(6.0, 4.5), p(6.5, 4.5), p(6.0, 5.0)];
assert!(hidden(&origin, behind));
let peeking = [p(6.0, 4.5), p(6.5, 1.5), p(6.0, 5.0)];
assert!(!hidden(&origin, peeking));
let corner = find(p(5.0, 2.0));
assert!(corner.solid.is_some());
let inside = [p(4.5, 2.5), p(4.8, 2.5), p(4.5, 2.8)];
assert!(hidden(&corner, inside));
let straddling = [p(4.5, 2.5), p(5.5, 2.5), p(4.5, 2.8)];
assert!(!hidden(&corner, straddling));
assert!(find(p(0.0, 0.0)).solid.is_none());
}
}