refactor(router/thetastar): Backtrack not once, but repeatedly, if condition is met

This commit is contained in:
Mikolaj Wielgus 2025-08-20 00:13:25 +02:00
parent cf100ac6f6
commit 5a1cb564dc
2 changed files with 74 additions and 58 deletions

View File

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::ops::ControlFlow;
use derive_getters::Getters; use derive_getters::Getters;
use geo::algorithm::line_measures::{Distance, Euclidean}; use geo::algorithm::line_measures::{Distance, Euclidean};
use petgraph::data::DataMap; use petgraph::data::DataMap;
@ -93,26 +95,28 @@ impl<R: AccessRules> ThetastarStrategy<Navmesh, f64, BandTermsegIndex>
&mut self, &mut self,
navmesh: &Navmesh, navmesh: &Navmesh,
probed_navnode: NavnodeIndex, probed_navnode: NavnodeIndex,
) -> Option<f64> { ) -> ControlFlow<Option<f64>> {
let result = self.navcord.step_to(self.layout, navmesh, probed_navnode); let result = self.navcord.step_to(self.layout, navmesh, probed_navnode);
match result { ControlFlow::Break(match result {
Ok(probe_length) => Some(probe_length), Ok(probe_length) => Some(probe_length),
Err(err) => { Err(err) => {
if let NavcorderException::CannotDraw(draw_err) = err { if let NavcorderException::CannotDraw(draw_err) = err {
let layout_err = match draw_err { let layout_err = match draw_err {
DrawException::NoTangents(..) => return None, DrawException::NoTangents(..) => return ControlFlow::Break(None),
DrawException::CannotFinishIn(.., layout_err) => layout_err, DrawException::CannotFinishIn(.., layout_err) => layout_err,
DrawException::CannotWrapAround(.., layout_err) => layout_err, DrawException::CannotWrapAround(.., layout_err) => layout_err,
}; };
let (ghost, obstacle) = layout_err.maybe_ghost_and_obstacle()?; let Some((ghost, obstacle)) = layout_err.maybe_ghost_and_obstacle() else {
return ControlFlow::Break(None);
};
self.probe_ghosts = vec![*ghost]; self.probe_ghosts = vec![*ghost];
self.probe_obstacles = vec![obstacle]; self.probe_obstacles = vec![obstacle];
} }
None None
} }
} })
} }
fn remove_probe(&mut self, _navmesh: &Navmesh) { fn remove_probe(&mut self, _navmesh: &Navmesh) {

View File

@ -120,7 +120,11 @@ where
navnode: G::NodeId, navnode: G::NodeId,
tracker: &PathTracker<G>, tracker: &PathTracker<G>,
) -> Result<Option<R>, ()>; ) -> Result<Option<R>, ()>;
fn place_probe_to_navnode(&mut self, graph: &G, probed_navnode: G::NodeId) -> Option<K>; fn place_probe_to_navnode(
&mut self,
graph: &G,
probed_navnode: G::NodeId,
) -> ControlFlow<Option<K>>;
fn remove_probe(&mut self, graph: &G); fn remove_probe(&mut self, graph: &G);
fn estimate_cost_to_goal(&mut self, graph: &G, navnode: G::NodeId) -> K; fn estimate_cost_to_goal(&mut self, graph: &G, navnode: G::NodeId) -> K;
} }
@ -145,10 +149,17 @@ pub enum ThetastarState<N: Copy, E: Copy> {
} }
/// The pathfinding algorithm Topola uses to find the shortest path to route /// The pathfinding algorithm Topola uses to find the shortest path to route
/// is Theta*. Theta* is just A* with an improvement: every time an navedge is /// is Theta* with conditional repeated backtracking. Theta* is just A* with an
/// scanned, the algorithm first tries to draw directly to its target navnode /// improvement: every time an navedge is scanned, the algorithm first tries to
/// from the predecessor of the currently visited navnode. Note that this /// draw directly to its target navnode from the predecessor of the currently
/// creates paths with edges that do not all lie on the navmesh. /// visited navnode. Note that this creates paths with edges that do not all lie
/// on the navmesh.
///
/// Conditional repeated backtracking is our improvement to Theta*: if
/// line-of-sight routing fails if a condition is met, continue trying to draw
/// from parent of the parent navnode, and so on. This is different from Theta*
/// because in Theta* there is only one backtracking step -- only one attempt to
/// do line-of-sight routing.
#[derive(Getters)] #[derive(Getters)]
pub struct ThetastarStepper<G, K> pub struct ThetastarStepper<G, K>
where where
@ -301,61 +312,61 @@ where
// necessarily scored before adding it to `.visit_next`. // necessarily scored before adding it to `.visit_next`.
//let node_score = self.scores[&visited_navnode]; //let node_score = self.scores[&visited_navnode];
let to_navnode = (&self.graph).edge_ref(curr_navedge).target(); let to_navnode = (&self.graph).edge_ref(curr_navedge).target();
let mut curr_navnode = visited_navnode;
// Loop to repeatedly backtrack.
if let (Some(parent_navnode), Some(los_cost)) = loop {
let Some(parent_navnode) = self.path_tracker.predecessor(curr_navnode)
else {
break (None, None);
};
if let Some(parent_navnode) = self.path_tracker.predecessor(visited_navnode) {
// Visit parent node. // Visit parent node.
strategy.visit_navnode(&self.graph, parent_navnode, &self.path_tracker); strategy.visit_navnode(&self.graph, parent_navnode, &self.path_tracker);
let parent_score = self.scores[&parent_navnode]; if let ControlFlow::Break(result) =
if let Some(los_cost) =
strategy.place_probe_to_navnode(&self.graph, to_navnode) strategy.place_probe_to_navnode(&self.graph, to_navnode)
{ {
let next = to_navnode; break (Some(parent_navnode), result);
let next_score = parent_score + los_cost;
match self.scores.entry(next) {
Entry::Occupied(mut entry) => {
// No need to add neighbors that we have already reached through a
// shorter path than now.
if *entry.get() <= next_score {
// We just remove the probe instantly
// here instead of doing it in
// ThetastarState::Probing or a new
// state to avoid complicating.
strategy.remove_probe(&self.graph);
self.state = ThetastarState::VisitingProbeOnNavedge(
visited_navnode,
curr_navedge,
);
return Ok(ControlFlow::Continue(self.state));
}
entry.insert(next_score);
}
Entry::Vacant(entry) => {
entry.insert(next_score);
}
}
self.push_to_frontier(next, next_score, parent_navnode, strategy);
self.state = ThetastarState::Probing(visited_navnode);
Ok(ControlFlow::Continue(self.state))
} else {
// Come back from parent node if drawing from it failed.
strategy.visit_navnode(
&self.graph,
visited_navnode,
&self.path_tracker,
);
self.state = ThetastarState::VisitingProbeOnNavedge(
visited_navnode,
curr_navedge,
);
Ok(ControlFlow::Continue(self.state))
} }
curr_navnode = parent_navnode;
} {
let parent_score = self.scores[&parent_navnode];
let next = to_navnode;
let next_score = parent_score + los_cost;
match self.scores.entry(next) {
Entry::Occupied(mut entry) => {
// No need to add neighbors that we have already reached through a
// shorter path than now.
if *entry.get() <= next_score {
// We just remove the probe instantly
// here instead of doing it in
// ThetastarState::Probing or a new
// state to avoid complicating.
strategy.remove_probe(&self.graph);
self.state = ThetastarState::VisitingProbeOnNavedge(
visited_navnode,
curr_navedge,
);
return Ok(ControlFlow::Continue(self.state));
}
entry.insert(next_score);
}
Entry::Vacant(entry) => {
entry.insert(next_score);
}
}
self.push_to_frontier(next, next_score, parent_navnode, strategy);
self.state = ThetastarState::Probing(visited_navnode);
Ok(ControlFlow::Continue(self.state))
} else { } else {
// Come back from parent node if drawing from it failed.
strategy.visit_navnode(&self.graph, visited_navnode, &self.path_tracker);
self.state = self.state =
ThetastarState::VisitingProbeOnNavedge(visited_navnode, curr_navedge); ThetastarState::VisitingProbeOnNavedge(visited_navnode, curr_navedge);
Ok(ControlFlow::Continue(self.state)) Ok(ControlFlow::Continue(self.state))
@ -369,7 +380,8 @@ where
let visited_score = self.scores[&visited_navnode]; let visited_score = self.scores[&visited_navnode];
let to_navnode = (&self.graph).edge_ref(curr_navedge).target(); let to_navnode = (&self.graph).edge_ref(curr_navedge).target();
if let Some(navedge_cost) = strategy.place_probe_to_navnode(&self.graph, to_navnode) if let ControlFlow::Break(Some(navedge_cost)) =
strategy.place_probe_to_navnode(&self.graph, to_navnode)
{ {
let next = to_navnode; let next = to_navnode;
let next_score = visited_score + navedge_cost; let next_score = visited_score + navedge_cost;