feat(topola-egui): Add progress bar for the currently routed ratline

The capability to measure progress will later be useful to choose slower
but better optimization strategies if more time is available.
This commit is contained in:
Mikolaj Wielgus 2025-07-09 11:45:15 +02:00
parent 29dc59df04
commit 68d9844d0d
15 changed files with 233 additions and 43 deletions

View File

@ -4,7 +4,7 @@
use std::ops::ControlFlow;
use topola::interactor::activity::ActivityStepperWithStatus;
use topola::{interactor::activity::ActivityStepperWithStatus, stepper::EstimateProgress};
use crate::{translator::Translator, viewport::Viewport};
@ -38,6 +38,20 @@ impl StatusBar {
"x: {} y: {} \t {}",
latest_pos.x, -latest_pos.y, message
));
if let Some(activity) = maybe_activity {
let value = activity.estimate_progress_value();
let maximum = activity.estimate_progress_maximum();
ui.add(
egui::ProgressBar::new((value / maximum) as f32).text(format!(
"{:.1} ({:.1}/{:.1})",
value / maximum * 100.0,
value,
maximum
)),
);
}
});
}
}

View File

@ -376,7 +376,6 @@ impl Viewport {
};
if menu_bar.show_pathfinding_scores {
//TODO "{astar.scores[index]} ({astar.estimate_scores[index]}) (...)"
let score_text = thetastar
.scores()
.get(&navnode)
@ -384,7 +383,7 @@ impl Viewport {
format!("g={:.2}", s)
});
let estimate_score_text = thetastar
.estimate_scores()
.cost_to_goal_estimate_scores()
.get(&navnode)
.map_or_else(String::new, |s| {
format!("(f={:.2})", s)

View File

@ -17,7 +17,7 @@ use crate::{
router::{
navcord::Navcord, navmesh::Navmesh, thetastar::ThetastarStepper, RouteStepper, Router,
},
stepper::Step,
stepper::{EstimateProgress, Step},
};
use super::{invoker::GetDebugOverlayData, Autorouter, AutorouterError, AutorouterOptions};
@ -164,6 +164,22 @@ impl<M: AccessMesadata> Step<Autorouter<M>, Option<LayoutEdit>, AutorouteContinu
}
}
impl EstimateProgress for AutorouteExecutionStepper {
type Value = f64;
fn estimate_progress_value(&self) -> f64 {
self.route
.as_ref()
.map_or(0.0, |route| route.estimate_progress_value())
}
fn estimate_progress_maximum(&self) -> f64 {
self.route
.as_ref()
.map_or(0.0, |route| route.estimate_progress_maximum())
}
}
impl GetDebugOverlayData for AutorouteExecutionStepper {
fn maybe_thetastar(&self) -> Option<&ThetastarStepper<Navmesh, f64>> {
self.route.as_ref().map(|route| route.thetastar())

View File

@ -15,7 +15,7 @@ use crate::{
geometry::{primitive::PrimitiveShape, shape::MeasureLength},
graph::MakeRef,
router::{navcord::Navcord, navmesh::Navmesh, thetastar::ThetastarStepper},
stepper::Step,
stepper::{EstimateProgress, Step},
};
use super::{
@ -100,6 +100,10 @@ impl<M: AccessMesadata> Step<Autorouter<M>, (f64, f64)> for CompareDetoursExecut
}
}
impl EstimateProgress for CompareDetoursExecutionStepper {
type Value = f64;
}
impl GetDebugOverlayData for CompareDetoursExecutionStepper {
fn maybe_thetastar(&self) -> Option<&ThetastarStepper<Navmesh, f64>> {
self.autoroute.maybe_thetastar()

View File

@ -11,7 +11,7 @@ use crate::{
board::AccessMesadata,
layout::{via::ViaWeight, LayoutEdit},
router::ng,
stepper::{Abort, Step},
stepper::{Abort, EstimateProgress, Step},
};
use super::{
@ -165,3 +165,39 @@ impl<M: AccessMesadata + Clone> Abort<Invoker<M>> for ExecutionStepper<M> {
}
}
}
// Since enum_dispatch does not really support generics, we implement this the
// long way.
impl<M> EstimateProgress for ExecutionStepper<M> {
type Value = f64;
fn estimate_progress_value(&self) -> f64 {
match self {
ExecutionStepper::Autoroute(autoroute) => autoroute.estimate_progress_value(),
ExecutionStepper::TopoAutoroute(toporoute) => toporoute.estimate_progress_value(),
ExecutionStepper::PlaceVia(place_via) => place_via.estimate_progress_value(),
ExecutionStepper::RemoveBands(remove_bands) => remove_bands.estimate_progress_value(),
ExecutionStepper::CompareDetours(compare_detours) => {
compare_detours.estimate_progress_value()
}
ExecutionStepper::MeasureLength(measure_length) => {
measure_length.estimate_progress_value()
}
}
}
fn estimate_progress_maximum(&self) -> f64 {
match self {
ExecutionStepper::Autoroute(autoroute) => autoroute.estimate_progress_maximum(),
ExecutionStepper::TopoAutoroute(toporoute) => toporoute.estimate_progress_maximum(),
ExecutionStepper::PlaceVia(place_via) => place_via.estimate_progress_maximum(),
ExecutionStepper::RemoveBands(remove_bands) => remove_bands.estimate_progress_maximum(),
ExecutionStepper::CompareDetours(compare_detours) => {
compare_detours.estimate_progress_maximum()
}
ExecutionStepper::MeasureLength(measure_length) => {
measure_length.estimate_progress_maximum()
}
}
}
}

View File

@ -150,10 +150,10 @@ impl<M: AccessMesadata + Clone> Invoker<M> {
}
}
#[debug_requires(self.ongoing_command.is_none())]
/// Pass given command to be executed.
///
/// Function used to set given [`Command`] to ongoing state, dispatch and execute it.
#[debug_requires(self.ongoing_command.is_none())]
pub fn execute_stepper(
&mut self,
command: Command,

View File

@ -8,6 +8,7 @@
use crate::{
board::AccessMesadata, geometry::shape::MeasureLength as MeasureLengthTrait, graph::MakeRef,
stepper::EstimateProgress,
};
use super::{invoker::GetDebugOverlayData, selection::BandSelection, Autorouter, AutorouterError};
@ -47,4 +48,7 @@ impl MeasureLengthExecutionStepper {
}
}
impl EstimateProgress for MeasureLengthExecutionStepper {
type Value = f64;
}
impl GetDebugOverlayData for MeasureLengthExecutionStepper {}

View File

@ -9,6 +9,7 @@
use crate::{
board::AccessMesadata,
layout::{via::ViaWeight, LayoutEdit},
stepper::EstimateProgress,
};
use super::{invoker::GetDebugOverlayData, Autorouter, AutorouterError};
@ -46,4 +47,7 @@ impl PlaceViaExecutionStepper {
}
}
impl EstimateProgress for PlaceViaExecutionStepper {
type Value = f64;
}
impl GetDebugOverlayData for PlaceViaExecutionStepper {}

View File

@ -4,7 +4,7 @@
//! Provides functionality to remove bands from the layout.
use crate::{board::AccessMesadata, layout::LayoutEdit};
use crate::{board::AccessMesadata, layout::LayoutEdit, stepper::EstimateProgress};
use super::{invoker::GetDebugOverlayData, selection::BandSelection, Autorouter, AutorouterError};
@ -44,4 +44,7 @@ impl RemoveBandsExecutionStepper {
}
}
impl EstimateProgress for RemoveBandsExecutionStepper {
type Value = f64;
}
impl GetDebugOverlayData for RemoveBandsExecutionStepper {}

View File

@ -25,7 +25,7 @@ use crate::{
ng,
thetastar::ThetastarStepper,
},
stepper::{Abort, OnEvent, Step},
stepper::{Abort, EstimateProgress, OnEvent, Step},
};
/// Stores the interactive input data from the user.
@ -98,6 +98,26 @@ impl<M: AccessMesadata + Clone> Abort<Invoker<M>> for ActivityStepper<M> {
}
}
// Since enum_dispatch does not really support generics, we implement this the
// long way.
impl<M> EstimateProgress for ActivityStepper<M> {
type Value = f64;
fn estimate_progress_value(&self) -> f64 {
match self {
ActivityStepper::Interaction(..) => 0.0,
ActivityStepper::Execution(execution) => execution.estimate_progress_value(),
}
}
fn estimate_progress_maximum(&self) -> f64 {
match self {
ActivityStepper::Interaction(..) => 0.0,
ActivityStepper::Execution(execution) => execution.estimate_progress_maximum(),
}
}
}
impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for ActivityStepper<M> {
type Output = Result<(), InteractionError>;
@ -179,6 +199,18 @@ impl<M: AccessMesadata + Clone> OnEvent<ActivityContext<'_, M>, InteractiveEvent
}
}
impl<M> EstimateProgress for ActivityStepperWithStatus<M> {
type Value = f64;
fn estimate_progress_value(&self) -> f64 {
self.activity.estimate_progress_value()
}
fn estimate_progress_maximum(&self) -> f64 {
self.activity.estimate_progress_maximum()
}
}
impl<M> GetDebugOverlayData for ActivityStepperWithStatus<M> {
fn maybe_thetastar(&self) -> Option<&ThetastarStepper<Navmesh, f64>> {
self.activity.maybe_thetastar()

View File

@ -15,7 +15,7 @@ use crate::{
geometry::primitive::PrimitiveShape,
graph::GenericIndex,
layout::{poly::PolyWeight, Layout, LayoutEdit},
stepper::Abort,
stepper::{Abort, EstimateProgress},
};
use super::{
@ -232,6 +232,10 @@ impl<R: AccessRules + Clone + std::panic::RefUnwindSafe> AutorouteExecutionStepp
}
}
impl<M> EstimateProgress for AutorouteExecutionStepper<M> {
type Value = f64;
}
impl<M> GetDebugOverlayData for AutorouteExecutionStepper<M> {
fn maybe_topo_navmesh(&self) -> Option<pie::navmesh::NavmeshRef<'_, super::PieNavmeshBase>> {
Some(pie::navmesh::NavmeshRef {

View File

@ -19,7 +19,7 @@ use crate::{
thetastar::{ThetastarError, ThetastarStepper},
Router, RouterThetastarStrategy,
},
stepper::Step,
stepper::{EstimateProgress, Step},
};
#[derive(Getters, Dissolve)]
@ -80,6 +80,7 @@ impl<R: AccessRules> Step<Router<'_, R>, BandTermsegIndex> for RouteStepper {
let target = self.thetastar.graph().destination();
let mut strategy = RouterThetastarStrategy::new(layout, &mut self.navcord, target);
let result = self.thetastar.step(&mut strategy);
self.ghosts = strategy.probe_ghosts;
self.obstacles = strategy.probe_obstacles;
@ -97,3 +98,15 @@ impl<R: AccessRules> Step<Router<'_, R>, BandTermsegIndex> for RouteStepper {
}
}
}
impl EstimateProgress for RouteStepper {
type Value = f64;
fn estimate_progress_value(&self) -> f64 {
self.thetastar.estimate_progress_value()
}
fn estimate_progress_maximum(&self) -> f64 {
self.thetastar.estimate_progress_maximum()
}
}

View File

@ -119,7 +119,7 @@ impl<R: AccessRules> ThetastarStrategy<Navmesh, f64, BandTermsegIndex>
self.navcord.step_back(self.layout);
}
fn estimate_cost(&mut self, navmesh: &Navmesh, vertex: NavnodeIndex) -> f64 {
fn estimate_cost_to_goal(&mut self, navmesh: &Navmesh, vertex: NavnodeIndex) -> f64 {
let start_point = PrimitiveIndex::from(navmesh.node_weight(vertex).unwrap().node)
.primitive(self.layout.drawing())
.shape()

View File

@ -11,7 +11,7 @@
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, BinaryHeap};
use std::ops::ControlFlow;
use std::ops::{ControlFlow, Sub};
use derive_getters::Getters;
use petgraph::algo::Measure;
@ -20,7 +20,7 @@ use thiserror::Error;
use std::cmp::Ordering;
use crate::stepper::Step;
use crate::stepper::{EstimateProgress, Step};
#[derive(Copy, Clone, Debug)]
pub struct MinScored<K, T>(pub K, pub T);
@ -122,7 +122,7 @@ where
) -> Result<Option<R>, ()>;
fn place_probe_to_navnode<'a>(&mut self, graph: &'a G, probed_navnode: G::NodeId) -> Option<K>;
fn remove_probe(&mut self, graph: &G);
fn estimate_cost(&mut self, graph: &G, navnode: G::NodeId) -> K;
fn estimate_cost_to_goal(&mut self, graph: &G, navnode: G::NodeId) -> K;
}
pub trait MakeEdgeRef: IntoEdgeReferences {
@ -155,21 +155,28 @@ where
G: GraphBase,
G::NodeId: Eq + Ord,
for<'a> &'a G: IntoEdges<NodeId = G::NodeId, EdgeId = G::EdgeId> + MakeEdgeRef,
K: Measure + Copy,
K: Measure + Copy + Sub<Output = K>,
{
state: ThetastarState<G::NodeId, G::EdgeId>,
graph: G,
/// The priority queue of the navnodes to expand.
#[getter(skip)]
visit_next: BinaryHeap<MinScored<K, G::NodeId>>,
frontier: BinaryHeap<MinScored<K, G::NodeId>>,
/// Also known as the g-scores, or just g.
scores: BTreeMap<G::NodeId, K>,
/// Also known as the f-scores, or just f.
estimate_scores: BTreeMap<G::NodeId, K>,
cost_to_goal_estimate_scores: BTreeMap<G::NodeId, K>,
#[getter(skip)]
path_tracker: PathTracker<G>,
// FIXME: To work around edge references borrowing from the graph we collect then reiterate over them.
#[getter(skip)]
edge_ids: Vec<G::EdgeId>,
#[getter(skip)]
progress_estimate_value: K,
#[getter(skip)]
progress_estimate_maximum: K,
}
#[derive(Error, Debug, Clone)]
@ -183,29 +190,55 @@ where
G: GraphBase,
G::NodeId: Eq + Ord,
for<'a> &'a G: IntoEdges<NodeId = G::NodeId, EdgeId = G::EdgeId> + MakeEdgeRef,
K: Measure + Copy,
K: Measure + Copy + Sub<Output = K>,
{
pub fn new<R>(
graph: G,
start: G::NodeId,
strategy: &mut impl ThetastarStrategy<G, K, R>,
) -> Self {
let estimated_cost_from_start_to_goal = strategy.estimate_cost_to_goal(&graph, start);
let mut this = Self {
state: ThetastarState::Scanning,
graph,
visit_next: BinaryHeap::new(),
frontier: BinaryHeap::new(),
scores: BTreeMap::new(),
estimate_scores: BTreeMap::new(),
cost_to_goal_estimate_scores: BTreeMap::new(),
path_tracker: PathTracker::<G>::new(),
edge_ids: Vec::new(),
progress_estimate_value: K::default(),
progress_estimate_maximum: estimated_cost_from_start_to_goal,
};
let zero_score = K::default();
this.scores.insert(start, zero_score);
this.visit_next
.push(MinScored(strategy.estimate_cost(&this.graph, start), start));
this.frontier
.push(MinScored(estimated_cost_from_start_to_goal, start));
this
}
fn push_to_frontier<R>(
&mut self,
next: G::NodeId,
next_score: K,
predecessor: G::NodeId,
strategy: &mut impl ThetastarStrategy<G, K, R>,
) {
let cost_to_goal_estimate = strategy.estimate_cost_to_goal(&self.graph, next);
if cost_to_goal_estimate > self.progress_estimate_maximum {
self.progress_estimate_maximum = cost_to_goal_estimate;
}
if self.progress_estimate_maximum - cost_to_goal_estimate > self.progress_estimate_value {
self.progress_estimate_value = self.progress_estimate_maximum - cost_to_goal_estimate;
}
self.path_tracker.set_predecessor(next, predecessor);
let next_estimate_score = next_score + cost_to_goal_estimate;
self.frontier.push(MinScored(next_estimate_score, next));
}
}
impl<G, K, R, S: ThetastarStrategy<G, K, R>>
@ -214,7 +247,7 @@ where
G: GraphBase,
G::NodeId: Eq + Ord,
for<'a> &'a G: IntoEdges<NodeId = G::NodeId, EdgeId = G::EdgeId> + MakeEdgeRef,
K: Measure + Copy,
K: Measure + Copy + Sub<Output = K>,
{
type Error = ThetastarError;
@ -227,7 +260,7 @@ where
> {
match self.state {
ThetastarState::Scanning => {
let Some(MinScored(estimate_score, navnode)) = self.visit_next.pop() else {
let Some(MinScored(estimate_score, navnode)) = self.frontier.pop() else {
return Err(ThetastarError::NotFound);
};
@ -243,7 +276,7 @@ where
return Ok(ControlFlow::Break((cost, path, result)));
}
match self.estimate_scores.entry(navnode) {
match self.cost_to_goal_estimate_scores.entry(navnode) {
Entry::Occupied(mut entry) => {
// If the node has already been visited with an equal or lower
// estimated score than now, then we do not need to re-visit it.
@ -299,10 +332,7 @@ where
}
}
self.path_tracker.set_predecessor(next, parent_navnode);
let next_estimate_score =
next_score + strategy.estimate_cost(&self.graph, next);
self.visit_next.push(MinScored(next_estimate_score, next));
self.push_to_frontier(next, next_score, parent_navnode, strategy);
self.state = ThetastarState::Probing(visited_navnode);
Ok(ControlFlow::Continue(self.state))
@ -353,10 +383,7 @@ where
}
}
self.path_tracker.set_predecessor(next, visited_navnode);
let next_estimate_score =
next_score + strategy.estimate_cost(&self.graph, next);
self.visit_next.push(MinScored(next_estimate_score, next));
self.push_to_frontier(next, next_score, visited_navnode, strategy);
self.state = ThetastarState::Probing(visited_navnode);
Ok(ControlFlow::Continue(self.state))
@ -374,3 +401,21 @@ where
}
}
}
impl<G, K> EstimateProgress for ThetastarStepper<G, K>
where
G: GraphBase,
G::NodeId: Eq + Ord,
for<'a> &'a G: IntoEdges<NodeId = G::NodeId, EdgeId = G::EdgeId> + MakeEdgeRef,
K: Measure + Copy + Sub<Output = K>,
{
type Value = K;
fn estimate_progress_value(&self) -> K {
self.progress_estimate_value
}
fn estimate_progress_maximum(&self) -> K {
self.progress_estimate_maximum
}
}

View File

@ -11,14 +11,14 @@ use core::ops::ControlFlow;
///
/// An object that implements this trait is called a "stepper".
///
/// Steppers always progress linearly, that is, it is presumed that the context
/// does not change between calls in a way that can affect the stepper's future
/// states. Advanceable data structures designed for uses where the future state
/// intentionally *may* change from the information supplied as arguments are
/// not considered steppers. An example of such an advanceable non-stepper is
/// the [`Navcord`](crate::router::navcord::Navcord) struct, as it does not progress
/// linearly because it branches out by on each call taking in a
/// changeable `to` argument that affects the future states.
/// Steppers always progress linearly and their future states are determined by
/// the initial state. It is assumed that the changes in context cannot change
/// the stepper's execution. Advanceable data structures designed for uses where
/// the future state intentionally *may* change from the information supplied
/// after initialization are not considered steppers. An example of such an
/// advanceable non-stepper is the [`Navcord`] (crate::router::navcord::Navcord)
/// struct, as it does not progress linearly because it branches out on each
/// call by taking in a changeable `to` argument that affects the future states.
///
/// Petgraph's counterpart of this trait is its
/// [`petgraph::visit::Walker<Context>`] trait.
@ -53,9 +53,25 @@ pub trait Abort<C> {
fn abort(&mut self, context: &mut C);
}
/// Steppers that may receive discrete events and act on them, implement this trait.
/// Steppers that can receive discrete events and act on them, implement this
/// trait.
// XXX: Doesn't this violate the rule that stepper's future states are
// determined by its initial state?
pub trait OnEvent<Ctx, Event> {
type Output;
fn on_event(&mut self, context: &mut Ctx, event: Event) -> Self::Output;
}
/// Some steppers report estimates of how far they are from completion.
pub trait EstimateProgress {
type Value: Default;
fn estimate_progress_value(&self) -> Self::Value {
Self::Value::default()
}
fn estimate_progress_maximum(&self) -> Self::Value {
Self::Value::default()
}
}