feat(router/ng/router): Implementation of the topological router

- feat(autorouter): Prepare for population of planar Topo-Navmesh with existing routes
  See also issue #166.
- feat(topola-egui): Add dialog for topological navmesh layer selection
- feat(router/ng/eval): Optionally restrict set of allowed TopoNavmesh edges
- fix(router/ng/eval): Use poly_ext_handover
This commit is contained in:
Ellen Emilia Anna Zscheile 2025-03-24 20:58:02 +01:00
parent c8848ef269
commit a561b278fc
34 changed files with 2629 additions and 168 deletions

View File

@ -5,6 +5,7 @@
subject_length = 80
line_length = 100
style = "conventional"
merge_commit = false
# Should be the same as the list of directories in crates/ and src/.
allowed_scopes = [
@ -61,10 +62,15 @@ allowed_scopes = [
"math/cyclic_search",
"math/polygon_tangents",
"math/tangents",
"math/tunnel",
"router/draw",
"router/navcord",
"router/navcorder",
"router/navmesh",
"router/ng/eval",
"router/ng/floating",
"router/ng/poly",
"router/ng/router",
"router/route",
"router/router",
"router/thetastar",
@ -72,5 +78,3 @@ allowed_scopes = [
"stepper",
"triangulation",
]
merge_commit = false

View File

@ -205,11 +205,11 @@ impl<B: NavmeshBase> Navmesh<B> {
}
}
pub(crate) fn resolve_edge_data<PNI: Ord, EP>(
edges: &BTreeMap<EdgeIndex<NavmeshIndex<PNI>>, (Edge<PNI>, usize)>,
pub(crate) fn resolve_edge_data<PNI: Ord, EP, T>(
edges: &BTreeMap<EdgeIndex<NavmeshIndex<PNI>>, (Edge<PNI>, T)>,
from_node: NavmeshIndex<PNI>,
to_node: NavmeshIndex<PNI>,
) -> Option<(Edge<&PNI>, MaybeReversed<usize, EP>)> {
) -> Option<(Edge<&PNI>, MaybeReversed<&T, EP>)> {
let reversed = from_node > to_node;
let edge_idx: EdgeIndex<NavmeshIndex<PNI>> = (from_node, to_node).into();
let edge = edges.get(&edge_idx)?;
@ -217,11 +217,103 @@ pub(crate) fn resolve_edge_data<PNI: Ord, EP>(
if reversed {
data.flip();
}
let mut ret = MaybeReversed::new(edge.1);
let mut ret = MaybeReversed::new(&edge.1);
ret.reversed = reversed;
Some((data, ret))
}
pub(crate) fn resolve_edge_data_mut<PNI: Ord, EP, T>(
edges: &mut BTreeMap<EdgeIndex<NavmeshIndex<PNI>>, (Edge<PNI>, T)>,
from_node: NavmeshIndex<PNI>,
to_node: NavmeshIndex<PNI>,
) -> Option<(Edge<&PNI>, MaybeReversed<&mut T, EP>)> {
let reversed = from_node > to_node;
let edge_idx: EdgeIndex<NavmeshIndex<PNI>> = (from_node, to_node).into();
let edge = edges.get_mut(&edge_idx)?;
let mut data = edge.0.as_ref();
if reversed {
data.flip();
}
let mut ret = MaybeReversed::new(&mut edge.1);
ret.reversed = reversed;
Some((data, ret))
}
impl<B: NavmeshBase> NavmeshSer<B> {
pub fn edge_data(
&self,
from_node: NavmeshIndex<B::PrimalNodeIndex>,
to_node: NavmeshIndex<B::PrimalNodeIndex>,
) -> Option<
MaybeReversed<
&Arc<[RelaxedPath<B::EtchedPath, B::GapComment>]>,
RelaxedPath<B::EtchedPath, B::GapComment>,
>,
> {
resolve_edge_data(&self.edges, from_node, to_node).map(|(_, item)| item)
}
pub fn edge_data_mut(
&mut self,
from_node: NavmeshIndex<B::PrimalNodeIndex>,
to_node: NavmeshIndex<B::PrimalNodeIndex>,
) -> Option<
MaybeReversed<
&mut Arc<[RelaxedPath<B::EtchedPath, B::GapComment>]>,
RelaxedPath<B::EtchedPath, B::GapComment>,
>,
> {
resolve_edge_data_mut(&mut self.edges, from_node, to_node).map(|(_, item)| item)
}
/// See [`find_other_end`](planarr::find_other_end).
pub fn planarr_find_other_end(
&self,
node: &NavmeshIndex<B::PrimalNodeIndex>,
start: &NavmeshIndex<B::PrimalNodeIndex>,
pos: usize,
already_inserted_at_start: bool,
stop: &NavmeshIndex<B::PrimalNodeIndex>,
) -> Option<(usize, planarr::OtherEnd)> {
planarr::find_other_end(
self.nodes[node].neighs.iter().map(move |neigh| {
let edge = self
.edge_data(node.clone(), neigh.clone())
.expect("unable to resolve neighbor");
(neigh.clone(), edge)
}),
start,
pos,
already_inserted_at_start,
stop,
)
}
/// See [`find_all_other_ends`](planarr::find_all_other_ends).
pub fn planarr_find_all_other_ends<'a>(
&'a self,
node: &'a NavmeshIndex<B::PrimalNodeIndex>,
start: &'a NavmeshIndex<B::PrimalNodeIndex>,
pos: usize,
already_inserted_at_start: bool,
) -> Option<(
usize,
impl Iterator<Item = (NavmeshIndex<B::PrimalNodeIndex>, planarr::OtherEnd)> + 'a,
)> {
planarr::find_all_other_ends(
self.nodes[node].neighs.iter().map(move |neigh| {
let edge = self
.edge_data(node.clone(), neigh.clone())
.expect("unable to resolve neighbor");
(neigh.clone(), edge)
}),
start,
pos,
already_inserted_at_start,
)
}
}
/// Removes a path (weak or normal) with the given label from the navmesh
pub fn remove_path<EP, GC>(edge_paths: &mut [EdgePaths<EP, GC>], label: &RelaxedPath<EP, GC>)
where
@ -273,7 +365,8 @@ impl<'a, B: NavmeshBase + 'a> NavmeshRefMut<'a, B> {
Edge<&B::PrimalNodeIndex>,
MaybeReversed<usize, RelaxedPath<B::EtchedPath, B::GapComment>>,
)> {
resolve_edge_data(self.edges, from_node, to_node)
resolve_edge_data::<_, B::EtchedPath, _>(self.edges, from_node, to_node)
.map(|(edge, mayrev)| (edge, mayrev.map(|i: &usize| *i)))
}
/// Removes a path (weak or normal) with the given label from the navmesh
@ -309,7 +402,8 @@ impl<'a, B: NavmeshBase + 'a> NavmeshRef<'a, B> {
Edge<&B::PrimalNodeIndex>,
MaybeReversed<usize, RelaxedPath<B::EtchedPath, B::GapComment>>,
)> {
resolve_edge_data(self.edges, from_node, to_node)
resolve_edge_data::<_, B::EtchedPath, _>(self.edges, from_node, to_node)
.map(|(edge, mayrev)| (edge, mayrev.map(|i: &usize| *i)))
}
#[inline(always)]

View File

@ -17,7 +17,7 @@ use crate::{
///
/// This trait implements generic function for accessing or modifying different
/// compounds of board parts like nets or layers
pub trait AccessMesadata: AccessRules {
pub trait AccessMesadata: AccessRules + std::panic::RefUnwindSafe {
/// Renames a layer based on its index.
fn bename_layer(&mut self, layer: usize, layername: String);

View File

@ -268,6 +268,7 @@ impl PlaceActions {
pub struct RouteActions {
pub autoroute: Trigger,
pub topo_autoroute: Trigger,
}
impl RouteActions {
@ -279,6 +280,12 @@ impl RouteActions {
egui::Key::R,
)
.into_trigger(),
topo_autoroute: Action::new(
tr.text("tr-menu-route-topo-autoroute"),
egui::Modifiers::CTRL | egui::Modifiers::SHIFT,
egui::Key::R,
)
.into_trigger(),
}
}
@ -294,6 +301,7 @@ impl RouteActions {
ui.add_enabled_ui(have_workspace, |ui| {
ui.add_enabled_ui(workspace_activities_enabled, |ui| {
self.autoroute.button(ctx, ui);
self.topo_autoroute.button(ctx, ui);
});
ui.separator();

View File

@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: MIT
use std::{ops::ControlFlow, path::Path, sync::mpsc::Sender};
use std::{collections::BTreeSet, ops::ControlFlow, path::Path, sync::mpsc::Sender};
use topola::{
autorouter::{
@ -274,17 +274,22 @@ impl MenuBar {
.recalculate_topo_navmesh
.consume_key_triggered(ctx, ui)
{
let board = workspace.interactor.invoker().autorouter().board();
workspace
.overlay
.recalculate_topo_navmesh(board, &workspace.appearance_panel);
if let Some(active_layer) = workspace.appearance_panel.active_layer {
let board = workspace.interactor.invoker().autorouter().board();
workspace
.overlay
.recalculate_topo_navmesh(board, active_layer);
}
} else if actions.place.place_via.consume_key_enabled(
ctx,
ui,
&mut self.is_placing_via,
) {
} else if workspace_activities_enabled {
let mut schedule = |op: fn(Selection, AutorouterOptions) -> Command| {
fn schedule<F: FnOnce(Selection) -> Command>(
workspace: &mut Workspace,
op: F,
) {
let mut selection = workspace.overlay.take_selection();
if let Some(active_layer) = workspace.appearance_panel.active_layer {
let active_layer = workspace
@ -301,14 +306,34 @@ impl MenuBar {
.0
.retain(|i| i.layer == active_layer);
}
workspace
.interactor
.schedule(op(selection, self.autorouter_options));
};
workspace.interactor.schedule(op(selection));
}
let opts = self.autorouter_options;
if actions.edit.remove_bands.consume_key_triggered(ctx, ui) {
schedule(|selection, _| Command::RemoveBands(selection.band_selection));
schedule(workspace, |selection| {
Command::RemoveBands(selection.band_selection)
})
} else if actions.route.topo_autoroute.consume_key_triggered(ctx, ui) {
if let Some(active_layer) = workspace.appearance_panel.active_layer {
let active_layer = workspace
.interactor
.invoker()
.autorouter()
.board()
.layout()
.rules()
.layer_layername(active_layer)
.expect("unknown active layer")
.to_string();
schedule(workspace, |selection| Command::TopoAutoroute {
selection: selection.pin_selection,
allowed_edges: BTreeSet::new(),
active_layer,
routed_band_width: opts.router_options.routed_band_width,
});
}
} else if actions.route.autoroute.consume_key_triggered(ctx, ui) {
schedule(|selection, opts| {
schedule(workspace, |selection| {
Command::Autoroute(selection.pin_selection, opts)
});
} else if actions
@ -316,7 +341,7 @@ impl MenuBar {
.compare_detours
.consume_key_triggered(ctx, ui)
{
schedule(|selection, opts| {
schedule(workspace, |selection| {
Command::CompareDetours(selection.pin_selection, opts)
});
} else if actions
@ -324,7 +349,7 @@ impl MenuBar {
.measure_length
.consume_key_triggered(ctx, ui)
{
schedule(|selection, _| {
schedule(workspace, |selection| {
Command::MeasureLength(selection.band_selection)
});
} else if actions

View File

@ -12,24 +12,11 @@ use topola::{
selection::{BboxSelectionKind, Selection},
},
board::{AccessMesadata, Board},
layout::NodeIndex,
router::planar_incr_embed,
router::ng::{calculate_navmesh as ng_calculate_navmesh, PieNavmesh},
};
use crate::appearance_panel::AppearancePanel;
#[derive(Clone, Copy, Debug)]
pub struct PieNavmeshBase;
impl planar_incr_embed::NavmeshBase for PieNavmeshBase {
type PrimalNodeIndex = NodeIndex;
type EtchedPath = planar_incr_embed::navmesh::EdgeIndex<NodeIndex>;
type GapComment = ();
type Scalar = f64;
}
pub type PieNavmesh = planar_incr_embed::navmesh::Navmesh<PieNavmeshBase>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SelectionMode {
Addition,
@ -81,48 +68,10 @@ impl Overlay {
pub fn recalculate_topo_navmesh(
&mut self,
board: &Board<impl AccessMesadata>,
appearance_panel: &AppearancePanel,
active_layer: usize,
) {
use spade::Triangulation;
use topola::router::planar_incr_embed::navmesh::TrianVertex;
let Some(active_layer) = appearance_panel.active_layer else {
return;
};
if let Ok(triangulation) =
spade::DelaunayTriangulation::<TrianVertex<NodeIndex, f64>>::bulk_load(
board
.layout()
.drawing()
.rtree()
.locate_in_envelope_intersecting(&AABB::<[f64; 3]>::from_corners(
[-f64::INFINITY, -f64::INFINITY, active_layer as f64],
[f64::INFINITY, f64::INFINITY, active_layer as f64],
))
.map(|&geom| geom.data)
.filter_map(|node| {
board
.layout()
.apex_of_compoundless_node(node, active_layer)
.map(|(_, pos)| (node, pos))
})
.map(|(idx, pos)| TrianVertex {
idx,
pos: spade::mitigate_underflow(spade::Point2 {
x: pos.x(),
y: pos.y(),
}),
})
.collect(),
)
{
self.planar_incr_navmesh = Some(
planar_incr_embed::navmesh::NavmeshSer::<PieNavmeshBase>::from_triangulation(
&triangulation,
)
.into(),
);
if let Ok(pien) = ng_calculate_navmesh(board, active_layer) {
self.planar_incr_navmesh = Some(pien);
}
}

View File

@ -15,12 +15,12 @@ impl StatusBar {
Self {}
}
pub fn update(
pub fn update<M>(
&mut self,
ctx: &egui::Context,
_tr: &Translator,
viewport: &Viewport,
maybe_activity: Option<&ActivityStepperWithStatus>,
maybe_activity: Option<&ActivityStepperWithStatus<M>>,
) {
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
let latest_pos = viewport.transform.inverse()

View File

@ -10,7 +10,8 @@ use petgraph::{
use rstar::{Envelope, AABB};
use topola::{
autorouter::invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetActivePolygons, GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper,
GetMaybeTopoNavmesh, GetNavmeshDebugTexts, GetObstacles, GetPolygonalBlockers,
},
board::AccessMesadata,
drawing::{
@ -26,6 +27,7 @@ use topola::{
layout::poly::MakePolygon,
math::{Circle, RotationSense},
router::navmesh::NavnodeIndex,
router::ng::pie,
};
use crate::{
@ -184,6 +186,12 @@ impl Viewport {
let layers = &mut workspace.appearance_panel;
let overlay = &mut workspace.overlay;
let board = workspace.interactor.invoker().autorouter().board();
let active_polygons = workspace
.interactor
.maybe_activity()
.as_ref()
.map(|i| i.active_polygons())
.unwrap_or_default();
for i in (0..layers.visible.len()).rev() {
if layers.visible[i] {
@ -231,6 +239,7 @@ impl Viewport {
let color = if overlay
.selection()
.contains_node(board, GenericNode::Compound(poly.into()))
|| active_polygons.iter().find(|&&i| i == poly).is_some()
{
config
.colors(ctx)
@ -402,14 +411,26 @@ impl Viewport {
}
if menu_bar.show_topo_navmesh {
if let Some(navmesh) = workspace.overlay.planar_incr_navmesh() {
if let Some(navmesh) = workspace
.interactor
.maybe_activity()
.as_ref()
.and_then(|i| i.maybe_topo_navmesh())
.or_else(|| {
workspace
.overlay
.planar_incr_navmesh()
.as_ref()
.map(|navmesh| navmesh.as_ref())
})
{
// calculate dual node position approximations
use std::collections::BTreeMap;
use topola::geometry::shape::AccessShape;
use topola::router::planar_incr_embed::NavmeshIndex;
use topola::router::ng::pie::NavmeshIndex;
let mut map = BTreeMap::new();
let resolve_primal = |p: &topola::layout::NodeIndex| {
board.layout().node_shape(*p).center()
let resolve_primal = |p: &topola::drawing::dot::FixedDotIndex| {
(*p).primitive(board.layout().drawing()).shape().center()
};
for (nidx, node) in &*navmesh.nodes {
@ -444,24 +465,58 @@ impl Viewport {
Some(&x) => x,
},
};
let edge_len = navmesh.edge_paths[edge.1].len();
let lhs_pos = edge.0.lhs.map(|i| resolve_primal(&i));
let rhs_pos = edge.0.rhs.map(|i| resolve_primal(&i));
use egui::Color32;
let stroke = if edge_len == 0 {
egui::Stroke::new(
1.0,
if got_primal {
Color32::from_rgb(255, 175, 0)
} else {
Color32::from_rgb(159, 255, 33)
},
)
} else {
egui::Stroke::new(
1.5 + (edge_len as f32).atan(),
Color32::from_rgb(250, 250, 0),
)
let make_stroke = |len: usize| {
if len == 0 {
egui::Stroke::new(
0.5,
if got_primal {
Color32::from_rgb(255, 175, 0)
} else {
Color32::from_rgb(159, 255, 33)
},
)
} else {
egui::Stroke::new(
1.0 + (len as f32).atan(),
Color32::from_rgb(250, 250, 0),
)
}
};
painter.paint_edge(a_pos, b_pos, stroke);
if let (Some(lhs), Some(rhs)) = (lhs_pos, rhs_pos) {
let edge_lens = navmesh.edge_paths[edge.1]
.split(|x| x == &pie::RelaxedPath::Weak(()))
.collect::<Vec<_>>();
assert_eq!(edge_lens.len(), 3);
let middle = (a_pos + b_pos) / 2.0;
let mut offset_lhs = lhs - middle;
offset_lhs /= offset_lhs.dot(offset_lhs).sqrt() / 50.0;
let mut offset_rhs = rhs - middle;
offset_rhs /= offset_rhs.dot(offset_rhs).sqrt() / 50.0;
painter.paint_edge(
a_pos + offset_lhs,
b_pos + offset_lhs,
make_stroke(edge_lens[0].len()),
);
painter.paint_edge(
a_pos,
b_pos,
make_stroke(edge_lens[1].len()),
);
painter.paint_edge(
a_pos + offset_rhs,
b_pos + offset_rhs,
make_stroke(edge_lens[2].len()),
);
} else {
let edge_len = navmesh.edge_paths[edge.1]
.iter()
.filter(|i| matches!(i, pie::RelaxedPath::Normal(_)))
.count();
painter.paint_edge(a_pos, b_pos, make_stroke(edge_len));
}
}
}
}
@ -476,7 +531,7 @@ impl Viewport {
}
if let Some(activity) = workspace.interactor.maybe_activity() {
for ghost in activity.ghosts().iter() {
for ghost in activity.ghosts() {
painter
.paint_primitive(ghost, egui::Color32::from_rgb(75, 75, 150));
}
@ -490,6 +545,13 @@ impl Viewport {
);
}
for linestring in activity.polygonal_blockers() {
painter.paint_linestring(
linestring,
egui::Color32::from_rgb(115, 0, 255),
);
}
if let Some(ref navmesh) =
activity.maybe_thetastar().map(|astar| astar.graph())
{

View File

@ -36,6 +36,7 @@ tr-menu-place-place-route-plan = Place Route Plan
tr-menu-route = Route
tr-menu-route-autoroute = Autoroute
tr-menu-route-topo-autoroute = Topological single-layer Autoroute
tr-menu-route-routed-band-width = Routed Band Width
tr-menu-help = Help
@ -65,6 +66,10 @@ tr-dialog-error-messages = Error Messages
tr-dialog-error-messages-reset = Reset Messages
tr-dialog-error-messages-discard = Discard
tr-dialog-init-topo-navmesh = Initialize Topological Navmesh
tr-choose-active-layer-to-use = Choose active layer to use!
tr-dialog-init-topo-navmesh-submit = Run
tr-module-specctra-dsn-file-loader = Specctra DSN file loader
tr-module-history-file-loader = History file loader
tr-module-invoker = Invoker

View File

@ -23,6 +23,7 @@ use crate::{
use super::{
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetPolygonalBlockers,
},
Autorouter, AutorouterError, AutorouterOptions,
};
@ -187,6 +188,8 @@ impl GetGhosts for AutorouteExecutionStepper {
}
}
impl GetPolygonalBlockers for AutorouteExecutionStepper {}
impl GetObstacles for AutorouteExecutionStepper {
fn obstacles(&self) -> &[PrimitiveIndex] {
self.route.as_ref().map_or(&[], |route| route.obstacles())

View File

@ -7,6 +7,7 @@ use geo::Point;
use petgraph::graph::{EdgeIndex, NodeIndex};
use serde::{Deserialize, Serialize};
use spade::InsertionError;
use std::collections::BTreeSet;
use thiserror::Error;
use crate::{
@ -14,7 +15,7 @@ use crate::{
drawing::{band::BandTermsegIndex, dot::FixedDotIndex, Infringement},
graph::MakeRef,
layout::{via::ViaWeight, LayoutEdit},
router::{navmesh::NavmeshError, thetastar::ThetastarError, RouterOptions},
router::{navmesh::NavmeshError, ng, thetastar::ThetastarError, RouterOptions},
triangulation::GetTrianvertexNodeIndex,
};
@ -43,6 +44,8 @@ pub enum AutorouterError {
Navmesh(#[from] NavmeshError),
#[error("routing failed: {0}")]
Thetastar(#[from] ThetastarError),
#[error(transparent)]
Spade(#[from] spade::InsertionError),
#[error("could not place via")]
CouldNotPlaceVia(#[from] Infringement),
#[error("could not remove band")]
@ -132,6 +135,95 @@ impl<M: AccessMesadata> Autorouter<M> {
Ok(())
}
pub fn topo_autoroute(
&mut self,
selection: &PinSelection,
allowed_edges: BTreeSet<ng::PieEdgeIndex>,
active_layer: usize,
width: f64,
init_navmesh: Option<ng::PieNavmesh>,
) -> Result<ng::AutorouteExecutionStepper<M>, AutorouterError>
where
M: Clone,
{
self.topo_autoroute_ratlines(
self.selected_ratlines(selection),
allowed_edges,
active_layer,
width,
init_navmesh,
)
}
pub(super) fn topo_autoroute_ratlines(
&mut self,
ratlines: Vec<EdgeIndex<usize>>,
allowed_edges: BTreeSet<ng::PieEdgeIndex>,
active_layer: usize,
width: f64,
init_navmesh: Option<ng::PieNavmesh>,
) -> Result<ng::AutorouteExecutionStepper<M>, AutorouterError>
where
M: Clone,
{
let navmesh = if let Some(x) = init_navmesh {
x
} else {
ng::calculate_navmesh(&self.board, active_layer)?
};
let mut got_any_valid_goals = false;
use ng::pie::NavmeshIndex;
let ret = ng::AutorouteExecutionStepper::new(
self.board.layout(),
&navmesh,
self.board
.bands_by_id()
.iter()
.map(|(&k, &v)| (k, v))
.collect(),
active_layer,
allowed_edges,
ratlines.into_iter().filter_map(|ratline| {
let (source, target) = self.ratline_endpoints(ratline);
if navmesh
.as_ref()
.node_data(&NavmeshIndex::Primal(source))
.is_none()
|| navmesh
.as_ref()
.node_data(&NavmeshIndex::Primal(target))
.is_none()
{
// e.g. due to wrong active layer
return None;
}
if self.board.band_between_nodes(source, target).is_some() {
// already connected
return None;
}
got_any_valid_goals = true;
Some(ng::Goal {
source,
target,
width,
})
}),
);
if !got_any_valid_goals {
Err(AutorouterError::NothingToRoute)
} else {
Ok(ret)
}
}
pub fn place_via(
&self,
weight: ViaWeight,

View File

@ -22,6 +22,7 @@ use super::{
autoroute::{AutorouteContinueStatus, AutorouteExecutionStepper},
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetPolygonalBlockers,
},
Autorouter, AutorouterError, AutorouterOptions,
};
@ -120,6 +121,8 @@ impl GetGhosts for CompareDetoursExecutionStepper {
}
}
impl GetPolygonalBlockers for CompareDetoursExecutionStepper {}
impl GetObstacles for CompareDetoursExecutionStepper {
fn obstacles(&self) -> &[PrimitiveIndex] {
self.autoroute.obstacles()

View File

@ -2,21 +2,23 @@
//
// SPDX-License-Identifier: MIT
use std::ops::ControlFlow;
use std::{collections::BTreeSet, ops::ControlFlow};
use enum_dispatch::enum_dispatch;
use serde::{Deserialize, Serialize};
use crate::{
board::AccessMesadata,
layout::{via::ViaWeight, LayoutEdit},
graph::GenericIndex,
layout::{poly::PolyWeight, via::ViaWeight, LayoutEdit},
router::ng,
stepper::{Abort, Step},
};
use super::{
autoroute::AutorouteExecutionStepper,
compare_detours::CompareDetoursExecutionStepper,
invoker::{Invoker, InvokerError},
invoker::{GetActivePolygons, GetMaybeTopoNavmesh, Invoker, InvokerError},
measure_length::MeasureLengthExecutionStepper,
place_via::PlaceViaExecutionStepper,
remove_bands::RemoveBandsExecutionStepper,
@ -29,6 +31,13 @@ type Type = PinSelection;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Command {
Autoroute(PinSelection, AutorouterOptions),
TopoAutoroute {
selection: PinSelection,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
allowed_edges: BTreeSet<ng::PieEdgeIndex>,
active_layer: String,
routed_band_width: f64,
},
PlaceVia(ViaWeight),
RemoveBands(BandSelection),
CompareDetours(Type, AutorouterOptions),
@ -39,19 +48,21 @@ pub enum Command {
GetMaybeThetastarStepper,
GetMaybeNavcord,
GetGhosts,
GetPolygonalBlockers,
GetObstacles,
GetNavmeshDebugTexts
)]
pub enum ExecutionStepper {
pub enum ExecutionStepper<M> {
Autoroute(AutorouteExecutionStepper),
TopoAutoroute(ng::AutorouteExecutionStepper<M>),
PlaceVia(PlaceViaExecutionStepper),
RemoveBands(RemoveBandsExecutionStepper),
CompareDetours(CompareDetoursExecutionStepper),
MeasureLength(MeasureLengthExecutionStepper),
}
impl ExecutionStepper {
fn step_catch_err<M: AccessMesadata>(
impl<M: AccessMesadata + Clone> ExecutionStepper<M> {
fn step_catch_err(
&mut self,
autorouter: &mut Autorouter<M>,
) -> Result<ControlFlow<(Option<LayoutEdit>, String)>, InvokerError> {
@ -62,6 +73,29 @@ impl ExecutionStepper {
ControlFlow::Break((edit, "finished autorouting".to_string()))
}
},
ExecutionStepper::TopoAutoroute(autoroute) => {
let ret = match autoroute.step() {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(false) => {
ControlFlow::Break((None, "topo-autorouting failed".to_string()))
}
ControlFlow::Break(true) => {
for (ep, band) in &autoroute.last_bands {
let (source, target) = ep.end_points.into();
autorouter
.board
.try_set_band_between_nodes(source, target, *band);
}
ControlFlow::Break((
Some(autoroute.last_recorder.clone()),
"finished topo-autorouting".to_string(),
))
}
};
// TODO: maintain topo-navmesh just like layout
*autorouter.board.layout_mut() = autoroute.last_layout.clone();
ret
}
ExecutionStepper::PlaceVia(place_via) => {
let edit = place_via.doit(autorouter)?;
ControlFlow::Break((edit, "finished placing via".to_string()))
@ -90,7 +124,7 @@ impl ExecutionStepper {
}
}
impl<M: AccessMesadata> Step<Invoker<M>, String> for ExecutionStepper {
impl<M: AccessMesadata + Clone> Step<Invoker<M>, String> for ExecutionStepper<M> {
type Error = InvokerError;
fn step(&mut self, invoker: &mut Invoker<M>) -> Result<ControlFlow<String>, InvokerError> {
@ -111,9 +145,36 @@ impl<M: AccessMesadata> Step<Invoker<M>, String> for ExecutionStepper {
}
}
impl<M: AccessMesadata> Abort<Invoker<M>> for ExecutionStepper {
fn abort(&mut self, context: &mut Invoker<M>) {
// TODO: fix this
self.finish(context);
impl<M: AccessMesadata + Clone> Abort<Invoker<M>> for ExecutionStepper<M> {
fn abort(&mut self, invoker: &mut Invoker<M>) {
match self {
ExecutionStepper::TopoAutoroute(autoroute) => {
autoroute.abort(&mut ());
// TODO: maintain topo-navmesh just like layout
*invoker.autorouter.board.layout_mut() = autoroute.last_layout.clone();
}
execution => {
// TODO
execution.finish(invoker);
}
}
}
}
impl<M> GetActivePolygons for ExecutionStepper<M> {
fn active_polygons(&self) -> &[GenericIndex<PolyWeight>] {
match self {
ExecutionStepper::TopoAutoroute(autoroute) => autoroute.active_polygons(),
_ => &[],
}
}
}
impl<M> GetMaybeTopoNavmesh for ExecutionStepper<M> {
fn maybe_topo_navmesh(&self) -> Option<ng::pie::navmesh::NavmeshRef<'_, ng::PieNavmeshBase>> {
match self {
ExecutionStepper::TopoAutoroute(autoroute) => autoroute.maybe_topo_navmesh(),
_ => None,
}
}
}

View File

@ -9,15 +9,19 @@ use std::{cmp::Ordering, ops::ControlFlow};
use contracts_try::debug_requires;
use derive_getters::{Dissolve, Getters};
use enum_dispatch::enum_dispatch;
use geo::geometry::LineString;
use thiserror::Error;
use crate::{
board::AccessMesadata,
drawing::graph::PrimitiveIndex,
geometry::{edit::ApplyGeometryEdit, primitive::PrimitiveShape},
graph::GenericIndex,
layout::poly::PolyWeight,
router::{
navcord::Navcord,
navmesh::{Navmesh, NavnodeIndex},
ng,
thetastar::ThetastarStepper,
},
stepper::Step,
@ -61,6 +65,30 @@ pub trait GetGhosts {
}
}
/// Getter for the polygonal blockers (polygonal regions which block routing)
#[enum_dispatch]
pub trait GetPolygonalBlockers {
fn polygonal_blockers(&self) -> &[LineString] {
&[]
}
}
/// Getter for the polygons around which some routing happens
#[enum_dispatch]
pub trait GetActivePolygons {
fn active_polygons(&self) -> &[GenericIndex<PolyWeight>] {
&[]
}
}
/// Getter trait to obtain Topological/Planar Navigation Mesh
#[enum_dispatch]
pub trait GetMaybeTopoNavmesh {
fn maybe_topo_navmesh(&self) -> Option<ng::pie::navmesh::NavmeshRef<'_, ng::PieNavmeshBase>> {
None
}
}
/// Trait for getting the obstacles that prevented Topola from creating
/// new objects (the shapes of these objects can be obtained with the above
/// `GetGhosts` trait), for the purpose of displaying these obstacles on the
@ -72,9 +100,9 @@ pub trait GetObstacles {
}
}
#[enum_dispatch]
/// Trait for getting text strings with debug information attached to navmesh
/// edges and vertices.
#[enum_dispatch]
pub trait GetNavmeshDebugTexts {
fn navnode_debug_text(&self, _navnode: NavnodeIndex) -> Option<&str> {
None
@ -107,7 +135,7 @@ pub struct Invoker<M> {
pub(super) ongoing_command: Option<Command>,
}
impl<M: AccessMesadata> Invoker<M> {
impl<M: AccessMesadata + Clone> Invoker<M> {
/// Creates a new instance of Invoker with the given autorouter instance
pub fn new(autorouter: Autorouter<M>) -> Self {
Self::new_with_history(autorouter, History::new())
@ -144,14 +172,17 @@ impl<M: AccessMesadata> Invoker<M> {
/// Pass given command to be executed.
///
/// Function used to set given [`Command`] to ongoing state, dispatch and execute it.
pub fn execute_stepper(&mut self, command: Command) -> Result<ExecutionStepper, InvokerError> {
pub fn execute_stepper(
&mut self,
command: Command,
) -> Result<ExecutionStepper<M>, InvokerError> {
let execute = self.dispatch_command(&command);
self.ongoing_command = Some(command);
execute
}
#[debug_requires(self.ongoing_command.is_none())]
fn dispatch_command(&mut self, command: &Command) -> Result<ExecutionStepper, InvokerError> {
fn dispatch_command(&mut self, command: &Command) -> Result<ExecutionStepper<M>, InvokerError> {
Ok(match command {
Command::Autoroute(selection, options) => {
let mut ratlines = self.autorouter.selected_ratlines(selection);
@ -172,6 +203,31 @@ impl<M: AccessMesadata> Invoker<M> {
ExecutionStepper::Autoroute(self.autorouter.autoroute_ratlines(ratlines, *options)?)
}
Command::TopoAutoroute {
selection,
allowed_edges,
active_layer,
routed_band_width,
} => {
let ratlines = self.autorouter.selected_ratlines(selection);
// TODO: consider "presort by pairwise detours"
ExecutionStepper::TopoAutoroute(
self.autorouter.topo_autoroute_ratlines(
ratlines,
allowed_edges.clone(),
self.autorouter
.board
.layout()
.rules()
.layername_layer(active_layer)
.unwrap(),
*routed_band_width,
None,
)?,
)
}
Command::PlaceVia(weight) => {
ExecutionStepper::PlaceVia(self.autorouter.place_via(*weight)?)
}

View File

@ -13,6 +13,7 @@ use crate::{
use super::{
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetPolygonalBlockers,
},
selection::BandSelection,
Autorouter, AutorouterError,
@ -53,8 +54,9 @@ impl MeasureLengthExecutionStepper {
}
}
impl GetMaybeThetastarStepper for MeasureLengthExecutionStepper {}
impl GetMaybeNavcord for MeasureLengthExecutionStepper {}
impl GetGhosts for MeasureLengthExecutionStepper {}
impl GetObstacles for MeasureLengthExecutionStepper {}
impl GetMaybeNavcord for MeasureLengthExecutionStepper {}
impl GetMaybeThetastarStepper for MeasureLengthExecutionStepper {}
impl GetNavmeshDebugTexts for MeasureLengthExecutionStepper {}
impl GetObstacles for MeasureLengthExecutionStepper {}
impl GetPolygonalBlockers for MeasureLengthExecutionStepper {}

View File

@ -14,6 +14,7 @@ use crate::{
use super::{
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetPolygonalBlockers,
},
Autorouter, AutorouterError,
};
@ -51,8 +52,9 @@ impl PlaceViaExecutionStepper {
}
}
impl GetMaybeThetastarStepper for PlaceViaExecutionStepper {}
impl GetMaybeNavcord for PlaceViaExecutionStepper {}
impl GetGhosts for PlaceViaExecutionStepper {}
impl GetObstacles for PlaceViaExecutionStepper {}
impl GetMaybeNavcord for PlaceViaExecutionStepper {}
impl GetMaybeThetastarStepper for PlaceViaExecutionStepper {}
impl GetNavmeshDebugTexts for PlaceViaExecutionStepper {}
impl GetObstacles for PlaceViaExecutionStepper {}
impl GetPolygonalBlockers for PlaceViaExecutionStepper {}

View File

@ -9,6 +9,7 @@ use crate::{board::AccessMesadata, layout::LayoutEdit};
use super::{
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
GetPolygonalBlockers,
},
selection::BandSelection,
Autorouter, AutorouterError,
@ -37,8 +38,11 @@ impl RemoveBandsExecutionStepper {
let mut edit = LayoutEdit::new();
for selector in self.selection.selectors() {
let band = autorouter.board.bandname_band(&selector.band).unwrap()[false];
autorouter.board.layout_mut().remove_band(&mut edit, band);
let band = *autorouter.board.bandname_band(&selector.band).unwrap();
autorouter
.board
.remove_band_by_id(&mut edit, band)
.map_err(|_| AutorouterError::CouldNotRemoveBand(band[false]))?;
}
Ok(Some(edit))
} else {
@ -47,8 +51,9 @@ impl RemoveBandsExecutionStepper {
}
}
impl GetMaybeThetastarStepper for RemoveBandsExecutionStepper {}
impl GetMaybeNavcord for RemoveBandsExecutionStepper {}
impl GetGhosts for RemoveBandsExecutionStepper {}
impl GetObstacles for RemoveBandsExecutionStepper {}
impl GetMaybeNavcord for RemoveBandsExecutionStepper {}
impl GetMaybeThetastarStepper for RemoveBandsExecutionStepper {}
impl GetNavmeshDebugTexts for RemoveBandsExecutionStepper {}
impl GetObstacles for RemoveBandsExecutionStepper {}
impl GetPolygonalBlockers for RemoveBandsExecutionStepper {}

View File

@ -20,11 +20,12 @@ use crate::{
dot::{DotIndex, DotWeight, FixedDotIndex, FixedDotWeight},
graph::PrimitiveIndex,
seg::{FixedSegIndex, FixedSegWeight, SegIndex, SegWeight},
Collect,
Collect, DrawingException,
},
geometry::{edit::ApplyGeometryEdit, GenericNode, GetLayer},
graph::{GenericIndex, MakeRef},
layout::{poly::PolyWeight, CompoundEntryLabel, CompoundWeight, Layout, LayoutEdit, NodeIndex},
router::ng::EtchedPath,
};
/// Represents a band between two pins.
@ -77,6 +78,7 @@ impl<'a> ResolvedSelector<'a> {
#[derive(Debug, Getters)]
pub struct Board<M> {
layout: Layout<M>,
bands_by_id: BiBTreeMap<EtchedPath, BandUid>,
// TODO: Simplify access logic to these members so that `#[getter(skip)]`s can be removed.
#[getter(skip)]
node_to_pinname: BTreeMap<NodeIndex, String>,
@ -89,6 +91,7 @@ impl<M> Board<M> {
pub fn new(layout: Layout<M>) -> Self {
Self {
layout,
bands_by_id: BiBTreeMap::new(),
node_to_pinname: BTreeMap::new(),
band_bandname: BiBTreeMap::new(),
}
@ -216,6 +219,10 @@ impl<M: AccessMesadata> Board<M> {
if self.band_bandname.get_by_right(&bandname).is_some() {
false
} else {
let ep = EtchedPath {
end_points: (source, target).into(),
};
self.bands_by_id.insert(ep, band);
self.band_bandname.insert(band, bandname);
true
}
@ -235,6 +242,46 @@ impl<M: AccessMesadata> Board<M> {
self.band_between_pins(source_pinname, target_pinname)
}
/// Removes the band between the two nodes
pub fn remove_band_between_nodes(
&mut self,
recorder: &mut LayoutEdit,
source: FixedDotIndex,
target: FixedDotIndex,
) -> Result<(), DrawingException> {
let ep = EtchedPath {
end_points: (source, target).into(),
};
let source_pinname = self
.node_pinname(&GenericNode::Primitive(source.into()))
.unwrap()
.to_string();
let target_pinname = self
.node_pinname(&GenericNode::Primitive(target.into()))
.unwrap()
.to_string();
self.band_bandname
.remove_by_right(&BandName::from((source_pinname, target_pinname)));
if let Some((_, uid)) = self.bands_by_id.remove_by_left(&ep) {
let (from, _) = uid.into();
self.layout.remove_band(recorder, from)?;
}
Ok(())
}
/// Removes the band between two nodes given by [`BandUid`]
pub fn remove_band_by_id(
&mut self,
recorder: &mut LayoutEdit,
uid: BandUid,
) -> Result<(), DrawingException> {
if let Some(ep) = self.bands_by_id.get_by_right(&uid) {
let (source, target) = ep.end_points.into();
self.remove_band_between_nodes(recorder, source, target)?;
}
Ok(())
}
/// Finds a band between two pin names.
pub fn band_between_pins(&self, pinname1: &str, pinname2: &str) -> Option<BandUid> {
self.band_bandname

View File

@ -36,6 +36,15 @@ impl<'a, CW: 'a, Cel: 'a, R: 'a> MakeRef<'a, Drawing<CW, Cel, R>> for Head {
}
}
impl Head {
pub fn maybe_cane(&self) -> Option<Cane> {
match self {
Head::Bare(..) => None,
Head::Cane(head) => Some(head.cane),
}
}
}
/// The head is bare when the routed band is not pulled out (i.e. is of zero
/// length). This happens on the first routing step and when the routed band
/// was completely retracted due to the routing algorithm backtracking. In these

View File

@ -228,6 +228,25 @@ impl<
);
}
pub fn is_joined_with<I>(&self, seg: I, node: GenericNode<PI, GenericIndex<CW>>) -> bool
where
I: Copy + GetPetgraphIndex,
CW: Clone,
Cel: Copy,
{
match node {
GenericNode::Primitive(prim) => self
.graph
.find_edge_undirected(seg.petgraph_index(), prim.petgraph_index())
.map_or(false, |(eidx, _direction)| {
matches!(self.graph.edge_weight(eidx).unwrap(), GeometryLabel::Joined)
}),
GenericNode::Compound(comp) => self
.compound_members(comp)
.any(|(_cel, i)| self.is_joined_with(seg, GenericNode::Primitive(i))),
}
}
pub fn add_bend<W: AccessBendWeight + Into<PW>>(
&mut self,
from: DI,

View File

@ -2,27 +2,31 @@
//
// SPDX-License-Identifier: MIT
use std::ops::ControlFlow;
use core::ops::ControlFlow;
use enum_dispatch::enum_dispatch;
use geo::Point;
use geo::geometry::{LineString, Point};
use thiserror::Error;
use crate::{
autorouter::{
execution::ExecutionStepper,
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts,
GetObstacles, Invoker, InvokerError,
GetActivePolygons, GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper,
GetMaybeTopoNavmesh, GetNavmeshDebugTexts, GetObstacles, GetPolygonalBlockers, Invoker,
InvokerError,
},
},
board::AccessMesadata,
drawing::graph::PrimitiveIndex,
geometry::primitive::PrimitiveShape,
graph::GenericIndex,
interactor::interaction::{InteractionError, InteractionStepper},
layout::poly::PolyWeight,
router::{
navcord::Navcord,
navmesh::{Navmesh, NavnodeIndex},
ng,
thetastar::ThetastarStepper,
},
stepper::{Abort, OnEvent, Step},
@ -70,18 +74,21 @@ pub enum ActivityError {
/// An activity is either an interaction or an execution
#[enum_dispatch(
GetMaybeThetastarStepper,
GetMaybeNavcord,
GetActivePolygons,
GetGhosts,
GetMaybeNavcord,
GetMaybeThetastarStepper,
GetMaybeTopoNavmesh,
GetNavmeshDebugTexts,
GetObstacles,
GetNavmeshDebugTexts
GetPolygonalBlockers
)]
pub enum ActivityStepper {
pub enum ActivityStepper<M> {
Interaction(InteractionStepper),
Execution(ExecutionStepper),
Execution(ExecutionStepper<M>),
}
impl<M: AccessMesadata> Step<ActivityContext<'_, M>, String> for ActivityStepper {
impl<M: AccessMesadata + Clone> Step<ActivityContext<'_, M>, String> for ActivityStepper<M> {
type Error = ActivityError;
fn step(
@ -95,7 +102,7 @@ impl<M: AccessMesadata> Step<ActivityContext<'_, M>, String> for ActivityStepper
}
}
impl<M: AccessMesadata> Abort<Invoker<M>> for ActivityStepper {
impl<M: AccessMesadata + Clone> Abort<Invoker<M>> for ActivityStepper<M> {
fn abort(&mut self, context: &mut Invoker<M>) {
match self {
ActivityStepper::Interaction(interaction) => interaction.abort(context),
@ -104,7 +111,7 @@ impl<M: AccessMesadata> Abort<Invoker<M>> for ActivityStepper {
}
}
impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for ActivityStepper {
impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for ActivityStepper<M> {
type Output = Result<(), InteractionError>;
fn on_event(
@ -120,27 +127,27 @@ impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for Ac
}
/// An ActivityStepper that preserves its status
pub struct ActivityStepperWithStatus {
activity: ActivityStepper,
pub struct ActivityStepperWithStatus<M> {
activity: ActivityStepper<M>,
maybe_status: Option<ControlFlow<String>>,
}
impl ActivityStepperWithStatus {
pub fn new_execution(execution: ExecutionStepper) -> ActivityStepperWithStatus {
impl<M> ActivityStepperWithStatus<M> {
pub fn new_execution(execution: ExecutionStepper<M>) -> Self {
Self {
activity: ActivityStepper::Execution(execution),
maybe_status: None,
}
}
pub fn new_interaction(interaction: InteractionStepper) -> ActivityStepperWithStatus {
pub fn new_interaction(interaction: InteractionStepper) -> Self {
Self {
activity: ActivityStepper::Interaction(interaction),
maybe_status: None,
}
}
pub fn activity(&self) -> &ActivityStepper {
pub fn activity(&self) -> &ActivityStepper<M> {
&self.activity
}
@ -149,7 +156,9 @@ impl ActivityStepperWithStatus {
}
}
impl<M: AccessMesadata> Step<ActivityContext<'_, M>, String> for ActivityStepperWithStatus {
impl<M: AccessMesadata + Clone> Step<ActivityContext<'_, M>, String>
for ActivityStepperWithStatus<M>
{
type Error = ActivityError;
fn step(
@ -162,15 +171,15 @@ impl<M: AccessMesadata> Step<ActivityContext<'_, M>, String> for ActivityStepper
}
}
impl<M: AccessMesadata> Abort<Invoker<M>> for ActivityStepperWithStatus {
impl<M: AccessMesadata + Clone> Abort<Invoker<M>> for ActivityStepperWithStatus<M> {
fn abort(&mut self, context: &mut Invoker<M>) {
self.maybe_status = Some(ControlFlow::Break(String::from("aborted")));
self.activity.abort(context);
}
}
impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent>
for ActivityStepperWithStatus
impl<M: AccessMesadata + Clone> OnEvent<ActivityContext<'_, M>, InteractiveEvent>
for ActivityStepperWithStatus<M>
{
type Output = Result<(), InteractionError>;
@ -183,31 +192,49 @@ impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent>
}
}
impl GetMaybeThetastarStepper for ActivityStepperWithStatus {
impl<M> GetActivePolygons for ActivityStepperWithStatus<M> {
fn active_polygons(&self) -> &[GenericIndex<PolyWeight>] {
self.activity.active_polygons()
}
}
impl<M> GetMaybeThetastarStepper for ActivityStepperWithStatus<M> {
fn maybe_thetastar(&self) -> Option<&ThetastarStepper<Navmesh, f64>> {
self.activity.maybe_thetastar()
}
}
impl GetMaybeNavcord for ActivityStepperWithStatus {
impl<M> GetMaybeTopoNavmesh for ActivityStepperWithStatus<M> {
fn maybe_topo_navmesh(&self) -> Option<ng::pie::navmesh::NavmeshRef<'_, ng::PieNavmeshBase>> {
self.activity.maybe_topo_navmesh()
}
}
impl<M> GetMaybeNavcord for ActivityStepperWithStatus<M> {
fn maybe_navcord(&self) -> Option<&Navcord> {
self.activity.maybe_navcord()
}
}
impl GetGhosts for ActivityStepperWithStatus {
impl<M> GetGhosts for ActivityStepperWithStatus<M> {
fn ghosts(&self) -> &[PrimitiveShape] {
self.activity.ghosts()
}
}
impl GetObstacles for ActivityStepperWithStatus {
impl<M> GetPolygonalBlockers for ActivityStepperWithStatus<M> {
fn polygonal_blockers(&self) -> &[LineString] {
self.activity.polygonal_blockers()
}
}
impl<M> GetObstacles for ActivityStepperWithStatus<M> {
fn obstacles(&self) -> &[PrimitiveIndex] {
self.activity.obstacles()
}
}
impl GetNavmeshDebugTexts for ActivityStepperWithStatus {
impl<M> GetNavmeshDebugTexts for ActivityStepperWithStatus<M> {
fn navnode_debug_text(&self, navnode: NavnodeIndex) -> Option<&str> {
self.activity.navnode_debug_text(navnode)
}

View File

@ -7,8 +7,8 @@ use thiserror::Error;
use crate::{
autorouter::invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
Invoker,
GetActivePolygons, GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper,
GetMaybeTopoNavmesh, GetNavmeshDebugTexts, GetObstacles, GetPolygonalBlockers, Invoker,
},
board::AccessMesadata,
stepper::{Abort, OnEvent, Step},
@ -67,8 +67,11 @@ impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for In
}
}
impl GetActivePolygons for InteractionStepper {}
impl GetGhosts for InteractionStepper {}
impl GetMaybeThetastarStepper for InteractionStepper {}
impl GetMaybeNavcord for InteractionStepper {}
impl GetMaybeThetastarStepper for InteractionStepper {}
impl GetMaybeTopoNavmesh for InteractionStepper {}
impl GetNavmeshDebugTexts for InteractionStepper {}
impl GetObstacles for InteractionStepper {}
impl GetPolygonalBlockers for InteractionStepper {}

View File

@ -27,10 +27,10 @@ use crate::{
/// Structure that manages the invoker and activities
pub struct Interactor<M> {
invoker: Invoker<M>,
activity: Option<ActivityStepperWithStatus>,
activity: Option<ActivityStepperWithStatus<M>>,
}
impl<M: AccessMesadata> Interactor<M> {
impl<M: AccessMesadata + Clone> Interactor<M> {
/// Create a new instance of Interactor with the given Board instance
pub fn new(board: Board<M>) -> Result<Self, InsertionError> {
Ok(Self {
@ -135,7 +135,7 @@ impl<M: AccessMesadata> Interactor<M> {
}
/// Returns the currently running activity
pub fn maybe_activity(&self) -> &Option<ActivityStepperWithStatus> {
pub fn maybe_activity(&self) -> &Option<ActivityStepperWithStatus<M>> {
&self.activity
}
}

View File

@ -2,10 +2,13 @@
//
// SPDX-License-Identifier: MIT
use core::iter;
use contracts_try::debug_ensures;
use derive_getters::Getters;
use enum_dispatch::enum_dispatch;
use geo::Point;
use planar_incr_embed::RelaxedPath;
use rstar::AABB;
use crate::{
@ -376,6 +379,8 @@ impl<R: AccessRules> Layout<R> {
}
}
// TODO: computation of bands between outer node and direction towards "outside"
/// Finds all bands on `layer` between `left` and `right`
/// (usually assuming `left` and `right` are neighbors in a Delaunay triangulation)
/// and returns them ordered from `left` to `right`.
@ -384,7 +389,7 @@ impl<R: AccessRules> Layout<R> {
layer: usize,
left: NodeIndex,
right: NodeIndex,
) -> Vec<BandUid> {
) -> impl Iterator<Item = (BandUid, LooseIndex)> {
assert_ne!(left, right);
let left_pos = self.node_shape(left).center();
let right_pos = self.node_shape(right).center();
@ -454,7 +459,16 @@ impl<R: AccessRules> Layout<R> {
);
(0.0..=1.0)
.contains(&location)
.then_some((location, band_uid))
.then_some((location, band_uid, loose))
})
.filter(|(_, band_uid, _)| {
// filter entries which are connected to either lhs or rhs (and possibly both)
let (bts1, bts2) = band_uid.into();
let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index());
let geometry = self.drawing.geometry();
[(bts1, left), (bts1, right), (bts2, left), (bts2, right)]
.iter()
.all(|&(x, y)| !geometry.is_joined_with(x, y))
})
.collect();
bands.sort_by(|a, b| f64::total_cmp(&a.0, &b.0));
@ -463,7 +477,88 @@ impl<R: AccessRules> Layout<R> {
// both in the case of "edge" of a primitive/loose, and in case the band actually goes into a segment
// and then again out of it.
bands.into_iter().map(|(_, band_uid)| band_uid).collect()
bands
.into_iter()
.map(|(_, band_uid, loose)| (band_uid, loose))
}
fn does_compound_have_core(&self, primary: NodeIndex, core: DotIndex) -> bool {
let core: PrimitiveIndex = core.into();
match primary {
GenericNode::Primitive(pi) => pi == core,
GenericNode::Compound(compound) => self
.drawing
.geometry()
.compound_members(compound)
.any(|(_, pi)| pi == core),
}
}
/// Finds all bands on `layer` between `left` and `right`
/// (usually assuming `left` and `right` are neighbors in a Delaunay triangulation)
/// and returns them ordered from `left` to `right`.
pub fn bands_between_nodes_with_alignment(
&self,
layer: usize,
left: NodeIndex,
right: NodeIndex,
) -> impl Iterator<Item = RelaxedPath<BandUid, ()>> + '_ {
let mut alignment_idx: u8 = 0;
// resolve end-points possibly to compounds such that
// `does_compound_have_core` produces correct results.
let maybe_to_compound = |x: NodeIndex| match x {
GenericNode::Primitive(pi) => self
.drawing
.geometry()
.compounds(pi)
.next()
.map(|(_, idx)| idx),
GenericNode::Compound(compound) => Some(compound),
};
let resolve_node =
|x: NodeIndex| maybe_to_compound(x).map(GenericNode::Compound).unwrap_or(x);
let (left, right) = (resolve_node(left), resolve_node(right));
self.bands_between_nodes(layer, left, right)
.map(move |(band_uid, loose)| {
// first, tag entry with core
let maybe_core = match loose {
LooseIndex::Bend(lbi) => Some(self.drawing.geometry().core(lbi.into())),
_ => None,
};
Some((band_uid, maybe_core))
})
.chain(iter::once(None))
.flat_map(move |item| {
// insert alignment pseudo-paths if necessary
let alignment_incr = match item {
Some((_, maybe_core)) => match (alignment_idx, maybe_core) {
(0, Some(core)) if self.does_compound_have_core(left, core) => 0,
(_, Some(core)) if self.does_compound_have_core(left, core) => {
panic!("invalid band ordering")
}
(_, Some(core)) if self.does_compound_have_core(right, core) => 2u8
.checked_sub(alignment_idx)
.expect("invalid band ordering"),
(0, _) => 1,
(1, _) => 0,
_ => panic!("invalid band ordering"),
},
None => 2u8
.checked_sub(alignment_idx)
.expect("invalid band ordering"),
};
alignment_idx += alignment_incr;
iter::repeat_n(RelaxedPath::Weak(()), alignment_incr.into()).chain(
item.map(|(band_uid, _)| RelaxedPath::Normal(band_uid))
.into_iter(),
)
})
}
pub fn rules(&self) -> &R {

View File

@ -16,6 +16,7 @@
#![cfg_attr(not(feature = "disable_contracts"), feature(try_blocks))]
// TODO: fix all occurences
#![allow(unused_must_use)]
#![allow(clippy::too_many_arguments)]
pub mod graph;
#[macro_use]

View File

@ -15,6 +15,9 @@ pub use polygon_tangents::*;
mod tangents;
pub use tangents::*;
mod tunnel;
pub use tunnel::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RotationSense {
Counterclockwise,

View File

@ -8,6 +8,24 @@ use super::{
};
use geo::{algorithm::Centroid, Point, Polygon};
pub fn is_poly_convex_hull_cw<I>(poly_ext_hull: &[(Point, I)], pivot: usize) -> bool {
let len = poly_ext_hull.len();
if pivot >= len {
return false;
}
let prev = poly_ext_hull[(len + pivot - 1) % len].0 .0;
let curr = poly_ext_hull[pivot].0 .0;
let next = poly_ext_hull[(pivot + 1) % len].0 .0;
// see also: https://en.wikipedia.org/w/index.php?title=Curve_orientation&oldid=1250027587#Orientation_of_a_simple_polygon
#[rustfmt::skip]
let det = (curr.x * next.y + prev.x * curr.y + prev.y * next.x)
- (curr.y * next.x + prev.y * curr.x + prev.x * next.y);
det < 0.
}
#[derive(Clone, Debug, thiserror::Error, PartialEq)]
pub enum PolyTangentException<I> {
#[error("trying to target empty polygon")]

52
src/math/tunnel.rs Normal file
View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
//! Utilities for working with "tunnel vision"
//! (basically a simple kind of 2D ray tracing, where we are only interested
//! in incremental restriction / intersection of circle segments)
use crate::math::{between_vectors_cached, perp_dot_product};
use geo::Point;
#[derive(Clone, Copy, Debug)]
/// circle segment given as offsets from the origin
/// (not necessarily with the same radius, only the angle matters)
/// oriented counter-clockwise
pub struct Tunnel(pub Point, pub Point);
impl Tunnel {
fn cross(&self) -> f64 {
perp_dot_product(self.0, self.1)
}
fn between_vectors(&self, cross: f64, p: Point) -> bool {
between_vectors_cached(p, self.0, self.1, cross)
}
pub fn intersection(self, othr: &Self) -> Option<Self> {
let cross_self = self.cross();
let cross_othr = othr.cross();
// update segment data
let in_between = |p_self: Point, p_othr: Point| {
if p_self == p_othr {
Some(p_self)
} else if self.between_vectors(cross_self, p_othr) {
Some(p_othr)
} else if othr.between_vectors(cross_othr, p_self) {
Some(p_self)
} else {
None
}
};
let lhs = in_between(self.0, othr.0)?;
let rhs = in_between(self.1, othr.1)?;
if lhs == rhs {
None
} else {
Some(Self(lhs, rhs))
}
}
}

View File

@ -6,11 +6,10 @@ pub mod draw;
pub mod navcord;
pub mod navcorder;
pub mod navmesh;
pub mod ng;
mod route;
mod router;
pub mod thetastar;
pub use route::RouteStepper;
pub use router::*;
pub use planar_incr_embed;

483
src/router/ng/eval.rs Normal file
View File

@ -0,0 +1,483 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use geo::{algorithm::line_measures::metric_spaces::Euclidean, Distance};
use pie::{algo::pmg_astar::InsertionInfo, NavmeshIndex, RelaxedPath};
use std::collections::BTreeMap;
use crate::{
drawing::{
band::BandUid,
dot::FixedDotIndex,
graph::MakePrimitive as _,
head::{BareHead, GetFace as _, Head},
primitive::MakePrimitiveShape as _,
rules::AccessRules,
Collect,
},
geometry::{primitive::PrimitiveShape, shape::AccessShape as _, shape::MeasureLength as _},
graph::{GenericIndex, GetPetgraphIndex as _},
layout::{poly::PolyWeight, CompoundWeight},
math::{poly_ext_handover, RotationSense},
router::{
draw::Draw,
ng::{
pie, Alignment, AstarContext, Common, EtchedPath, EvalException, FloatingRouting,
PieNavmeshBase, PieNavmeshRef, PolygonRouting, SubContext,
},
},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Etched {
Core(FixedDotIndex),
Path(EtchedPath),
}
impl Etched {
fn resolve(
&self,
bands: &BTreeMap<EtchedPath, BandUid>,
) -> Result<Option<BandUid>, EvalException> {
Ok(match self {
Etched::Path(ep) => Some(ep.resolve_to_uid(bands)?),
_ => None,
})
}
}
impl<'a> TryFrom<&'a RelaxedPath<EtchedPath, ()>> for Etched {
type Error = ();
fn try_from(x: &'a RelaxedPath<EtchedPath, ()>) -> Result<Etched, ()> {
match x {
RelaxedPath::Weak(()) => Err(()),
RelaxedPath::Normal(ep) => Ok(Etched::Path(ep.clone())),
}
}
}
impl AstarContext {
fn evaluate_navmesh_intern<R: AccessRules + Clone>(
navmesh: PieNavmeshRef<'_>,
ctx: &Self,
common: &Common<R>,
ins_info: InsertionInfo<PieNavmeshBase>,
) -> Result<(f64, Self), EvalException> {
let (start_idx, end_idx) = (ins_info.prev_node, ins_info.cur_node);
if !common.allowed_edges.is_empty() {
let edge_idx = pie::navmesh::OrderedPair::from((start_idx, end_idx));
if !common.allowed_edges.contains(&edge_idx) {
return Err(EvalException::EdgeDisallowed(edge_idx));
}
}
let mut sub = if let Some(x) = ins_info.maybe_new_goal {
// start processing a new goal
let face = match start_idx {
NavmeshIndex::Primal(prim) => prim,
NavmeshIndex::Dual(_) => panic!("invalid goal initialization"),
};
SubContext {
label: x,
active_head: BareHead { face }.into(),
polygon: None,
floating: None,
}
} else {
ctx.sub.as_ref().expect("no goal initialized").clone()
};
let mut layout = ctx.last_layout(common);
let mut recorder = ctx.recorder.clone();
let width = *common.widths.get(&sub.label).expect("no width given");
let edge_meta = ins_info.edge_meta;
// paths on edge are ordered from edge_meta.lhs, to edge_meta.rhs
let edge_paths = navmesh.access_edge_paths(ins_info.epi);
debug_assert_eq!(
edge_paths.as_ref()[ins_info.intro],
RelaxedPath::Normal(sub.label.clone())
);
let to_pos = navmesh.node_data(&end_idx).unwrap().pos;
let to_pos = geo::point! { x: to_pos.x, y: to_pos.y };
match (start_idx, end_idx) {
(NavmeshIndex::Primal(_), _) => {
// no alignment to handle, handle like `floating`
// TODO: keep track of what is left and right to us
//sub.append_to_center_poly(&layout, to_pos, None);
//sub.check_center_poly(&layout, common.active_layer)?;
// TODO: prevent any wrapping around the start
Ok((
ctx.length + Euclidean::distance(sub.head_center(&layout), to_pos),
AstarContext {
recorder,
bands: ctx.bands.clone(),
length: ctx.length,
sub: Some(sub),
},
))
}
(_, NavmeshIndex::Primal(prim)) => {
//if let Some(mut floating) = sub.floating.take() {
// this would be overly strict
//floating.push(&layout, sub.active_head.face(), to_pos, Some(prim));
//}
let mut length = ctx.length;
if let Some(old_poly) = sub.polygon.take() {
if prim != old_poly.apex {
let destination = prim.primitive(layout.drawing()).shape().center();
let exit = old_poly.entry_point(destination, true)?;
let (new_head, length_delta) = old_poly.route_to_exit(
&mut layout,
&mut recorder,
sub.active_head,
exit,
width,
)?;
sub.active_head = new_head;
length += length_delta;
}
}
let fin = layout.finish_in_dot(&mut recorder, sub.active_head, prim, width)?;
length += sub
.active_head
.maybe_cane()
.map(|cane| cane.bend.primitive(layout.drawing()).shape().length())
.unwrap_or(0.0);
length += {
match fin.primitive(layout.drawing()).shape() {
PrimitiveShape::Dot(_) => unreachable!(),
PrimitiveShape::Seg(seg) => seg.length(),
PrimitiveShape::Bend(bend) => bend.length(),
}
};
let mut bands = ctx.bands.clone();
bands.insert(
sub.label.clone(),
layout
.drawing()
.loose_band_uid(fin.into())
.expect("a completely routed band should've Seg's as ends"),
);
Ok((
length,
AstarContext {
recorder,
bands,
length,
sub: Some(sub),
},
))
}
_ => {
let edge_paths = edge_paths.as_ref();
let alignment = match (edge_meta.lhs, edge_meta.rhs) {
(Some(_), Some(_)) => {
let mut alignment = Alignment::Left;
for _ in edge_paths
.iter()
.take(ins_info.intro)
.filter(|i| matches!(i, RelaxedPath::Weak(())))
{
alignment.incr_inplace();
}
alignment
}
(Some(_), None) => Alignment::Left,
(None, Some(_)) => Alignment::Right,
// this should only happen when one end-point is primal, handled above
(None, None) => unreachable!(),
};
let (lhs, rhs) = (
if let Some(lhs_idx) = ins_info.intro.checked_sub(1) {
(&edge_paths[lhs_idx]).try_into().ok()
} else {
edge_meta.lhs.map(Etched::Core)
},
if ins_info.intro + 1 < edge_paths.len() {
(&edge_paths[ins_info.intro + 1]).try_into().ok()
} else {
edge_meta.rhs.map(Etched::Core)
},
);
if lhs == Some(Etched::Path(sub.label.clone()))
|| rhs == Some(Etched::Path(sub.label.clone()))
{
return Err(EvalException::RouteBouncedBack);
}
let next_floating = match (edge_meta.lhs, edge_meta.rhs) {
(Some(lhs), Some(rhs)) => Some(FloatingRouting::new(
&layout,
sub.active_head.face(),
lhs.primitive(&layout.drawing()).shape().center(),
rhs.primitive(&layout.drawing()).shape().center(),
)),
_ => None,
};
if let Some(floating) = &mut sub.floating {
if let Some(next_floating) = next_floating {
*floating = floating.push(&next_floating, sub.active_head.face())?;
} else {
sub.floating = None;
}
}
let (wrap_etched, wrap_core, cw) = match alignment {
Alignment::Center => {
if sub.floating.is_none() {
sub.floating = next_floating;
}
return Ok((
ctx.length + Euclidean::distance(sub.head_center(&layout), to_pos),
AstarContext {
recorder,
bands: ctx.bands.clone(),
length: ctx.length,
sub: Some(sub),
},
));
}
Alignment::Left => (
lhs.unwrap(),
edge_meta.lhs.unwrap(),
RotationSense::Counterclockwise,
),
Alignment::Right => (
rhs.unwrap(),
edge_meta.rhs.unwrap(),
RotationSense::Clockwise,
),
};
// we left the floating context above
sub.floating = None;
let current_poly = layout
.drawing()
.compounds(GenericIndex::<()>::new(wrap_core.petgraph_index()))
.find_map(|(_, compound)| {
if let CompoundWeight::Poly(_) = layout.drawing().compound_weight(compound)
{
Some(compound)
} else {
None
}
})
.map(|compound| GenericIndex::<PolyWeight>::new(compound.petgraph_index()));
let (active_head, length_delta) = match (
sub.polygon.take(),
current_poly,
wrap_etched,
) {
(Some(existing_poly), Some(current_poly), _)
if existing_poly.idx == current_poly =>
{
// still the same polygon
let old_uid = existing_poly.inner;
let new_uid = wrap_etched.resolve(&ctx.bands)?;
return if old_uid != new_uid {
log::warn!(
"encountered changing inner band around polygon with apex={:?}",
existing_poly.apex
);
Err(EvalException::InnerPathChangedAroundPolygon {
apex: existing_poly.apex,
old_uid,
new_uid,
})
} else {
sub.polygon = Some(existing_poly);
Ok((
ctx.length,
AstarContext {
recorder,
bands: ctx.bands.clone(),
length: ctx.length,
sub: Some(sub),
},
))
};
}
(None, _, Etched::Core(dot)) if sub.is_end_point(dot) => {
// `dot` is already the goal (even if it is inside a polygon)
// justification: that we manage to wrap directly around the goal means
// that there is also a shorter way to the goal
return Err(EvalException::UnnecessaryWrapAroundEndpoint);
}
(Some(old_poly), new_poly, _)
if new_poly.is_none()
|| matches!(wrap_etched, Etched::Core(dot) if sub.is_end_point(dot)) =>
{
log::debug!(
"routing away from polygon with apex={:?}, wrap around {:?} with head {:?}",
old_poly.apex,
wrap_etched,
sub.active_head
);
let destination = sub.head_center(&layout);
let exit = old_poly.entry_point(destination, true)?;
let (new_head, mut length_delta) = old_poly.route_to_exit(
&mut layout,
&mut recorder,
sub.active_head,
exit,
width,
)?;
sub.active_head = new_head;
let next_head = super::cane_around(
&mut layout,
&mut recorder,
&mut length_delta,
sub.active_head,
wrap_core,
wrap_etched.resolve(&ctx.bands)?,
cw,
width,
)?;
(Head::Cane(next_head), length_delta)
}
// handled above
(Some(_), None, _) => unreachable!(),
(None, None, _) => {
let mut length_delta = 0.0;
let next_head = super::cane_around(
&mut layout,
&mut recorder,
&mut length_delta,
sub.active_head,
wrap_core,
wrap_etched.resolve(&ctx.bands)?,
cw,
width,
)?;
(Head::Cane(next_head), length_delta)
}
(Some(old_poly), Some(current_poly), _) => {
log::debug!(
"routing at polygon with apex={:?}, wrap around {:?} with head {:?}",
old_poly.apex,
wrap_etched,
sub.active_head
);
let mut poly = PolygonRouting::new(&layout, cw, current_poly, wrap_core);
let (exit, entry) = match poly_ext_handover(
&old_poly.convex_hull,
old_poly.cw,
&poly.convex_hull,
poly.cw,
) {
None => {
return Err(EvalException::InvalidPolyHandoverData {
source_poly_ext: old_poly.convex_hull.clone(),
source_sense: old_poly.cw,
target_poly_ext: poly.convex_hull.clone(),
target_sense: poly.cw,
})
}
Some(x) => x,
};
// TODO: also handle bends around polygons...
// if the polygon encloses the head already, we can't route.
poly.entry_point = Some(entry);
poly.inner = wrap_etched.resolve(&ctx.bands)?;
log::debug!("exit point = {:?}; wrap around polygon {:?}", exit, poly,);
let (new_head, mut length_delta) = old_poly.route_to_exit(
&mut layout,
&mut recorder,
sub.active_head,
exit,
width,
)?;
sub.active_head = new_head;
let (res, length_delta2) = poly.route_to_entry(
&mut layout,
&mut recorder,
sub.active_head,
entry,
width,
)?;
length_delta += length_delta2;
sub.polygon = Some(poly);
(res, length_delta)
}
(None, Some(current_poly), _) => {
log::debug!(
"routing into polygon with apex={:?}, idx={:?}, wrap_around {:?} with head {:?}",
wrap_core,
current_poly,
wrap_etched,
sub.active_head
);
let mut poly = PolygonRouting::new(&layout, cw, current_poly, wrap_core);
let source = sub.head_center(&layout);
// if the polygon encloses the head already, we can't route.
let dot = poly.entry_point(source, false)?;
poly.entry_point = Some(dot);
poly.inner = wrap_etched.resolve(&ctx.bands)?;
log::debug!(
"wrap around polygon {:?}: entry point = {:?}, cw? {:?}",
poly,
dot,
cw
);
let res = poly.route_to_entry(
&mut layout,
&mut recorder,
sub.active_head,
dot,
width,
)?;
sub.polygon = Some(poly);
res
}
};
sub.active_head = active_head;
let length = ctx.length + length_delta;
Ok((
length,
AstarContext {
recorder,
bands: ctx.bands.clone(),
length,
sub: Some(sub),
},
))
}
}
}
pub fn evaluate_navmesh<R: AccessRules + Clone + std::panic::RefUnwindSafe>(
navmesh: PieNavmeshRef<'_>,
ctx: &Self,
common: &Common<R>,
ins_info: InsertionInfo<PieNavmeshBase>,
) -> Result<(f64, Self), EvalException> {
std::panic::catch_unwind(move || {
Self::evaluate_navmesh_intern(navmesh, ctx, common, ins_info)
})
.map_err(|e| EvalException::Panic(e.into()))
.and_then(core::convert::identity)
}
}

59
src/router/ng/floating.rs Normal file
View File

@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use geo::Point;
use crate::{
drawing::{
dot::DotIndex,
graph::MakePrimitive as _,
primitive::{GetWeight as _, Primitive},
rules::AccessRules,
},
geometry::GetSetPos as _,
layout::Layout,
math::Tunnel,
router::ng::EvalException,
};
/// floating edges don't count, instead, only the end points around the streak
/// get connected directly (but the intermediates shouldn't cross any vertices)
#[derive(Clone, Copy, Debug)]
pub struct FloatingRouting {
// the starting point of the floating routing
// (we just use the position of the head face)
//pub origin: Point,
/// the circle segment / tunnel in which we are allowed to navigate
pub tunnel: Tunnel,
}
impl FloatingRouting {
pub fn new<R: AccessRules>(
layout: &Layout<R>,
active_head_face: DotIndex,
lhs: Point,
rhs: Point,
) -> Self {
let active_head_pos = match active_head_face.primitive(layout.drawing()) {
Primitive::FixedDot(dot) => dot.weight().0,
Primitive::LooseDot(dot) => dot.weight().0,
_ => unreachable!(),
}
.pos();
Self {
tunnel: Tunnel(lhs - active_head_pos, rhs - active_head_pos),
}
}
pub fn push(self, othr: &Self, active_head_face: DotIndex) -> Result<Self, EvalException> {
Ok(Self {
tunnel: self.tunnel.intersection(&othr.tunnel).ok_or(
EvalException::FloatingEmptyTunnel {
origin: active_head_face,
},
)?,
})
}
}

495
src/router/ng/mod.rs Normal file
View File

@ -0,0 +1,495 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use pie::{
navmesh::{self, EdgeIndex, TrianVertex},
NavmeshIndex, RelaxedPath,
};
pub use planar_incr_embed as pie;
use geo::geometry::{LineString, Point};
use rstar::AABB;
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use crate::{
board::Board,
drawing::{
band::BandUid,
bend::BendIndex,
dot::{DotIndex, FixedDotIndex},
graph::{MakePrimitive as _, PrimitiveIndex},
head::{CaneHead, GetFace as _, Head},
primitive::MakePrimitiveShape as _,
rules::AccessRules,
Collect as _,
},
geometry::{
edit::ApplyGeometryEdit as _,
primitive::PrimitiveShape,
shape::{AccessShape as _, MeasureLength as _},
GenericNode,
},
graph::GetPetgraphIndex as _,
layout::{Layout, LayoutEdit},
math::{CachedPolyExt, RotationSense},
router::draw::{Draw, DrawException},
};
mod eval;
mod floating;
pub use floating::FloatingRouting;
mod poly;
use poly::*;
mod router;
pub use router::*;
#[derive(Clone, Copy, Debug)]
pub struct PieNavmeshBase;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct EtchedPath {
pub end_points: EdgeIndex<FixedDotIndex>,
}
impl EtchedPath {
fn resolve_to_uid(
&self,
bands: &BTreeMap<EtchedPath, BandUid>,
) -> Result<BandUid, EvalException> {
bands
.get(self)
.copied()
.ok_or_else(|| EvalException::ResolvingPathFailed { path: *self })
}
}
impl pie::NavmeshBase for PieNavmeshBase {
type PrimalNodeIndex = FixedDotIndex;
type EtchedPath = EtchedPath;
type GapComment = ();
type Scalar = f64;
}
pub type PieNavmesh = navmesh::Navmesh<PieNavmeshBase>;
pub type PieNavmeshRef<'a> = navmesh::NavmeshRef<'a, PieNavmeshBase>;
pub type PieEdgeIndex =
EdgeIndex<NavmeshIndex<<PieNavmeshBase as pie::NavmeshBase>::PrimalNodeIndex>>;
/// Context for a single to-be-routed trace
#[derive(Clone, Debug)]
pub struct SubContext {
pub label: EtchedPath,
/// the last "active" head (head before the streak of `floating` entries)
pub active_head: Head,
pub polygon: Option<PolygonRouting>,
// note that floating routing might be active while `poly` is also active,
// in order to correctly calculate the exit points of the polygon.
pub floating: Option<FloatingRouting>,
}
impl SubContext {
pub fn is_end_point(&self, dot: FixedDotIndex) -> bool {
dot == self.label.end_points[false] || dot == self.label.end_points[true]
}
}
/// Data shared between many tasks
#[derive(Debug)]
pub struct Common<R> {
pub layout: Layout<R>,
pub active_layer: usize,
/// width per path to be routed
pub widths: BTreeMap<EtchedPath, f64>,
/// If non-empty, then routing is only allowed to use these edges
pub allowed_edges: BTreeSet<PieEdgeIndex>,
}
/// The context for [`PmgAstar`](pie::algo::pmg_astar::PmgAstar).
#[derive(Clone, Debug)]
pub struct AstarContext {
/// TODO: make sure we can trust the `LayoutEdit`
pub recorder: LayoutEdit,
pub bands: BTreeMap<EtchedPath, BandUid>,
/// length including `active_head`
pub length: f64,
pub sub: Option<SubContext>,
}
impl AstarContext {
pub fn last_layout<R: AccessRules + Clone>(&self, common: &Common<R>) -> Layout<R> {
let mut layout = common.layout.clone();
layout.apply(&self.recorder);
layout
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Alignment {
Left,
Center,
Right,
}
impl core::ops::Neg for Alignment {
type Output = Self;
fn neg(self) -> Self {
match self {
Alignment::Left => Alignment::Right,
Alignment::Center => Alignment::Center,
Alignment::Right => Alignment::Left,
}
}
}
impl Alignment {
/// ## Panics
/// Panics if `self == Alignment::Right`
pub fn incr_inplace(&mut self) {
*self = match *self {
Alignment::Left => Alignment::Center,
Alignment::Center => Alignment::Right,
Alignment::Right => panic!("too many alignment markers on edge"),
}
}
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum EvalException {
#[error("floating routing exhausted, tunnel empty (origin = {origin:?})")]
FloatingEmptyTunnel { origin: DotIndex },
#[error("invalid polygon tangent arguments (origin = {origin:?})")]
InvalidPolyTangentData {
poly_ext: CachedPolyExt<FixedDotIndex>,
origin: Point,
},
#[error("invalid polygon handover arguments")]
InvalidPolyHandoverData {
source_poly_ext: CachedPolyExt<FixedDotIndex>,
source_sense: RotationSense,
target_poly_ext: CachedPolyExt<FixedDotIndex>,
target_sense: RotationSense,
},
#[error(transparent)]
Draw(#[from] DrawException),
#[error("unable to resolve path to BandUid")]
ResolvingPathFailed { path: EtchedPath },
#[error("route got bounced back")]
RouteBouncedBack,
#[error("route wrapped unnecessarily around end-point")]
UnnecessaryWrapAroundEndpoint,
#[error("inner path changed around polygon")]
InnerPathChangedAroundPolygon {
apex: FixedDotIndex,
old_uid: Option<BandUid>,
new_uid: Option<BandUid>,
},
#[error("bend not found")]
BendNotFound { core: FixedDotIndex, uid: BandUid },
#[error("edge disallowed: {0:?}")]
EdgeDisallowed(PieEdgeIndex),
#[error("panicked")]
Panic(Arc<dyn std::any::Any + Send + 'static>),
}
impl EvalException {
fn ghosts_blockers_and_obstacles(
&self,
) -> (Vec<PrimitiveShape>, Vec<LineString>, Vec<PrimitiveIndex>) {
match self {
Self::FloatingEmptyTunnel { origin } => {
(Vec::new(), Vec::new(), vec![(*origin).into()])
}
Self::InvalidPolyTangentData { poly_ext, .. } => (
Vec::new(),
vec![LineString(
poly_ext.0[..].iter().map(|&(pt, _, _)| pt.0).collect(),
)],
Vec::new(),
),
Self::InvalidPolyHandoverData {
source_poly_ext,
target_poly_ext,
..
} => (
Vec::new(),
vec![
LineString(
source_poly_ext.0[..]
.iter()
.map(|&(pt, _, _)| pt.0)
.collect(),
),
LineString(
target_poly_ext.0[..]
.iter()
.map(|&(pt, _, _)| pt.0)
.collect(),
),
],
Vec::new(),
),
Self::Draw(DrawException::NoTangents(_)) => (Vec::new(), Vec::new(), Vec::new()),
Self::Draw(DrawException::CannotFinishIn(_, dwxc))
| Self::Draw(DrawException::CannotWrapAround(_, dwxc)) => {
match dwxc.maybe_ghost_and_obstacle() {
None => (Vec::new(), Vec::new(), Vec::new()),
Some((ghost, obstacle)) => (vec![*ghost], Vec::new(), vec![obstacle]),
}
}
Self::ResolvingPathFailed { .. } => (Vec::new(), Vec::new(), Vec::new()),
Self::RouteBouncedBack | Self::UnnecessaryWrapAroundEndpoint => {
(Vec::new(), Vec::new(), Vec::new())
}
Self::InnerPathChangedAroundPolygon { .. } => (Vec::new(), Vec::new(), Vec::new()),
Self::BendNotFound { .. } => (Vec::new(), Vec::new(), Vec::new()),
Self::EdgeDisallowed(_) | Self::Panic(_) => (Vec::new(), Vec::new(), Vec::new()),
}
}
}
pub fn calculate_navmesh<R: AccessRules>(
board: &Board<R>,
active_layer: usize,
) -> Result<PieNavmesh, spade::InsertionError> {
use pie::NavmeshIndex::*;
use spade::Triangulation;
let triangulation = spade::DelaunayTriangulation::<TrianVertex<FixedDotIndex, f64>>::bulk_load(
board
.layout()
.drawing()
.rtree()
.locate_in_envelope_intersecting(&AABB::<[f64; 3]>::from_corners(
[-f64::INFINITY, -f64::INFINITY, active_layer as f64],
[f64::INFINITY, f64::INFINITY, active_layer as f64],
))
.map(|&geom| geom.data)
.filter_map(|node| board.layout().apex_of_compoundless_node(node, active_layer))
.map(|(idx, pos)| TrianVertex {
idx,
pos: spade::mitigate_underflow(spade::Point2 {
x: pos.x(),
y: pos.y(),
}),
})
.collect(),
)?;
let mut navmesh = navmesh::NavmeshSer::<PieNavmeshBase>::from_triangulation(&triangulation);
let barrier2: Arc<[RelaxedPath<_, _>]> =
Arc::from(vec![RelaxedPath::Weak(()), RelaxedPath::Weak(())]);
// populate DualInner-Dual* routed traces
for value in navmesh.edges.values_mut() {
if let (Some(lhs), Some(rhs)) = (value.0.lhs, value.0.rhs) {
value.1 = barrier2.clone();
let wrap = |dot| GenericNode::Primitive(PrimitiveIndex::FixedDot(dot));
let bands = board
.layout()
.bands_between_nodes_with_alignment(active_layer, wrap(lhs), wrap(rhs))
.map(|i| match i {
RelaxedPath::Weak(()) => RelaxedPath::Weak(()),
RelaxedPath::Normal(band_uid) => {
RelaxedPath::Normal(*board.bands_by_id().get_by_right(&band_uid).unwrap())
}
})
.collect::<Vec<_>>();
if bands != *barrier2 {
log::debug!("navmesh generated with {:?} = {:?}", value, &bands);
value.1 = Arc::from(bands);
}
}
}
// TODO: insert fixed and outer routed traces/bands into the navmesh
// see also: https://codeberg.org/topola/topola/issues/166
// due to not handling outer routed traces/bends,
// the above code might produce an inconsistent navmesh
// populate Primal-Dual* routed traces
let dual_ends: BTreeMap<_, _> = navmesh
.nodes
.iter()
.filter_map(|(node, data)| {
if let Dual(node) = node {
Some((node, data))
} else {
None
}
})
.map(|(node, data)| {
let mut binoccur = BTreeMap::new();
for &neigh in &data.neighs {
for &path in navmesh
.edge_data(Dual(*node), neigh)
.expect("unable to resolve neighbor")
.map::<_, RelaxedPath<FixedDotIndex, ()>, _>(|i| i[..].iter())
{
if let RelaxedPath::Normal(ep) = path {
// every path should occur twice, or some other multiple of 2;
// find those which don't.
if binoccur.insert(ep, neigh).is_some() {
binoccur.remove(&ep);
}
}
}
}
(*node, binoccur)
})
.filter(|(_, binoccur)| !binoccur.is_empty())
.collect();
let old_navmesh_edges_keys = navmesh.edges.keys().copied().collect::<Vec<_>>();
// NOTE: this doesn't correctly handle the case that a path which ends in one dual node
// occurs multiple times (we don't know which parts belong together)
for key in old_navmesh_edges_keys {
let (prim, dual) =
if let (Primal(prim), Dual(dual)) | (Dual(dual), Primal(prim)) = key.into() {
(prim, dual)
} else {
continue;
};
if let Some(dual_ends) = dual_ends.get(&dual) {
for (&ep, &other) in dual_ends {
// check if `ep` ends in `prim`.
if !(ep.end_points[false] == prim || ep.end_points[true] == prim) {
continue;
}
// find ordering.
let pos = navmesh.edges[&(Dual(dual), other).into()]
.1
.iter()
.position(|&i| i == RelaxedPath::Normal(ep))
.unwrap();
match navmesh.planarr_find_other_end(&Dual(dual), &other, pos, true, &Primal(prim))
{
None => {
log::warn!(
"topo-navmesh end path in planarr {:?}, {:?} -> {:?}: unable to find other end",
dual,
other,
prim,
);
}
Some((_, other_end)) => {
log::trace!(
"topo-navmesh end path in planarr {:?}, {:?} -> {:?}: other end @ {}",
dual,
other,
prim,
other_end.insert_pos,
);
// the edge is valid because it otherwise wouldn't have been the result from
// `planar_find_other_end` above
navmesh
.edge_data_mut(Dual(dual), Primal(prim))
.unwrap()
.with_borrow_mut(|mut x| {
x.insert(other_end.insert_pos, RelaxedPath::Normal(ep));
});
}
}
}
}
}
Ok(navmesh.into())
}
impl SubContext {
fn head_center<R: AccessRules>(&self, layout: &Layout<R>) -> Point {
self.active_head
.face()
.primitive(layout.drawing())
.shape()
.center()
}
}
fn cane_around<R: AccessRules>(
layout: &mut Layout<R>,
recorder: &mut LayoutEdit,
route_length: &mut f64,
old_head: Head,
core: FixedDotIndex,
inner: Option<BandUid>,
sense: RotationSense,
width: f64,
) -> Result<CaneHead, EvalException> {
log::debug!(
"cane around: head {:?}, core {:?}, inner {:?}, sense {:?}",
old_head,
core,
inner,
sense
);
// TODO: fix `-sense` vs `sense`.
let ret = match inner {
None => layout.cane_around_dot(recorder, old_head, core, -sense, width),
Some(inner) => {
// now, inner is expected to be a bend.
// TODO: handle the case that the same path wraps multiple times around the same core
let inner_bend = layout
.drawing()
.geometry()
.all_rails(core.petgraph_index())
.filter_map(|bi| {
if let BendIndex::Loose(lbi) = bi {
if layout.drawing().loose_band_uid(lbi.into()).ok() == Some(inner) {
Some(lbi)
} else {
None
}
} else {
None
}
})
.next();
if let Some(inner_bend) = inner_bend {
layout.cane_around_bend(recorder, old_head, inner_bend.into(), -sense, width)
} else {
return Err(EvalException::BendNotFound {
core: core,
uid: inner,
});
}
}
}?;
// record the length of the current seg, and the old bend, if any
*route_length += ret.cane.seg.primitive(layout.drawing()).shape().length()
+ old_head
.maybe_cane()
.map(|cane| cane.bend.primitive(layout.drawing()).shape().length())
.unwrap_or(0.0);
Ok(ret)
}

207
src/router/ng/poly.rs Normal file
View File

@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use geo::Point;
use specctra_core::rules::AccessRules;
use crate::{
drawing::{
band::BandUid,
dot::FixedDotIndex,
graph::{MakePrimitive as _, PrimitiveIndex},
head::{CaneHead, Head},
primitive::MakePrimitiveShape as _,
},
geometry::{compound::ManageCompounds, shape::AccessShape as _, GetSetPos as _},
graph::{GenericIndex, GetPetgraphIndex as _},
layout::{poly::PolyWeight, CompoundEntryLabel, Layout, LayoutEdit},
math::{is_poly_convex_hull_cw, CachedPolyExt, RotationSense},
router::ng::{
pie::{mayrev, utils::rotate_iter},
EvalException,
},
};
#[derive(Clone, Debug)]
// the entry point is where `active_head` ends
// TODO: two-phase initialization via separate types
pub struct PolygonRouting {
pub idx: GenericIndex<PolyWeight>,
pub apex: FixedDotIndex,
pub convex_hull: CachedPolyExt<FixedDotIndex>,
pub entry_point: Option<FixedDotIndex>,
pub inner: Option<BandUid>,
pub cw: RotationSense,
}
impl PolygonRouting {
/// calculates the convex hull of a poly exterior
pub fn new<R>(
layout: &Layout<R>,
cw: RotationSense,
polyidx: GenericIndex<PolyWeight>,
apex: FixedDotIndex,
) -> Self {
let convex_hull = layout
.drawing()
.geometry()
.compound_members(GenericIndex::new(polyidx.petgraph_index()))
.filter_map(|(entry_label, primitive_node)| {
let PrimitiveIndex::FixedDot(poly_dot) = primitive_node else {
return None;
};
if apex == poly_dot {
None
} else {
Some((
layout
.drawing()
.geometry()
.dot_weight(poly_dot.into())
.pos(),
entry_label,
poly_dot,
))
}
})
.filter(|(_, entry_label, _)| *entry_label == CompoundEntryLabel::Normal)
.map(|(pt, _, idx)| (pt, idx))
.collect::<Vec<_>>();
// `poly_ext` is convex, so any pivot point is okay
let ext_is_cw = is_poly_convex_hull_cw(&convex_hull[..], 1);
Self {
idx: polyidx,
apex,
convex_hull: CachedPolyExt::new(&convex_hull[..], ext_is_cw),
entry_point: None,
inner: None,
cw,
}
}
pub fn center<R: AccessRules>(&self, layout: &Layout<R>) -> Point {
self.apex.primitive(layout.drawing()).shape().center()
}
/// calculate the entry or exit point for the polygon (set `invert_cw` to `true` for exit point)
pub fn entry_point(
&self,
destination: Point,
invert_cw: bool,
) -> Result<FixedDotIndex, EvalException> {
// note that the left-most point has the greatest angle (measured counter-clockwise)
let Some((lhs, rhs)) = self.convex_hull.tangent_points(destination) else {
return Err(EvalException::InvalidPolyTangentData {
poly_ext: self.convex_hull.clone(),
origin: destination,
});
};
let cw = match self.cw {
RotationSense::Counterclockwise => false,
RotationSense::Clockwise => true,
};
Ok(if invert_cw ^ cw { lhs } else { rhs })
}
fn route_next<R: AccessRules>(
&self,
layout: &mut Layout<R>,
recorder: &mut LayoutEdit,
route_length: &mut f64,
old_head: Head,
ext_core: FixedDotIndex,
width: f64,
) -> Result<CaneHead, EvalException> {
super::cane_around(
layout,
recorder,
route_length,
old_head,
ext_core,
self.inner,
self.cw,
width,
)
}
pub fn route_to_entry<R: AccessRules>(
&self,
layout: &mut Layout<R>,
recorder: &mut LayoutEdit,
old_head: Head,
entry_point: FixedDotIndex,
width: f64,
) -> Result<(Head, f64), EvalException> {
let mut route_length = 0.0;
let active_head = Head::Cane(self.route_next(
layout,
recorder,
&mut route_length,
old_head,
entry_point,
width,
)?);
Ok((active_head, route_length))
}
pub fn route_to_exit<R: AccessRules>(
&self,
layout: &mut Layout<R>,
recorder: &mut LayoutEdit,
mut active_head: Head,
exit: FixedDotIndex,
width: f64,
) -> Result<(Head, f64), EvalException> {
let old_entry = self.entry_point.unwrap();
if old_entry == exit {
// nothing to do
return Ok((active_head, 0.0));
}
log::debug!(
"route_to_exit on {:?} from {:?} to {:?} sense {:?}",
self.apex,
old_entry,
exit,
self.cw,
);
let mut mr = mayrev::MaybeReversed::new(&self.convex_hull.0[..]);
// the convex hull is oriented counter-clockwise
// FIXME(fogti): I have no clue where the orientation gets wrong...
mr.reversed = !match self.cw {
RotationSense::Counterclockwise => false,
RotationSense::Clockwise => true,
};
let mut route_length = 0.0;
for pdot in rotate_iter(Iterator::map(mr.iter(), |(_, pdot, _)| *pdot), |&pdot| {
pdot == old_entry
})
.1
.skip(1)
{
// route along the origin polygon
active_head = Head::Cane(self.route_next(
layout,
recorder,
&mut route_length,
active_head,
pdot,
width,
)?);
if pdot == exit {
break;
}
}
Ok((active_head, route_length))
}
}

573
src/router/ng/router.rs Normal file
View File

@ -0,0 +1,573 @@
// SPDX-FileCopyrightText: 2025 Topola contributors
//
// SPDX-License-Identifier: MIT
use core::ops::ControlFlow;
use geo::geometry::LineString;
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use crate::{
autorouter::invoker::{
GetActivePolygons, GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper,
GetMaybeTopoNavmesh, GetNavmeshDebugTexts, GetObstacles, GetPolygonalBlockers,
},
drawing::{band::BandUid, dot::FixedDotIndex, graph::PrimitiveIndex, rules::AccessRules},
geometry::primitive::PrimitiveShape,
graph::GenericIndex,
layout::{poly::PolyWeight, Layout, LayoutEdit},
stepper::Abort,
};
use super::{
pie::{
self,
algo::{pmg_astar::InsertionInfo, Goal as PieGoal},
navmesh::{EdgeIndex, EdgePaths},
Edge, NavmeshIndex, Node, RelaxedPath,
},
AstarContext, Common, EtchedPath, PieEdgeIndex, PieNavmesh, PieNavmeshBase,
};
pub type PmgAstar = pie::algo::pmg_astar::PmgAstar<PieNavmeshBase, AstarContext>;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Goal {
pub source: FixedDotIndex,
pub target: FixedDotIndex,
pub width: f64,
}
impl Goal {
#[inline]
pub fn label(&self) -> EtchedPath {
EtchedPath {
end_points: EdgeIndex::from((self.source, self.target)),
}
}
pub fn to_pie(&self) -> PieGoal<FixedDotIndex, EtchedPath> {
assert_ne!(self.source, self.target);
let mut target = BTreeSet::new();
target.insert(self.target);
PieGoal {
source: self.source,
target,
label: self.label(),
}
}
pub fn register<R>(&self, common: &mut Common<R>) {
common.widths.insert(self.label(), self.width);
}
}
/// Manages the autorouting process across multiple ratlines.
pub struct AutorouteExecutionStepper<R> {
pmg_astar: PmgAstar,
common: Common<R>,
original_edge_paths: Box<[EdgePaths<EtchedPath, ()>]>,
pub last_layout: Layout<R>,
pub last_recorder: LayoutEdit,
pub last_edge_paths: Box<[EdgePaths<EtchedPath, ()>]>,
pub last_bands: BTreeMap<EtchedPath, BandUid>,
pub active_polygons: Vec<GenericIndex<PolyWeight>>,
pub ghosts: Vec<PrimitiveShape>,
pub polygonal_blockers: Vec<LineString>,
pub obstacles: Vec<PrimitiveIndex>,
}
impl<R: Clone> AutorouteExecutionStepper<R> {
fn finish(&mut self) {
self.active_polygons.clear();
self.ghosts.clear();
self.obstacles.clear();
self.polygonal_blockers.clear();
}
}
impl<M: Clone + std::panic::RefUnwindSafe> Abort<()> for AutorouteExecutionStepper<M> {
fn abort(&mut self, _: &mut ()) {
self.last_layout = self.common.layout.clone();
self.last_recorder = LayoutEdit::new();
self.last_edge_paths = self.original_edge_paths.clone();
self.last_bands = BTreeMap::new();
self.finish();
}
}
impl<R: AccessRules + Clone + std::panic::RefUnwindSafe> AutorouteExecutionStepper<R> {
/// `navmesh` can be initialized using [`calculate_navmesh`](super::calculate_navmesh)
pub fn new(
layout: &Layout<R>,
navmesh: &PieNavmesh,
bands: BTreeMap<EtchedPath, BandUid>,
active_layer: usize,
allowed_edges: BTreeSet<PieEdgeIndex>,
goals: impl Iterator<Item = Goal>,
) -> Self {
let mut common = Common {
layout: layout.clone(),
active_layer,
widths: BTreeMap::new(),
allowed_edges,
};
let mut astar_goals = Vec::new();
for i in goals {
i.register(&mut common);
astar_goals.push(i.to_pie());
}
let context = AstarContext {
recorder: LayoutEdit::new(),
bands,
length: 0.0,
sub: None,
};
let (mut ghosts, mut polygonal_blockers, mut obstacles) =
(Vec::new(), Vec::new(), Vec::new());
let pmg_astar = PmgAstar::new(
navmesh,
astar_goals,
&context,
|navmesh, context, ins_info| match AstarContext::evaluate_navmesh(
navmesh, context, &common, ins_info,
) {
Ok(x) => Some(x),
Err(e) => {
let (mut new_ghosts, mut new_blockers, mut new_obstacles) =
e.ghosts_blockers_and_obstacles();
ghosts.append(&mut new_ghosts);
polygonal_blockers.append(&mut new_blockers);
obstacles.append(&mut new_obstacles);
None
}
},
);
Self {
pmg_astar,
common,
original_edge_paths: navmesh.edge_paths.clone(),
last_layout: layout.clone(),
last_recorder: LayoutEdit::new(),
last_edge_paths: navmesh.edge_paths.clone(),
last_bands: context.bands.clone(),
active_polygons: Vec::new(),
ghosts,
polygonal_blockers,
obstacles,
}
}
pub fn step(&mut self) -> ControlFlow<bool, ()> {
self.active_polygons.clear();
self.ghosts.clear();
self.obstacles.clear();
self.polygonal_blockers.clear();
match self.pmg_astar.step(
|navmesh, context, ins_info| match AstarContext::evaluate_navmesh(
navmesh,
context,
&self.common,
ins_info,
) {
Ok(x) => {
if let Some(poly) = x.1.sub.as_ref().and_then(|i| i.polygon.as_ref()) {
if !self.active_polygons.contains(&poly.idx) {
self.active_polygons.push(poly.idx);
}
}
Some(x)
}
Err(e) => {
log::debug!("eval-navmesh error: {:?}", e);
let (mut new_ghosts, mut new_blockers, mut new_obstacles) =
e.ghosts_blockers_and_obstacles();
if let Some(poly) = context.sub.as_ref().and_then(|i| i.polygon.as_ref()) {
if !self.active_polygons.contains(&poly.idx) {
self.active_polygons.push(poly.idx);
}
}
self.ghosts.append(&mut new_ghosts);
self.polygonal_blockers.append(&mut new_blockers);
self.obstacles.append(&mut new_obstacles);
None
}
},
) {
// no valid result found
ControlFlow::Break(None) => {
self.last_layout = self.common.layout.clone();
self.last_recorder = LayoutEdit::new();
self.last_edge_paths = self.original_edge_paths.clone();
self.last_bands = BTreeMap::new();
self.finish();
ControlFlow::Break(false)
}
// some valid result found
ControlFlow::Break(Some((_costs, edge_paths, ctx))) => {
self.last_layout = ctx.last_layout(&self.common);
self.last_recorder = ctx.recorder;
self.last_edge_paths = edge_paths;
self.last_bands = ctx.bands;
self.finish();
ControlFlow::Break(true)
}
// intermediate data
ControlFlow::Continue(intermed) => {
self.last_layout = intermed.context.last_layout(&self.common);
self.last_edge_paths = intermed.edge_paths;
ControlFlow::Continue(())
}
}
}
}
impl<M> GetActivePolygons for AutorouteExecutionStepper<M> {
fn active_polygons(&self) -> &[GenericIndex<PolyWeight>] {
&self.active_polygons[..]
}
}
impl<M> GetGhosts for AutorouteExecutionStepper<M> {
fn ghosts(&self) -> &[PrimitiveShape] {
&self.ghosts[..]
}
}
impl<M> GetMaybeThetastarStepper for AutorouteExecutionStepper<M> {}
impl<M> GetMaybeNavcord for AutorouteExecutionStepper<M> {}
impl<M> GetNavmeshDebugTexts for AutorouteExecutionStepper<M> {}
impl<M> GetMaybeTopoNavmesh for AutorouteExecutionStepper<M> {
fn maybe_topo_navmesh(&self) -> Option<pie::navmesh::NavmeshRef<'_, super::PieNavmeshBase>> {
Some(pie::navmesh::NavmeshRef {
nodes: &self.pmg_astar.nodes,
edges: &self.pmg_astar.edges,
edge_paths: &self.last_edge_paths,
})
}
}
impl<M> GetObstacles for AutorouteExecutionStepper<M> {
fn obstacles(&self) -> &[PrimitiveIndex] {
&self.obstacles[..]
}
}
impl<M> GetPolygonalBlockers for AutorouteExecutionStepper<M> {
fn polygonal_blockers(&self) -> &[LineString] {
&self.polygonal_blockers[..]
}
}
#[derive(Clone, Debug)]
struct ManualRouteStep {
goal: Goal,
edge_paths: Box<[EdgePaths<EtchedPath, ()>]>,
prev_node: Option<NavmeshIndex<FixedDotIndex>>,
cur_node: NavmeshIndex<FixedDotIndex>,
}
/// Manages the manual "etching"/"implementation" of lowering paths from a topological navmesh
/// onto a layout.
pub struct ManualrouteExecutionStepper<R> {
/// remaining steps are in reverse order
remaining_steps: Vec<ManualRouteStep>,
context: AstarContext,
aborted: bool,
// constant stuff
nodes: Arc<BTreeMap<NavmeshIndex<FixedDotIndex>, Node<FixedDotIndex, f64>>>,
edges: Arc<BTreeMap<EdgeIndex<NavmeshIndex<FixedDotIndex>>, (Edge<FixedDotIndex>, usize)>>,
common: Common<R>,
original_edge_paths: Box<[EdgePaths<EtchedPath, ()>]>,
// results
pub last_layout: Layout<R>,
pub last_recorder: LayoutEdit,
// visualization / debug
pub active_polygons: Vec<GenericIndex<PolyWeight>>,
pub ghosts: Vec<PrimitiveShape>,
pub polygonal_blockers: Vec<LineString>,
pub obstacles: Vec<PrimitiveIndex>,
}
impl<M> ManualrouteExecutionStepper<M> {
pub fn last_edge_paths(&self) -> &Box<[EdgePaths<EtchedPath, ()>]> {
match self.remaining_steps.last() {
_ if self.aborted => &self.original_edge_paths,
Some(goal) => &goal.edge_paths,
None => &self.original_edge_paths,
}
}
pub fn last_bands(&self) -> &BTreeMap<EtchedPath, BandUid> {
&self.context.bands
}
}
impl<M: Clone> ManualrouteExecutionStepper<M> {
fn finish(&mut self) {
self.active_polygons.clear();
self.ghosts.clear();
self.obstacles.clear();
self.polygonal_blockers.clear();
}
}
impl<M: Clone + std::panic::RefUnwindSafe> Abort<()> for ManualrouteExecutionStepper<M> {
fn abort(&mut self, _: &mut ()) {
self.last_layout = self.common.layout.clone();
self.last_recorder = LayoutEdit::new();
self.context.bands = BTreeMap::new();
self.finish();
self.aborted = true;
}
}
impl<R: AccessRules + Clone + std::panic::RefUnwindSafe> ManualrouteExecutionStepper<R> {
/// `navmesh` can be initialized using [`calculate_navmesh`](super::calculate_navmesh).
/// This function assumes that `dest_navmesh` is already populated with all the `goals`, but `layout` isn't.
///
/// ## Panics
/// This function panics if the goals mention paths which don't exist in the navmesh,
/// or if the original navmesh is inconsistent.
pub fn new(
layout: &Layout<R>,
dest_navmesh: &PieNavmesh,
bands: BTreeMap<EtchedPath, BandUid>,
active_layer: usize,
goals: impl core::iter::DoubleEndedIterator<Item = Goal>,
) -> Self {
let mut common = Common {
layout: layout.clone(),
active_layer,
widths: BTreeMap::new(),
allowed_edges: BTreeSet::new(),
};
let nodes = dest_navmesh.nodes.clone();
let edges = dest_navmesh.edges.clone();
let mut mres_steps = Vec::new();
let mut tmp_navmesh_edge_paths = dest_navmesh.edge_paths.clone();
for i in goals.rev() {
i.register(&mut common);
mres_steps.push(ManualRouteStep {
goal: i,
edge_paths: tmp_navmesh_edge_paths.clone(),
prev_node: None,
cur_node: NavmeshIndex::Primal(i.source),
});
pie::navmesh::remove_path(&mut tmp_navmesh_edge_paths, &RelaxedPath::Normal(i.label()));
}
let context = AstarContext {
recorder: LayoutEdit::new(),
bands,
length: 0.0,
sub: None,
};
Self {
remaining_steps: mres_steps,
context,
aborted: false,
nodes,
edges,
common,
original_edge_paths: tmp_navmesh_edge_paths,
last_layout: layout.clone(),
last_recorder: LayoutEdit::new(),
active_polygons: Vec::new(),
ghosts: Vec::new(),
polygonal_blockers: Vec::new(),
obstacles: Vec::new(),
}
}
/// This function might panic if some goal's route loops back to where it came from
pub fn step(&mut self) -> ControlFlow<bool, ()> {
self.active_polygons.clear();
self.ghosts.clear();
self.obstacles.clear();
self.polygonal_blockers.clear();
let goal = if let Some(goal) = self.remaining_steps.last_mut() {
goal
} else {
// some valid result found
self.last_layout = self.context.last_layout(&self.common);
self.last_recorder = self.context.recorder.clone();
self.finish();
return ControlFlow::Break(true);
};
// try to advance current goal
let source = goal.cur_node.clone();
let label = goal.goal.label();
let navmesh = pie::navmesh::NavmeshRef::<'_, super::PieNavmeshBase> {
nodes: &*self.nodes,
edges: &*self.edges,
edge_paths: &goal.edge_paths,
};
let ins_info = match goal.prev_node {
None => {
// bootstrap this goal (see also pie's `start_pmga`)
navmesh
.node_data(&source)
.unwrap()
.neighs
.iter()
.find_map(|&neigh| {
// `edge_data`: can't panic because the original navmesh is valid (assumed to be)
// and nodes, edges stay the same.
let (edge_meta, epi) = navmesh.resolve_edge_data(source, neigh).unwrap();
navmesh
.access_edge_paths(epi)
.iter()
.enumerate()
.find(|&(_, j)| j == &RelaxedPath::Normal(label))
.map(|(intro, _)| InsertionInfo {
prev_node: source,
cur_node: neigh,
edge_meta: edge_meta.to_owned(),
epi,
intro,
maybe_new_goal: Some(label),
})
})
.unwrap()
}
Some(_) if source == NavmeshIndex::Primal(goal.goal.target) => {
// this goal is finished -> pursue next goal
let edge_paths = goal.edge_paths.clone();
self.remaining_steps.pop();
if self.remaining_steps.is_empty() {
self.original_edge_paths = edge_paths;
}
return ControlFlow::Continue(());
}
Some(prev_node) => {
// find next edge
navmesh
.node_data(&source)
.unwrap()
.neighs
.iter()
// we never want to go back to where we came from
.filter(|&neigh| *neigh != prev_node)
.find_map(|&neigh| {
// `edge_data`: can't panic because the original navmesh is valid (assumed to be)
// and nodes, edges stay the same.
let (edge_meta, epi) = navmesh.resolve_edge_data(source, neigh).unwrap();
navmesh
.access_edge_paths(epi)
.iter()
.enumerate()
.find(|&(_, j)| j == &RelaxedPath::Normal(label))
.map(|(intro, _)| InsertionInfo {
prev_node: source,
cur_node: neigh,
edge_meta: edge_meta.to_owned(),
epi,
intro,
maybe_new_goal: None,
})
})
.unwrap()
}
};
goal.prev_node = Some(ins_info.prev_node);
goal.cur_node = ins_info.cur_node;
match AstarContext::evaluate_navmesh(navmesh, &self.context, &self.common, ins_info) {
Ok((_costs, context)) => {
if let Some(poly) = context.sub.as_ref().and_then(|i| i.polygon.as_ref()) {
if !self.active_polygons.contains(&poly.idx) {
self.active_polygons.push(poly.idx);
}
}
self.context = context;
self.last_layout = self.context.last_layout(&self.common);
ControlFlow::Continue(())
}
Err(e) => {
log::debug!("eval-navmesh error: {:?}", e);
let (mut new_ghosts, mut new_blockers, mut new_obstacles) =
e.ghosts_blockers_and_obstacles();
if let Some(poly) = self.context.sub.as_ref().and_then(|i| i.polygon.as_ref()) {
if !self.active_polygons.contains(&poly.idx) {
self.active_polygons.push(poly.idx);
}
}
self.ghosts.append(&mut new_ghosts);
self.polygonal_blockers.append(&mut new_blockers);
self.obstacles.append(&mut new_obstacles);
// no valid result found
self.last_layout = self.common.layout.clone();
self.last_recorder = LayoutEdit::new();
self.context.bands = BTreeMap::new();
self.finish();
ControlFlow::Break(false)
}
}
}
}
impl<M> GetActivePolygons for ManualrouteExecutionStepper<M> {
fn active_polygons(&self) -> &[GenericIndex<PolyWeight>] {
&self.active_polygons[..]
}
}
impl<M> GetGhosts for ManualrouteExecutionStepper<M> {
fn ghosts(&self) -> &[PrimitiveShape] {
&self.ghosts[..]
}
}
impl<M> GetMaybeThetastarStepper for ManualrouteExecutionStepper<M> {}
impl<M> GetMaybeNavcord for ManualrouteExecutionStepper<M> {}
impl<M> GetNavmeshDebugTexts for ManualrouteExecutionStepper<M> {}
impl<M> GetMaybeTopoNavmesh for ManualrouteExecutionStepper<M> {
fn maybe_topo_navmesh(&self) -> Option<pie::navmesh::NavmeshRef<'_, super::PieNavmeshBase>> {
Some(pie::navmesh::NavmeshRef {
nodes: &self.nodes,
edges: &self.edges,
edge_paths: &self.last_edge_paths(),
})
}
}
impl<M> GetObstacles for ManualrouteExecutionStepper<M> {
fn obstacles(&self) -> &[PrimitiveIndex] {
&self.obstacles[..]
}
}
impl<M> GetPolygonalBlockers for ManualrouteExecutionStepper<M> {
fn polygonal_blockers(&self) -> &[LineString] {
&self.polygonal_blockers[..]
}
}