mirror of https://codeberg.org/topola/topola.git
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:
parent
c8848ef269
commit
a561b278fc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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[..]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue