1use core::cmp::Ordering;
68use std::collections::{BinaryHeap, HashMap};
69
70use axiolid_contracts::Sign;
71use axiolid_core::Point2;
72use axiolid_overlay::Polygon;
73
74use crate::map::{
75 admissible, free_triangles, free_triangles_in, in_polygon, in_triangle, meets_triangle, outside,
76};
77use crate::{
78 crosses, side, validate_region, within, DistanceMap, FarthestError, LengthInterval, RouteError,
79 WeightedMap, MAX_CELLS,
80};
81
82#[derive(Debug, Clone, Copy, PartialEq)]
84#[non_exhaustive]
85pub struct ForcedWalk {
86 pub length: LengthInterval,
90 pub shortest: f64,
94 pub witness: Option<Point2>,
98 pub converged: bool,
101 pub cells: usize,
103}
104
105pub fn forced_walk(
118 from: &DistanceMap,
119 to: &DistanceMap,
120 through: &Polygon,
121 tolerance: f64,
122) -> Result<ForcedWalk, FarthestError> {
123 forced_walk_within(from, to, through, tolerance, MAX_CELLS)
124}
125
126#[derive(Debug, Clone, Copy)]
129struct Cell {
130 corners: [Point2; 3],
131 root: usize,
132 anchor: (Point2, f64),
133 lower: f64,
134 depth: u32,
135 order: usize,
136}
137
138impl PartialEq for Cell {
139 fn eq(&self, other: &Self) -> bool {
140 self.cmp(other) == Ordering::Equal
141 }
142}
143
144impl Eq for Cell {}
145
146impl PartialOrd for Cell {
147 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
148 Some(self.cmp(other))
149 }
150}
151
152impl Ord for Cell {
153 fn cmp(&self, other: &Self) -> Ordering {
156 other
157 .lower
158 .total_cmp(&self.lower)
159 .then(other.order.cmp(&self.order))
160 }
161}
162
163pub fn forced_walk_within(
169 from: &DistanceMap,
170 to: &DistanceMap,
171 through: &Polygon,
172 tolerance: f64,
173 max_cells: usize,
174) -> Result<ForcedWalk, FarthestError> {
175 if !(tolerance.is_finite() && tolerance >= 0.0) {
176 return Err(FarthestError::InvalidTolerance);
177 }
178 if !from.same_space(to) {
179 return Err(FarthestError::MismatchedMaps);
180 }
181 validate_region(core::slice::from_ref(through), &[])?;
182 let roots = free_triangles(from)?;
183 let relative = (from.hops() as f64 + to.hops() as f64 + 16.0) * 2.0 * f64::EPSILON;
185 let scale = from
186 .nodes()
187 .iter()
188 .chain(through.outer.points.iter())
189 .fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
190 let slack = |depth: u32, radius: f64| {
192 2.0 * (f64::from(depth + 4) * 4.0 * f64::EPSILON * scale + 4.0 * f64::EPSILON * radius)
193 };
194 let mut shortest = f64::INFINITY;
196 for (&origin, &weight) in from.sites().iter().zip(from.weights()) {
197 if let Some(d) = to.at(origin)? {
198 shortest = shortest.min(weight + d);
199 }
200 }
201 let floor = shortest * (1.0 - relative);
202 let vertices = |map: &DistanceMap| -> Result<Vec<Vertex>, RouteError> {
203 map.nodes()
204 .iter()
205 .copied()
206 .zip(map.vertex_distances())
207 .filter(|(_, d)| d.is_finite())
208 .map(|(at, distance)| {
209 Ok(Vertex {
210 at,
211 distance,
212 solid: solid_corner(map.region(), map.obstacles(), at)?,
213 })
214 })
215 .collect()
216 };
217 let mut search = Search {
218 from,
219 to,
220 through,
221 from_vertices: vertices(from)?,
222 to_vertices: vertices(to)?,
223 roots: &roots,
224 evaluated: HashMap::new(),
225 upper: None,
226 };
227 let mut heap = BinaryHeap::new();
228 let mut cells = 0usize;
229 let mut met = false;
230 for (root, corners) in roots.iter().enumerate() {
231 if outside(through, corners)? {
232 continue;
233 }
234 met = true;
235 cells += 1;
236 match search.cell(*corners, root, None, 0, cells, floor, &slack)? {
237 Anchored::Cell(cell) => heap.push(cell),
238 Anchored::Unreached => {}
240 Anchored::None => return Err(FarthestError::Triangulation),
241 }
242 }
243 if !met {
244 return Err(FarthestError::Empty);
245 }
246 let best = |search: &Search| search.upper.map_or(f64::INFINITY, |(d, _)| d);
247 while let Some(top) = heap.peek() {
248 if top.lower >= best(&search) - tolerance || cells >= max_cells {
249 break;
250 }
251 let cell = heap.pop().expect("peeked");
252 let [a, b, c] = cell.corners;
253 let mid = |p: Point2, q: Point2| Point2::new(0.5 * p.x + 0.5 * q.x, 0.5 * p.y + 0.5 * q.y);
254 let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
255 for corners in [[a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]] {
256 if outside(through, &corners)? {
257 continue;
258 }
259 cells += 1;
260 let child = search.cell(
261 corners,
262 cell.root,
263 Some(cell.anchor),
264 cell.depth + 1,
265 cells,
266 floor,
267 &slack,
268 )?;
269 if let Anchored::Cell(child) = child {
270 if child.lower < best(&search) {
271 heap.push(child);
272 }
273 }
274 }
275 }
276 let Some((upper, witness)) = search.upper else {
277 if heap.is_empty() {
278 return Ok(ForcedWalk {
280 length: LengthInterval {
281 lower: f64::INFINITY,
282 upper: f64::INFINITY,
283 },
284 shortest,
285 witness: None,
286 converged: true,
287 cells,
288 });
289 }
290 let lower = heap.peek().map_or(floor, |c| c.lower.max(floor));
291 return Ok(ForcedWalk {
292 length: LengthInterval {
293 lower: lower * (1.0 - relative),
294 upper: f64::INFINITY,
295 },
296 shortest,
297 witness: None,
298 converged: false,
299 cells,
300 });
301 };
302 let lower = heap.peek().map_or(upper, |c| c.lower.min(upper)).max(floor);
303 Ok(ForcedWalk {
304 length: LengthInterval {
305 lower: (lower * (1.0 - relative)).min(upper),
306 upper: upper * (1.0 + relative),
307 },
308 shortest,
309 witness: Some(witness),
310 converged: upper - lower <= tolerance,
311 cells,
312 })
313}
314
315enum Anchored {
316 Cell(Cell),
317 Unreached,
319 None,
322}
323
324struct Search<'a> {
325 from: &'a DistanceMap,
326 to: &'a DistanceMap,
327 through: &'a Polygon,
328 from_vertices: Vec<Vertex>,
330 to_vertices: Vec<Vertex>,
331 roots: &'a [[Point2; 3]],
332 evaluated: HashMap<(u64, u64), Option<f64>>,
333 upper: Option<(f64, Point2)>,
335}
336
337impl Search<'_> {
338 fn pair_bound(&self, t: &[Point2; 3]) -> Result<f64, RouteError> {
342 let origins = candidates(&self.from_vertices, t, self.from.obstacles())?;
343 let targets = candidates(&self.to_vertices, t, self.to.obstacles())?;
344 let Some(&(least, ..)) = targets.first() else {
345 return Ok(f64::INFINITY);
346 };
347 let mut best = f64::INFINITY;
348 for &(s, u, du, gu) in &origins {
349 if s + least >= best {
350 break;
351 }
352 for &(r, v, dv, gv) in &targets {
353 if s + r >= best {
354 break;
355 }
356 best = best.min(du + dv + (u - v).length().max(gu + gv));
357 }
358 }
359 Ok(best)
360 }
361
362 fn walk(&mut self, p: Point2) -> Result<Option<f64>, FarthestError> {
364 let key = (p.x.to_bits(), p.y.to_bits());
365 if let Some(d) = self.evaluated.get(&key) {
366 return Ok(*d);
367 }
368 let d = match (self.from.at(p)?, self.to.at(p)?) {
369 (Some(a), Some(b)) => Some(a + b),
370 _ => None,
371 };
372 self.evaluated.insert(key, d);
373 Ok(d)
374 }
375
376 #[allow(clippy::too_many_arguments)]
377 fn cell(
378 &mut self,
379 corners: [Point2; 3],
380 root: usize,
381 inherited: Option<(Point2, f64)>,
382 depth: u32,
383 order: usize,
384 floor: f64,
385 slack: &dyn Fn(u32, f64) -> f64,
386 ) -> Result<Anchored, FarthestError> {
387 let centroid = Point2::new(
388 (corners[0].x + corners[1].x + corners[2].x) / 3.0,
389 (corners[0].y + corners[1].y + corners[2].y) / 3.0,
390 );
391 let radius = |a: Point2| {
392 corners
393 .iter()
394 .map(|c| (*c - a).length())
395 .fold(0.0, f64::max)
396 };
397 let mut best: Option<(f64, (Point2, f64))> =
398 inherited.map(|(a, f)| (lipschitz_lower(f, radius(a)), (a, f)));
399 for a in [corners[0], corners[1], corners[2], centroid] {
400 if !admissible(self.from, &self.roots[root], a)? {
401 continue;
402 }
403 let Some(f) = self.walk(a)? else {
405 return Ok(Anchored::Unreached);
406 };
407 if crate::map::in_polygon(self.through, a)? && self.upper.is_none_or(|(u, _)| f < u) {
408 self.upper = Some((f, a));
409 }
410 let bound = lipschitz_lower(f, radius(a));
411 if best.is_none_or(|(b, _)| bound > b) {
412 best = Some((bound, (a, f)));
413 }
414 }
415 let pairs = self.pair_bound(&corners)?;
416 Ok(match best {
417 Some((bound, anchor)) => Anchored::Cell(Cell {
418 corners,
419 root,
420 anchor,
421 lower: (bound.max(pairs) - slack(depth, radius(anchor.0))).max(floor),
422 depth,
423 order,
424 }),
425 None => Anchored::None,
426 })
427 }
428}
429
430fn candidates(
436 vertices: &[Vertex],
437 t: &[Point2; 3],
438 obstacles: &[(Point2, Point2)],
439) -> Result<Vec<(f64, Point2, f64, f64)>, RouteError> {
440 let mut out = Vec::new();
441 'vertex: for vertex in vertices {
442 let (v, d) = (vertex.at, vertex.distance);
443 if let Some((a, b)) = vertex.solid {
444 let inside = |c: Point2| -> Result<bool, RouteError> {
446 let (ab, ac) = (side(v, a, b)?, side(v, a, c)?);
447 let (ba, bc) = (side(v, b, a)?, side(v, b, c)?);
448 Ok(ac != Sign::Zero && ac == ab && bc != Sign::Zero && bc == ba)
449 };
450 if inside(t[0])? && inside(t[1])? && inside(t[2])? {
451 continue 'vertex;
452 }
453 }
454 for &(p, q) in obstacles {
455 if crosses(v, t[0], p, q)? && crosses(v, t[1], p, q)? && crosses(v, t[2], p, q)? {
456 continue 'vertex;
457 }
458 }
459 let g = triangle_distance(t, v)?;
460 out.push((g + d, v, d, g));
461 }
462 out.sort_by(|a, b| a.0.total_cmp(&b.0));
463 Ok(out)
464}
465
466fn lipschitz_lower(f: f64, radius: f64) -> f64 {
469 f - 2.0 * radius
470}
471
472struct Vertex {
474 at: Point2,
475 distance: f64,
476 solid: Option<(Point2, Point2)>,
481}
482
483fn solid_corner(
487 region: &[Polygon],
488 obstacles: &[(Point2, Point2)],
489 v: Point2,
490) -> Result<Option<(Point2, Point2)>, RouteError> {
491 if obstacles.iter().filter(|(p, q)| *p == v || *q == v).count() != 2 {
492 return Ok(None);
493 }
494 for polygon in region {
495 for (hole, ring) in
496 core::iter::once((false, &polygon.outer)).chain(polygon.holes.iter().map(|h| (true, h)))
497 {
498 let n = ring.points.len();
499 let Some(i) = ring.points.iter().position(|p| *p == v) else {
500 continue;
501 };
502 let (prev, next) = (ring.points[(i + n - 1) % n], ring.points[(i + 1) % n]);
503 let turn = side(prev, v, next)?;
504 if turn == Sign::Zero {
505 return Ok(None);
506 }
507 let k = (0..n)
510 .min_by(|&a, &b| {
511 let (p, q) = (ring.points[a], ring.points[b]);
512 p.y.total_cmp(&q.y).then(p.x.total_cmp(&q.x))
513 })
514 .expect("a ring has corners");
515 let orientation = side(
516 ring.points[(k + n - 1) % n],
517 ring.points[k],
518 ring.points[(k + 1) % n],
519 )?;
520 let small_is_inside = turn == orientation;
523 return Ok((small_is_inside == hole).then_some((prev, next)));
524 }
525 }
526 Ok(None)
527}
528
529fn triangle_distance(t: &[Point2; 3], v: Point2) -> Result<f64, RouteError> {
532 if meets_triangle(v, v, t)? {
533 return Ok(0.0);
534 }
535 Ok((0..3)
536 .map(|i| segment_distance(t[i], t[(i + 1) % 3], v))
537 .fold(f64::INFINITY, f64::min))
538}
539
540fn segment_distance(a: Point2, b: Point2, v: Point2) -> f64 {
541 let d = b - a;
542 let length2 = d.dot(d);
543 let s = if length2 > 0.0 {
544 ((v - a).dot(d) / length2).clamp(0.0, 1.0)
545 } else {
546 0.0
547 };
548 (v - (a + d * s)).length()
549}
550
551#[derive(Debug, Clone, Copy, PartialEq)]
554#[non_exhaustive]
555pub struct WeightedForcedWalk {
556 pub cost: LengthInterval,
560 pub shortest: LengthInterval,
564 pub witness: Option<Point2>,
568 pub converged: bool,
572 pub cells: usize,
574}
575
576pub fn weighted_forced_walk(
589 from: &WeightedMap,
590 to: &WeightedMap,
591 through: &Polygon,
592 tolerance: f64,
593) -> Result<WeightedForcedWalk, FarthestError> {
594 weighted_forced_walk_within(from, to, through, tolerance, MAX_CELLS)
595}
596
597struct Span {
600 a: Point2,
601 b: Point2,
602 least: f64,
603 solid: Option<(Point2, Point2)>,
604}
605
606pub fn weighted_forced_walk_within(
612 from: &WeightedMap,
613 to: &WeightedMap,
614 through: &Polygon,
615 tolerance: f64,
616 max_cells: usize,
617) -> Result<WeightedForcedWalk, FarthestError> {
618 if !(tolerance.is_finite() && tolerance >= 0.0) {
619 return Err(FarthestError::InvalidTolerance);
620 }
621 if !from.same_space(to) {
622 return Err(FarthestError::MismatchedMaps);
623 }
624 validate_region(core::slice::from_ref(through), &[])?;
625 let roots = free_triangles_in(from.region(), from.obstacles())?;
626 let steep: Vec<f64> = roots
627 .iter()
628 .map(|t| from.steepest(t))
629 .collect::<Result<_, _>>()?;
630 let relative = 16.0 * 2.0 * f64::EPSILON;
632 let scale = from
633 .region()
634 .iter()
635 .flat_map(|p| p.outer.points.iter())
636 .chain(through.outer.points.iter())
637 .fold(0.0f64, |m, p| m.max(p.x.abs()).max(p.y.abs()));
638 let slack = |depth: u32, radius: f64, k: f64| {
639 2.0 * k * (f64::from(depth + 4) * 4.0 * f64::EPSILON * scale + 4.0 * f64::EPSILON * radius)
640 };
641 let mut shortest = LengthInterval {
642 lower: f64::INFINITY,
643 upper: f64::INFINITY,
644 };
645 for (origin, weight) in from.seeded() {
646 if let Some((lo, hi)) = to.bracket(origin)? {
647 shortest.lower = shortest.lower.min(weight + lo);
648 shortest.upper = shortest.upper.min(weight + hi);
649 }
650 }
651 let floor = shortest.lower * (1.0 - relative);
652 let spans = |map: &WeightedMap| -> Result<Vec<Span>, RouteError> {
653 map.spans()
654 .into_iter()
655 .map(|((a, b), least)| {
656 Ok(Span {
657 a,
658 b,
659 least,
660 solid: if a == b {
661 solid_corner(map.region(), map.obstacles(), a)?
662 } else {
663 None
664 },
665 })
666 })
667 .collect()
668 };
669 let (from_spans, to_spans) = (spans(from)?, spans(to)?);
670 let mut evaluated: HashMap<(u64, u64), Option<(f64, f64)>> = HashMap::new();
671 let mut upper: Option<(f64, Point2, f64)> = None;
674 let mut cell = |corners: [Point2; 3],
675 root: usize,
676 inherited: Option<(Point2, f64)>,
677 depth: u32,
678 order: usize,
679 upper: &mut Option<(f64, Point2, f64)>|
680 -> Result<Anchored, FarthestError> {
681 let centroid = Point2::new(
682 (corners[0].x + corners[1].x + corners[2].x) / 3.0,
683 (corners[0].y + corners[1].y + corners[2].y) / 3.0,
684 );
685 let radius = |a: Point2| {
686 corners
687 .iter()
688 .map(|c| (*c - a).length())
689 .fold(0.0, f64::max)
690 };
691 let k = steep[root];
692 let mut best: Option<(f64, (Point2, f64))> =
693 inherited.map(|(a, f)| (weighted_lipschitz_lower(f, radius(a), k), (a, f)));
694 for a in [corners[0], corners[1], corners[2], centroid] {
695 if !in_triangle(&roots[root], a)? {
696 continue;
697 }
698 if from.walls().iter().try_fold(false, |m, &(p, q)| {
699 Ok::<_, RouteError>(m || (within(p, q, a) && side(p, q, a)? == Sign::Zero))
700 })? {
701 continue;
702 }
703 let key = (a.x.to_bits(), a.y.to_bits());
704 let bracket = match evaluated.get(&key) {
705 Some(b) => *b,
706 None => {
707 let b = match (from.bracket(a)?, to.bracket(a)?) {
708 (Some((l1, h1)), Some((l2, h2))) => Some((l1 + l2, h1 + h2)),
709 _ => None,
710 };
711 evaluated.insert(key, b);
712 b
713 }
714 };
715 let Some((lo, hi)) = bracket else {
717 return Ok(Anchored::Unreached);
718 };
719 if in_polygon(through, a)? && upper.is_none_or(|(u, ..)| hi < u) {
720 *upper = Some((hi, a, hi - lo));
721 }
722 let bound = weighted_lipschitz_lower(lo, radius(a), k);
723 if best.is_none_or(|(b, _)| bound > b) {
724 best = Some((bound, (a, lo)));
725 }
726 }
727 let pairs = weighted_pair_bound(from, &from_spans, &to_spans, &corners)?;
728 Ok(match best {
729 Some((bound, anchor)) => Anchored::Cell(Cell {
730 corners,
731 root,
732 anchor,
733 lower: (bound.max(pairs) - slack(depth, radius(anchor.0), k)).max(floor),
734 depth,
735 order,
736 }),
737 None => Anchored::None,
738 })
739 };
740 let mut heap = BinaryHeap::new();
741 let mut cells = 0usize;
742 let mut met = false;
743 for (root, corners) in roots.iter().enumerate() {
744 if outside(through, corners)? {
745 continue;
746 }
747 met = true;
748 cells += 1;
749 match cell(*corners, root, None, 0, cells, &mut upper)? {
750 Anchored::Cell(c) => heap.push(c),
751 Anchored::Unreached => {}
752 Anchored::None => return Err(FarthestError::Triangulation),
753 }
754 }
755 if !met {
756 return Err(FarthestError::Empty);
757 }
758 let goal = |upper: &Option<(f64, Point2, f64)>| {
761 upper.map_or(f64::INFINITY, |(u, _, width)| u - tolerance - width)
762 };
763 while let Some(top) = heap.peek() {
764 if top.lower >= goal(&upper) || cells >= max_cells {
765 break;
766 }
767 let parent = heap.pop().expect("peeked");
768 let [a, b, c] = parent.corners;
769 let mid = |p: Point2, q: Point2| Point2::new(0.5 * p.x + 0.5 * q.x, 0.5 * p.y + 0.5 * q.y);
770 let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
771 for corners in [[a, ab, ca], [ab, b, bc], [ca, bc, c], [ab, bc, ca]] {
772 if outside(through, &corners)? {
773 continue;
774 }
775 cells += 1;
776 let child = cell(
777 corners,
778 parent.root,
779 Some(parent.anchor),
780 parent.depth + 1,
781 cells,
782 &mut upper,
783 )?;
784 if let Anchored::Cell(child) = child {
785 if child.lower < upper.map_or(f64::INFINITY, |(u, ..)| u) {
786 heap.push(child);
787 }
788 }
789 }
790 }
791 let Some((hi, witness, width)) = upper else {
792 let lower = if heap.is_empty() {
793 f64::INFINITY
794 } else {
795 heap.peek().map_or(floor, |c| c.lower.max(floor)) * (1.0 - relative)
796 };
797 return Ok(WeightedForcedWalk {
798 cost: LengthInterval {
799 lower,
800 upper: f64::INFINITY,
801 },
802 shortest,
803 witness: None,
804 converged: heap.is_empty(),
805 cells,
806 });
807 };
808 let lower = heap.peek().map_or(hi, |c| c.lower.min(hi)).max(floor);
809 Ok(WeightedForcedWalk {
810 cost: LengthInterval {
811 lower: (lower * (1.0 - relative)).min(hi),
812 upper: hi * (1.0 + relative),
813 },
814 shortest,
815 witness: Some(witness),
816 converged: hi - lower <= tolerance + width,
817 cells,
818 })
819}
820
821fn weighted_lipschitz_lower(f: f64, radius: f64, k: f64) -> f64 {
825 f - 2.0 * k * radius
826}
827
828fn weighted_pair_bound(
831 map: &WeightedMap,
832 origins: &[Span],
833 targets: &[Span],
834 t: &[Point2; 3],
835) -> Result<f64, RouteError> {
836 let factor = map.inside_factor(t)?;
837 span_pair_bound(origins, targets, t, factor, map.obstacles())
838}
839
840fn span_pair_bound(
845 origins: &[Span],
846 targets: &[Span],
847 t: &[Point2; 3],
848 factor: f64,
849 obstacles: &[(Point2, Point2)],
850) -> Result<f64, RouteError> {
851 let origins = span_candidates(origins, t, obstacles)?;
852 let targets = span_candidates(targets, t, obstacles)?;
853 let Some(&(least, ..)) = targets.first() else {
854 return Ok(f64::INFINITY);
855 };
856 let mut best = f64::INFINITY;
857 for &(s, u, du, gu) in &origins {
858 if s + least >= best {
860 break;
861 }
862 for &(r, v, dv, gv) in &targets {
863 if s + r >= best {
864 break;
865 }
866 let apart = crate::weighted::span_distance((u.a, u.b), (v.a, v.b))?;
867 best = best.min(du + dv + factor * apart.max(gu + gv));
868 }
869 }
870 Ok(best)
871}
872
873fn span_candidates<'s>(
879 spans: &'s [Span],
880 t: &[Point2; 3],
881 obstacles: &[(Point2, Point2)],
882) -> Result<Vec<(f64, &'s Span, f64, f64)>, RouteError> {
883 let mut out = Vec::new();
884 'span: for span in spans {
885 if let Some((a, b)) = span.solid {
886 let v = span.a;
887 let inside = |c: Point2| -> Result<bool, RouteError> {
888 let (ab, ac) = (side(v, a, b)?, side(v, a, c)?);
889 let (ba, bc) = (side(v, b, a)?, side(v, b, c)?);
890 Ok(ac != Sign::Zero && ac == ab && bc != Sign::Zero && bc == ba)
891 };
892 if inside(t[0])? && inside(t[1])? && inside(t[2])? {
893 continue 'span;
894 }
895 }
896 for &(p, q) in obstacles {
897 let mut all = true;
898 for end in [span.a, span.b] {
899 for c in t {
900 if !crosses(end, *c, p, q)? {
901 all = false;
902 }
903 }
904 }
905 if all {
906 continue 'span;
907 }
908 }
909 let g = if span.a == span.b {
910 triangle_distance(t, span.a)?
911 } else if meets_triangle(span.a, span.b, t)? {
912 0.0
913 } else {
914 let mut g = f64::INFINITY;
915 for i in 0..3 {
916 let (c, d) = (t[i], t[(i + 1) % 3]);
917 g = g
918 .min(segment_distance(c, d, span.a))
919 .min(segment_distance(c, d, span.b))
920 .min(segment_distance(span.a, span.b, c));
921 }
922 g
923 };
924 out.push((g + span.least, span, span.least, g));
925 }
926 out.sort_by(|a, b| a.0.total_cmp(&b.0));
927 Ok(out)
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::distance_map;
934 use axiolid_overlay::Ring;
935
936 fn p(x: f64, y: f64) -> Point2 {
937 Point2::new(x, y)
938 }
939
940 fn ring(points: &[(f64, f64)]) -> Ring {
941 Ring {
942 points: points.iter().map(|(x, y)| p(*x, *y)).collect(),
943 }
944 }
945
946 fn two_holes() -> Polygon {
947 Polygon {
948 outer: ring(&[(0.0, 0.0), (12.0, 0.0), (12.0, 8.0), (0.0, 8.0)]),
949 holes: vec![
950 ring(&[(2.0, 2.0), (5.0, 2.0), (5.0, 6.0), (2.0, 6.0)]),
951 ring(&[(7.0, 2.0), (10.0, 2.0), (10.0, 6.0), (7.0, 6.0)]),
952 ],
953 }
954 }
955
956 fn vertices(map: &DistanceMap) -> Vec<Vertex> {
957 map.nodes()
958 .iter()
959 .copied()
960 .zip(map.vertex_distances())
961 .filter(|(_, d)| d.is_finite())
962 .map(|(at, distance)| Vertex {
963 at,
964 distance,
965 solid: solid_corner(map.region(), map.obstacles(), at).unwrap(),
966 })
967 .collect()
968 }
969
970 #[test]
973 fn the_lipschitz_bound_allows_both_distances_to_fall_together() {
974 let room = [Polygon {
975 outer: ring(&[(-10.0, -10.0), (10.0, -10.0), (10.0, 10.0), (-10.0, 10.0)]),
976 holes: Vec::new(),
977 }];
978 let map = distance_map(&room, &[], &[p(0.0, 0.0)]).unwrap();
979 let a = p(5.0, 0.0);
980 let f = 2.0 * map.at(a).unwrap().unwrap();
981 let y = p(4.0, 0.0);
983 let least = 2.0 * map.at(y).unwrap().unwrap();
984 assert!(lipschitz_lower(f, 1.0) <= least);
985 assert_eq!(lipschitz_lower(f, 1.0), least);
986 }
987
988 #[test]
991 fn the_pair_bound_is_the_least_over_every_pair() {
992 let region = [two_holes()];
993 let from = distance_map(®ion, &[], &[p(1.0, 1.0), p(6.0, 7.5)]).unwrap();
994 let to = distance_map(®ion, &[], &[p(11.0, 7.0), p(6.0, 0.5)]).unwrap();
995 let search = Search {
996 from: &from,
997 to: &to,
998 through: ®ion[0],
999 from_vertices: vertices(&from),
1000 to_vertices: vertices(&to),
1001 roots: &[],
1002 evaluated: HashMap::new(),
1003 upper: None,
1004 };
1005 let mut checked = 0;
1006 for i in 0..24 {
1007 for j in 0..16 {
1008 let (x, y) = (0.25 + 0.5 * f64::from(i), 0.25 + 0.5 * f64::from(j));
1009 let t = [p(x, y), p(x + 0.4, y), p(x, y + 0.4)];
1010 let origins = candidates(&search.from_vertices, &t, from.obstacles()).unwrap();
1011 let targets = candidates(&search.to_vertices, &t, to.obstacles()).unwrap();
1012 let mut brute = f64::INFINITY;
1013 for &(_, u, du, gu) in &origins {
1014 for &(_, v, dv, gv) in &targets {
1015 brute = brute.min(du + dv + (u - v).length().max(gu + gv));
1016 }
1017 }
1018 assert_eq!(search.pair_bound(&t).unwrap(), brute, "{t:?}");
1019 checked += 1;
1020 }
1021 }
1022 assert_eq!(checked, 384);
1023 }
1024
1025 fn weighted_spans(map: &WeightedMap) -> Vec<Span> {
1026 map.spans()
1027 .into_iter()
1028 .map(|((a, b), least)| Span {
1029 a,
1030 b,
1031 least,
1032 solid: if a == b {
1033 solid_corner(map.region(), map.obstacles(), a).unwrap()
1034 } else {
1035 None
1036 },
1037 })
1038 .collect()
1039 }
1040
1041 #[test]
1045 fn the_weighted_lipschitz_bound_allows_the_steepest_fall() {
1046 let room = [Polygon {
1047 outer: ring(&[(-10.0, -10.0), (10.0, -10.0), (10.0, 10.0), (-10.0, 10.0)]),
1048 holes: Vec::new(),
1049 }];
1050 let costly = [crate::CostRegion::new(room[0].clone(), 3.0)];
1051 let map = crate::weighted_distance_map(&room, &[], &[p(0.0, 0.0)], &costly, 1.0).unwrap();
1052 for (at, d) in [(p(5.0, 0.0), 15.0), (p(4.0, 0.0), 12.0)] {
1054 let (lo, hi) = map.bracket(at).unwrap().unwrap();
1055 assert!(lo <= d && d <= hi && hi - lo < 1e-9, "[{lo}, {hi}]");
1056 }
1057 let k = map
1058 .steepest(&[p(4.0, 0.0), p(5.0, 0.0), p(5.0, 1.0)])
1059 .unwrap();
1060 assert_eq!(k, 3.0);
1061 assert_eq!(weighted_lipschitz_lower(30.0, 1.0, k), 24.0);
1062 }
1063
1064 #[test]
1069 fn the_weighted_pair_bound_is_below_every_walk_through_the_cell() {
1070 let room = [Polygon {
1071 outer: ring(&[(0.0, 0.0), (10.0, 0.0), (10.0, 4.0), (0.0, 4.0)]),
1072 holes: Vec::new(),
1073 }];
1074 let costs = [
1075 crate::CostRegion::new(
1076 Polygon {
1077 outer: ring(&[(0.0, -1.0), (10.0, -1.0), (10.0, 0.0), (0.0, 0.0)]),
1078 holes: Vec::new(),
1079 },
1080 3.0,
1081 ),
1082 crate::CostRegion::new(
1083 Polygon {
1084 outer: ring(&[(4.0, 1.5), (6.0, 1.5), (6.0, 3.5), (4.0, 3.5)]),
1085 holes: Vec::new(),
1086 },
1087 3.0,
1088 ),
1089 ];
1090 let map = |t: Point2| crate::weighted_distance_map(&room, &[], &[t], &costs, 0.5).unwrap();
1091 let (from, to) = (map(p(1.0, 2.0)), map(p(9.0, 2.0)));
1092 let (fs, ts) = (weighted_spans(&from), weighted_spans(&to));
1093 let mut checked = 0;
1094 for i in 0..20 {
1095 for j in 0..8 {
1096 let (x, y) = (0.5 * f64::from(i), 0.5 * f64::from(j));
1097 let t = [p(x, y), p(x + 0.5, y), p(x, y + 0.5)];
1098 let bound = weighted_pair_bound(&from, &fs, &ts, &t).unwrap();
1099 for c in t {
1100 let walk =
1101 from.bracket(c).unwrap().unwrap().1 + to.bracket(c).unwrap().unwrap().1;
1102 assert!(bound <= walk + 1e-9, "{t:?}: {bound} above {walk} at {c:?}");
1103 }
1104 checked += 1;
1105 }
1106 }
1107 assert_eq!(checked, 160);
1108 let t = [p(2.0, 0.0), p(2.5, 0.0), p(2.0, 0.5)];
1110 assert_eq!(from.inside_factor(&t).unwrap(), 1.0);
1111 assert_eq!(from.steepest(&t).unwrap(), 3.0);
1112 assert_eq!(
1115 from.inside_factor(&[p(4.5, 2.0), p(5.0, 2.0), p(4.5, 2.5)])
1116 .unwrap(),
1117 3.0
1118 );
1119 assert_eq!(
1120 from.inside_factor(&[p(3.5, 2.0), p(4.5, 2.0), p(3.5, 2.5)])
1121 .unwrap(),
1122 1.0
1123 );
1124 }
1125
1126 #[test]
1128 fn a_vertex_is_hidden_only_from_the_whole_cell() {
1129 let region = [two_holes()];
1130 let map = distance_map(®ion, &[], &[p(1.0, 1.0)]).unwrap();
1131 let find = |at: Point2| {
1132 vertices(&map)
1133 .into_iter()
1134 .find(|v| v.at == at)
1135 .expect("a vertex")
1136 };
1137 let hidden = |v: &Vertex, t: [Point2; 3]| {
1138 !candidates(core::slice::from_ref(v), &t, map.obstacles())
1139 .unwrap()
1140 .iter()
1141 .any(|c| c.1 == v.at)
1142 };
1143 let origin = find(p(1.0, 1.0));
1145 let behind = [p(6.0, 4.5), p(6.5, 4.5), p(6.0, 5.0)];
1146 assert!(hidden(&origin, behind));
1147 let peeking = [p(6.0, 4.5), p(6.5, 1.5), p(6.0, 5.0)];
1149 assert!(!hidden(&origin, peeking));
1150 let corner = find(p(5.0, 2.0));
1154 assert!(corner.solid.is_some());
1155 let inside = [p(4.5, 2.5), p(4.8, 2.5), p(4.5, 2.8)];
1156 assert!(hidden(&corner, inside));
1157 let straddling = [p(4.5, 2.5), p(5.5, 2.5), p(4.5, 2.8)];
1158 assert!(!hidden(&corner, straddling));
1159 assert!(find(p(0.0, 0.0)).solid.is_none());
1161 }
1162}