diff --git a/committed.toml b/committed.toml index 7212cae..88e4710 100644 --- a/committed.toml +++ b/committed.toml @@ -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 diff --git a/crates/planar-incr-embed/src/navmesh/mod.rs b/crates/planar-incr-embed/src/navmesh/mod.rs index 4d00c7d..0b68664 100644 --- a/crates/planar-incr-embed/src/navmesh/mod.rs +++ b/crates/planar-incr-embed/src/navmesh/mod.rs @@ -205,11 +205,11 @@ impl Navmesh { } } -pub(crate) fn resolve_edge_data( - edges: &BTreeMap>, (Edge, usize)>, +pub(crate) fn resolve_edge_data( + edges: &BTreeMap>, (Edge, T)>, from_node: NavmeshIndex, to_node: NavmeshIndex, -) -> Option<(Edge<&PNI>, MaybeReversed)> { +) -> Option<(Edge<&PNI>, MaybeReversed<&T, EP>)> { let reversed = from_node > to_node; let edge_idx: EdgeIndex> = (from_node, to_node).into(); let edge = edges.get(&edge_idx)?; @@ -217,11 +217,103 @@ pub(crate) fn resolve_edge_data( 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( + edges: &mut BTreeMap>, (Edge, T)>, + from_node: NavmeshIndex, + to_node: NavmeshIndex, +) -> Option<(Edge<&PNI>, MaybeReversed<&mut T, EP>)> { + let reversed = from_node > to_node; + let edge_idx: EdgeIndex> = (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 NavmeshSer { + pub fn edge_data( + &self, + from_node: NavmeshIndex, + to_node: NavmeshIndex, + ) -> Option< + MaybeReversed< + &Arc<[RelaxedPath]>, + RelaxedPath, + >, + > { + resolve_edge_data(&self.edges, from_node, to_node).map(|(_, item)| item) + } + + pub fn edge_data_mut( + &mut self, + from_node: NavmeshIndex, + to_node: NavmeshIndex, + ) -> Option< + MaybeReversed< + &mut Arc<[RelaxedPath]>, + RelaxedPath, + >, + > { + 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, + start: &NavmeshIndex, + pos: usize, + already_inserted_at_start: bool, + stop: &NavmeshIndex, + ) -> 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, + start: &'a NavmeshIndex, + pos: usize, + already_inserted_at_start: bool, + ) -> Option<( + usize, + impl Iterator, 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(edge_paths: &mut [EdgePaths], label: &RelaxedPath) where @@ -273,7 +365,8 @@ impl<'a, B: NavmeshBase + 'a> NavmeshRefMut<'a, B> { Edge<&B::PrimalNodeIndex>, MaybeReversed>, )> { - 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>, )> { - 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)] diff --git a/crates/specctra-core/src/mesadata.rs b/crates/specctra-core/src/mesadata.rs index a7b3087..305d596 100644 --- a/crates/specctra-core/src/mesadata.rs +++ b/crates/specctra-core/src/mesadata.rs @@ -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); diff --git a/crates/topola-egui/src/actions.rs b/crates/topola-egui/src/actions.rs index f0069f6..208453b 100644 --- a/crates/topola-egui/src/actions.rs +++ b/crates/topola-egui/src/actions.rs @@ -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(); diff --git a/crates/topola-egui/src/menu_bar.rs b/crates/topola-egui/src/menu_bar.rs index a00e623..260e027 100644 --- a/crates/topola-egui/src/menu_bar.rs +++ b/crates/topola-egui/src/menu_bar.rs @@ -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 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 diff --git a/crates/topola-egui/src/overlay.rs b/crates/topola-egui/src/overlay.rs index f3af86d..38ce78e 100644 --- a/crates/topola-egui/src/overlay.rs +++ b/crates/topola-egui/src/overlay.rs @@ -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; - type GapComment = (); - type Scalar = f64; -} - -pub type PieNavmesh = planar_incr_embed::navmesh::Navmesh; - #[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, - 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::>::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::::from_triangulation( - &triangulation, - ) - .into(), - ); + if let Ok(pien) = ng_calculate_navmesh(board, active_layer) { + self.planar_incr_navmesh = Some(pien); } } diff --git a/crates/topola-egui/src/status_bar.rs b/crates/topola-egui/src/status_bar.rs index e9eac3f..9ecf9ac 100644 --- a/crates/topola-egui/src/status_bar.rs +++ b/crates/topola-egui/src/status_bar.rs @@ -15,12 +15,12 @@ impl StatusBar { Self {} } - pub fn update( + pub fn update( &mut self, ctx: &egui::Context, _tr: &Translator, viewport: &Viewport, - maybe_activity: Option<&ActivityStepperWithStatus>, + maybe_activity: Option<&ActivityStepperWithStatus>, ) { egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { let latest_pos = viewport.transform.inverse() diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index 77181dd..da4ed75 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -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::>(); + 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()) { diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index ad30c41..4ed0cf6 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -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 diff --git a/src/autorouter/autoroute.rs b/src/autorouter/autoroute.rs index c093ed7..96c40ad 100644 --- a/src/autorouter/autoroute.rs +++ b/src/autorouter/autoroute.rs @@ -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()) diff --git a/src/autorouter/autorouter.rs b/src/autorouter/autorouter.rs index 16503f4..784a8b9 100644 --- a/src/autorouter/autorouter.rs +++ b/src/autorouter/autorouter.rs @@ -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 Autorouter { Ok(()) } + pub fn topo_autoroute( + &mut self, + selection: &PinSelection, + allowed_edges: BTreeSet, + active_layer: usize, + width: f64, + init_navmesh: Option, + ) -> Result, 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>, + allowed_edges: BTreeSet, + active_layer: usize, + width: f64, + init_navmesh: Option, + ) -> Result, 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, diff --git a/src/autorouter/compare_detours.rs b/src/autorouter/compare_detours.rs index fa01894..bb1609c 100644 --- a/src/autorouter/compare_detours.rs +++ b/src/autorouter/compare_detours.rs @@ -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() diff --git a/src/autorouter/execution.rs b/src/autorouter/execution.rs index 7cb41c5..f997a4d 100644 --- a/src/autorouter/execution.rs +++ b/src/autorouter/execution.rs @@ -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, + 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 { Autoroute(AutorouteExecutionStepper), + TopoAutoroute(ng::AutorouteExecutionStepper), PlaceVia(PlaceViaExecutionStepper), RemoveBands(RemoveBandsExecutionStepper), CompareDetours(CompareDetoursExecutionStepper), MeasureLength(MeasureLengthExecutionStepper), } -impl ExecutionStepper { - fn step_catch_err( +impl ExecutionStepper { + fn step_catch_err( &mut self, autorouter: &mut Autorouter, ) -> Result, 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 Step, String> for ExecutionStepper { +impl Step, String> for ExecutionStepper { type Error = InvokerError; fn step(&mut self, invoker: &mut Invoker) -> Result, InvokerError> { @@ -111,9 +145,36 @@ impl Step, String> for ExecutionStepper { } } -impl Abort> for ExecutionStepper { - fn abort(&mut self, context: &mut Invoker) { - // TODO: fix this - self.finish(context); +impl Abort> for ExecutionStepper { + fn abort(&mut self, invoker: &mut Invoker) { + 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 GetActivePolygons for ExecutionStepper { + fn active_polygons(&self) -> &[GenericIndex] { + match self { + ExecutionStepper::TopoAutoroute(autoroute) => autoroute.active_polygons(), + _ => &[], + } + } +} + +impl GetMaybeTopoNavmesh for ExecutionStepper { + fn maybe_topo_navmesh(&self) -> Option> { + match self { + ExecutionStepper::TopoAutoroute(autoroute) => autoroute.maybe_topo_navmesh(), + _ => None, + } } } diff --git a/src/autorouter/invoker.rs b/src/autorouter/invoker.rs index 0a3119d..7d0bbbb 100644 --- a/src/autorouter/invoker.rs +++ b/src/autorouter/invoker.rs @@ -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] { + &[] + } +} + +/// Getter trait to obtain Topological/Planar Navigation Mesh +#[enum_dispatch] +pub trait GetMaybeTopoNavmesh { + fn maybe_topo_navmesh(&self) -> Option> { + 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 { pub(super) ongoing_command: Option, } -impl Invoker { +impl Invoker { /// Creates a new instance of Invoker with the given autorouter instance pub fn new(autorouter: Autorouter) -> Self { Self::new_with_history(autorouter, History::new()) @@ -144,14 +172,17 @@ impl Invoker { /// 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 { + pub fn execute_stepper( + &mut self, + command: Command, + ) -> Result, 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 { + fn dispatch_command(&mut self, command: &Command) -> Result, InvokerError> { Ok(match command { Command::Autoroute(selection, options) => { let mut ratlines = self.autorouter.selected_ratlines(selection); @@ -172,6 +203,31 @@ impl Invoker { 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)?) } diff --git a/src/autorouter/measure_length.rs b/src/autorouter/measure_length.rs index 36c32e3..7aaa588 100644 --- a/src/autorouter/measure_length.rs +++ b/src/autorouter/measure_length.rs @@ -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 {} diff --git a/src/autorouter/place_via.rs b/src/autorouter/place_via.rs index 20b0a97..a297057 100644 --- a/src/autorouter/place_via.rs +++ b/src/autorouter/place_via.rs @@ -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 {} diff --git a/src/autorouter/remove_bands.rs b/src/autorouter/remove_bands.rs index 081fdec..9736ca0 100644 --- a/src/autorouter/remove_bands.rs +++ b/src/autorouter/remove_bands.rs @@ -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 {} diff --git a/src/board/mod.rs b/src/board/mod.rs index e3dcd18..7a51a30 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -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 { layout: Layout, + bands_by_id: BiBTreeMap, // TODO: Simplify access logic to these members so that `#[getter(skip)]`s can be removed. #[getter(skip)] node_to_pinname: BTreeMap, @@ -89,6 +91,7 @@ impl Board { pub fn new(layout: Layout) -> Self { Self { layout, + bands_by_id: BiBTreeMap::new(), node_to_pinname: BTreeMap::new(), band_bandname: BiBTreeMap::new(), } @@ -216,6 +219,10 @@ impl Board { 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 Board { 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 { self.band_bandname diff --git a/src/drawing/head.rs b/src/drawing/head.rs index ea77dc2..1a91ab8 100644 --- a/src/drawing/head.rs +++ b/src/drawing/head.rs @@ -36,6 +36,15 @@ impl<'a, CW: 'a, Cel: 'a, R: 'a> MakeRef<'a, Drawing> for Head { } } +impl Head { + pub fn maybe_cane(&self) -> Option { + 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 diff --git a/src/geometry/geometry.rs b/src/geometry/geometry.rs index bec0b2d..7774e28 100644 --- a/src/geometry/geometry.rs +++ b/src/geometry/geometry.rs @@ -228,6 +228,25 @@ impl< ); } + pub fn is_joined_with(&self, seg: I, node: GenericNode>) -> 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>( &mut self, from: DI, diff --git a/src/interactor/activity.rs b/src/interactor/activity.rs index 9b0aeaa..1debe59 100644 --- a/src/interactor/activity.rs +++ b/src/interactor/activity.rs @@ -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 { Interaction(InteractionStepper), - Execution(ExecutionStepper), + Execution(ExecutionStepper), } -impl Step, String> for ActivityStepper { +impl Step, String> for ActivityStepper { type Error = ActivityError; fn step( @@ -95,7 +102,7 @@ impl Step, String> for ActivityStepper } } -impl Abort> for ActivityStepper { +impl Abort> for ActivityStepper { fn abort(&mut self, context: &mut Invoker) { match self { ActivityStepper::Interaction(interaction) => interaction.abort(context), @@ -104,7 +111,7 @@ impl Abort> for ActivityStepper { } } -impl OnEvent, InteractiveEvent> for ActivityStepper { +impl OnEvent, InteractiveEvent> for ActivityStepper { type Output = Result<(), InteractionError>; fn on_event( @@ -120,27 +127,27 @@ impl OnEvent, InteractiveEvent> for Ac } /// An ActivityStepper that preserves its status -pub struct ActivityStepperWithStatus { - activity: ActivityStepper, +pub struct ActivityStepperWithStatus { + activity: ActivityStepper, maybe_status: Option>, } -impl ActivityStepperWithStatus { - pub fn new_execution(execution: ExecutionStepper) -> ActivityStepperWithStatus { +impl ActivityStepperWithStatus { + pub fn new_execution(execution: ExecutionStepper) -> 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 { &self.activity } @@ -149,7 +156,9 @@ impl ActivityStepperWithStatus { } } -impl Step, String> for ActivityStepperWithStatus { +impl Step, String> + for ActivityStepperWithStatus +{ type Error = ActivityError; fn step( @@ -162,15 +171,15 @@ impl Step, String> for ActivityStepper } } -impl Abort> for ActivityStepperWithStatus { +impl Abort> for ActivityStepperWithStatus { fn abort(&mut self, context: &mut Invoker) { self.maybe_status = Some(ControlFlow::Break(String::from("aborted"))); self.activity.abort(context); } } -impl OnEvent, InteractiveEvent> - for ActivityStepperWithStatus +impl OnEvent, InteractiveEvent> + for ActivityStepperWithStatus { type Output = Result<(), InteractionError>; @@ -183,31 +192,49 @@ impl OnEvent, InteractiveEvent> } } -impl GetMaybeThetastarStepper for ActivityStepperWithStatus { +impl GetActivePolygons for ActivityStepperWithStatus { + fn active_polygons(&self) -> &[GenericIndex] { + self.activity.active_polygons() + } +} + +impl GetMaybeThetastarStepper for ActivityStepperWithStatus { fn maybe_thetastar(&self) -> Option<&ThetastarStepper> { self.activity.maybe_thetastar() } } -impl GetMaybeNavcord for ActivityStepperWithStatus { +impl GetMaybeTopoNavmesh for ActivityStepperWithStatus { + fn maybe_topo_navmesh(&self) -> Option> { + self.activity.maybe_topo_navmesh() + } +} + +impl GetMaybeNavcord for ActivityStepperWithStatus { fn maybe_navcord(&self) -> Option<&Navcord> { self.activity.maybe_navcord() } } -impl GetGhosts for ActivityStepperWithStatus { +impl GetGhosts for ActivityStepperWithStatus { fn ghosts(&self) -> &[PrimitiveShape] { self.activity.ghosts() } } -impl GetObstacles for ActivityStepperWithStatus { +impl GetPolygonalBlockers for ActivityStepperWithStatus { + fn polygonal_blockers(&self) -> &[LineString] { + self.activity.polygonal_blockers() + } +} + +impl GetObstacles for ActivityStepperWithStatus { fn obstacles(&self) -> &[PrimitiveIndex] { self.activity.obstacles() } } -impl GetNavmeshDebugTexts for ActivityStepperWithStatus { +impl GetNavmeshDebugTexts for ActivityStepperWithStatus { fn navnode_debug_text(&self, navnode: NavnodeIndex) -> Option<&str> { self.activity.navnode_debug_text(navnode) } diff --git a/src/interactor/interaction.rs b/src/interactor/interaction.rs index 25dd3a5..1a65c1c 100644 --- a/src/interactor/interaction.rs +++ b/src/interactor/interaction.rs @@ -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 OnEvent, 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 {} diff --git a/src/interactor/interactor.rs b/src/interactor/interactor.rs index cb2679e..47c14da 100644 --- a/src/interactor/interactor.rs +++ b/src/interactor/interactor.rs @@ -27,10 +27,10 @@ use crate::{ /// Structure that manages the invoker and activities pub struct Interactor { invoker: Invoker, - activity: Option, + activity: Option>, } -impl Interactor { +impl Interactor { /// Create a new instance of Interactor with the given Board instance pub fn new(board: Board) -> Result { Ok(Self { @@ -135,7 +135,7 @@ impl Interactor { } /// Returns the currently running activity - pub fn maybe_activity(&self) -> &Option { + pub fn maybe_activity(&self) -> &Option> { &self.activity } } diff --git a/src/layout/layout.rs b/src/layout/layout.rs index 7104fde..a8c4f84 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -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 Layout { } } + // 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 Layout { layer: usize, left: NodeIndex, right: NodeIndex, - ) -> Vec { + ) -> impl Iterator { 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 Layout { ); (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 Layout { // 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> + '_ { + 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 { diff --git a/src/lib.rs b/src/lib.rs index a3bc8a8..0e4789e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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] diff --git a/src/math/mod.rs b/src/math/mod.rs index f2f2103..713d524 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -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, diff --git a/src/math/polygon_tangents.rs b/src/math/polygon_tangents.rs index 4a52e46..bf45f97 100644 --- a/src/math/polygon_tangents.rs +++ b/src/math/polygon_tangents.rs @@ -8,6 +8,24 @@ use super::{ }; use geo::{algorithm::Centroid, Point, Polygon}; +pub fn is_poly_convex_hull_cw(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 { #[error("trying to target empty polygon")] diff --git a/src/math/tunnel.rs b/src/math/tunnel.rs new file mode 100644 index 0000000..95c2c03 --- /dev/null +++ b/src/math/tunnel.rs @@ -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 { + 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)) + } + } +} diff --git a/src/router/mod.rs b/src/router/mod.rs index 310642c..2bb80b2 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -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; diff --git a/src/router/ng/eval.rs b/src/router/ng/eval.rs new file mode 100644 index 0000000..e71daf8 --- /dev/null +++ b/src/router/ng/eval.rs @@ -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, + ) -> Result, EvalException> { + Ok(match self { + Etched::Path(ep) => Some(ep.resolve_to_uid(bands)?), + _ => None, + }) + } +} + +impl<'a> TryFrom<&'a RelaxedPath> for Etched { + type Error = (); + + fn try_from(x: &'a RelaxedPath) -> Result { + match x { + RelaxedPath::Weak(()) => Err(()), + RelaxedPath::Normal(ep) => Ok(Etched::Path(ep.clone())), + } + } +} + +impl AstarContext { + fn evaluate_navmesh_intern( + navmesh: PieNavmeshRef<'_>, + ctx: &Self, + common: &Common, + ins_info: InsertionInfo, + ) -> 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::::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( + navmesh: PieNavmeshRef<'_>, + ctx: &Self, + common: &Common, + ins_info: InsertionInfo, + ) -> 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) + } +} diff --git a/src/router/ng/floating.rs b/src/router/ng/floating.rs new file mode 100644 index 0000000..70f089d --- /dev/null +++ b/src/router/ng/floating.rs @@ -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( + layout: &Layout, + 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 { + Ok(Self { + tunnel: self.tunnel.intersection(&othr.tunnel).ok_or( + EvalException::FloatingEmptyTunnel { + origin: active_head_face, + }, + )?, + }) + } +} diff --git a/src/router/ng/mod.rs b/src/router/ng/mod.rs new file mode 100644 index 0000000..fd6734d --- /dev/null +++ b/src/router/ng/mod.rs @@ -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, +} + +impl EtchedPath { + fn resolve_to_uid( + &self, + bands: &BTreeMap, + ) -> Result { + 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; + +pub type PieNavmeshRef<'a> = navmesh::NavmeshRef<'a, PieNavmeshBase>; + +pub type PieEdgeIndex = + EdgeIndex::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, + + // 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, +} + +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 { + pub layout: Layout, + + pub active_layer: usize, + + /// width per path to be routed + pub widths: BTreeMap, + + /// If non-empty, then routing is only allowed to use these edges + pub allowed_edges: BTreeSet, +} + +/// 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, + + /// length including `active_head` + pub length: f64, + + pub sub: Option, +} + +impl AstarContext { + pub fn last_layout(&self, common: &Common) -> Layout { + 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, + origin: Point, + }, + + #[error("invalid polygon handover arguments")] + InvalidPolyHandoverData { + source_poly_ext: CachedPolyExt, + source_sense: RotationSense, + target_poly_ext: CachedPolyExt, + 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, + new_uid: Option, + }, + + #[error("bend not found")] + BendNotFound { core: FixedDotIndex, uid: BandUid }, + + #[error("edge disallowed: {0:?}")] + EdgeDisallowed(PieEdgeIndex), + + #[error("panicked")] + Panic(Arc), +} + +impl EvalException { + fn ghosts_blockers_and_obstacles( + &self, + ) -> (Vec, Vec, Vec) { + 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( + board: &Board, + active_layer: usize, +) -> Result { + use pie::NavmeshIndex::*; + use spade::Triangulation; + + let triangulation = spade::DelaunayTriangulation::>::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::::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::>(); + + 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, _>(|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::>(); + + // 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(&self, layout: &Layout) -> Point { + self.active_head + .face() + .primitive(layout.drawing()) + .shape() + .center() + } +} + +fn cane_around( + layout: &mut Layout, + recorder: &mut LayoutEdit, + route_length: &mut f64, + old_head: Head, + core: FixedDotIndex, + inner: Option, + sense: RotationSense, + width: f64, +) -> Result { + 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) +} diff --git a/src/router/ng/poly.rs b/src/router/ng/poly.rs new file mode 100644 index 0000000..89bfa07 --- /dev/null +++ b/src/router/ng/poly.rs @@ -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, + + pub apex: FixedDotIndex, + + pub convex_hull: CachedPolyExt, + + pub entry_point: Option, + + pub inner: Option, + + pub cw: RotationSense, +} + +impl PolygonRouting { + /// calculates the convex hull of a poly exterior + pub fn new( + layout: &Layout, + cw: RotationSense, + polyidx: GenericIndex, + 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::>(); + + // `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(&self, layout: &Layout) -> 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 { + // 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( + &self, + layout: &mut Layout, + recorder: &mut LayoutEdit, + route_length: &mut f64, + old_head: Head, + ext_core: FixedDotIndex, + width: f64, + ) -> Result { + super::cane_around( + layout, + recorder, + route_length, + old_head, + ext_core, + self.inner, + self.cw, + width, + ) + } + + pub fn route_to_entry( + &self, + layout: &mut Layout, + 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( + &self, + layout: &mut Layout, + 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)) + } +} diff --git a/src/router/ng/router.rs b/src/router/ng/router.rs new file mode 100644 index 0000000..c6cc04e --- /dev/null +++ b/src/router/ng/router.rs @@ -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; + +#[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 { + 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(&self, common: &mut Common) { + common.widths.insert(self.label(), self.width); + } +} + +/// Manages the autorouting process across multiple ratlines. +pub struct AutorouteExecutionStepper { + pmg_astar: PmgAstar, + common: Common, + original_edge_paths: Box<[EdgePaths]>, + + pub last_layout: Layout, + pub last_recorder: LayoutEdit, + pub last_edge_paths: Box<[EdgePaths]>, + pub last_bands: BTreeMap, + + pub active_polygons: Vec>, + pub ghosts: Vec, + pub polygonal_blockers: Vec, + pub obstacles: Vec, +} + +impl AutorouteExecutionStepper { + fn finish(&mut self) { + self.active_polygons.clear(); + self.ghosts.clear(); + self.obstacles.clear(); + self.polygonal_blockers.clear(); + } +} + +impl Abort<()> for AutorouteExecutionStepper { + 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 AutorouteExecutionStepper { + /// `navmesh` can be initialized using [`calculate_navmesh`](super::calculate_navmesh) + pub fn new( + layout: &Layout, + navmesh: &PieNavmesh, + bands: BTreeMap, + active_layer: usize, + allowed_edges: BTreeSet, + goals: impl Iterator, + ) -> 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 { + 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 GetActivePolygons for AutorouteExecutionStepper { + fn active_polygons(&self) -> &[GenericIndex] { + &self.active_polygons[..] + } +} + +impl GetGhosts for AutorouteExecutionStepper { + fn ghosts(&self) -> &[PrimitiveShape] { + &self.ghosts[..] + } +} + +impl GetMaybeThetastarStepper for AutorouteExecutionStepper {} +impl GetMaybeNavcord for AutorouteExecutionStepper {} +impl GetNavmeshDebugTexts for AutorouteExecutionStepper {} + +impl GetMaybeTopoNavmesh for AutorouteExecutionStepper { + fn maybe_topo_navmesh(&self) -> Option> { + Some(pie::navmesh::NavmeshRef { + nodes: &self.pmg_astar.nodes, + edges: &self.pmg_astar.edges, + edge_paths: &self.last_edge_paths, + }) + } +} + +impl GetObstacles for AutorouteExecutionStepper { + fn obstacles(&self) -> &[PrimitiveIndex] { + &self.obstacles[..] + } +} + +impl GetPolygonalBlockers for AutorouteExecutionStepper { + fn polygonal_blockers(&self) -> &[LineString] { + &self.polygonal_blockers[..] + } +} + +#[derive(Clone, Debug)] +struct ManualRouteStep { + goal: Goal, + edge_paths: Box<[EdgePaths]>, + prev_node: Option>, + cur_node: NavmeshIndex, +} + +/// Manages the manual "etching"/"implementation" of lowering paths from a topological navmesh +/// onto a layout. +pub struct ManualrouteExecutionStepper { + /// remaining steps are in reverse order + remaining_steps: Vec, + context: AstarContext, + aborted: bool, + + // constant stuff + nodes: Arc, Node>>, + edges: Arc>, (Edge, usize)>>, + common: Common, + original_edge_paths: Box<[EdgePaths]>, + + // results + pub last_layout: Layout, + pub last_recorder: LayoutEdit, + + // visualization / debug + pub active_polygons: Vec>, + pub ghosts: Vec, + pub polygonal_blockers: Vec, + pub obstacles: Vec, +} + +impl ManualrouteExecutionStepper { + pub fn last_edge_paths(&self) -> &Box<[EdgePaths]> { + 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 { + &self.context.bands + } +} + +impl ManualrouteExecutionStepper { + fn finish(&mut self) { + self.active_polygons.clear(); + self.ghosts.clear(); + self.obstacles.clear(); + self.polygonal_blockers.clear(); + } +} + +impl Abort<()> for ManualrouteExecutionStepper { + 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 ManualrouteExecutionStepper { + /// `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, + dest_navmesh: &PieNavmesh, + bands: BTreeMap, + active_layer: usize, + goals: impl core::iter::DoubleEndedIterator, + ) -> 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 { + 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 GetActivePolygons for ManualrouteExecutionStepper { + fn active_polygons(&self) -> &[GenericIndex] { + &self.active_polygons[..] + } +} + +impl GetGhosts for ManualrouteExecutionStepper { + fn ghosts(&self) -> &[PrimitiveShape] { + &self.ghosts[..] + } +} + +impl GetMaybeThetastarStepper for ManualrouteExecutionStepper {} +impl GetMaybeNavcord for ManualrouteExecutionStepper {} +impl GetNavmeshDebugTexts for ManualrouteExecutionStepper {} + +impl GetMaybeTopoNavmesh for ManualrouteExecutionStepper { + fn maybe_topo_navmesh(&self) -> Option> { + Some(pie::navmesh::NavmeshRef { + nodes: &self.nodes, + edges: &self.edges, + edge_paths: &self.last_edge_paths(), + }) + } +} + +impl GetObstacles for ManualrouteExecutionStepper { + fn obstacles(&self) -> &[PrimitiveIndex] { + &self.obstacles[..] + } +} + +impl GetPolygonalBlockers for ManualrouteExecutionStepper { + fn polygonal_blockers(&self) -> &[LineString] { + &self.polygonal_blockers[..] + } +}