diff --git a/crates/topola-egui/src/app.rs b/crates/topola-egui/src/app.rs index ce0d900..3d53d10 100644 --- a/crates/topola-egui/src/app.rs +++ b/crates/topola-egui/src/app.rs @@ -176,6 +176,7 @@ impl eframe::App for App { ctx, &mut self.translator, self.content_channel.0.clone(), + &mut self.error_dialog, &mut self.viewport, self.maybe_workspace.as_mut(), ); diff --git a/crates/topola-egui/src/menu_bar.rs b/crates/topola-egui/src/menu_bar.rs index 260e027..d29ed3b 100644 --- a/crates/topola-egui/src/menu_bar.rs +++ b/crates/topola-egui/src/menu_bar.rs @@ -17,6 +17,7 @@ use topola::{ use crate::{ actions::Actions, app::{execute, handle_file}, + error_dialog::ErrorDialog, translator::Translator, viewport::Viewport, workspace::Workspace, @@ -63,6 +64,7 @@ impl MenuBar { ctx: &egui::Context, tr: &mut Translator, content_sender: Sender>, + error_dialog: &mut ErrorDialog, viewport: &mut Viewport, maybe_workspace: Option<&mut Workspace>, ) -> Result<(), InvokerError> { @@ -287,6 +289,7 @@ impl MenuBar { ) { } else if workspace_activities_enabled { fn schedule Command>( + error_dialog: &mut ErrorDialog, workspace: &mut Workspace, op: F, ) { @@ -306,11 +309,13 @@ impl MenuBar { .0 .retain(|i| i.layer == active_layer); } - workspace.interactor.schedule(op(selection)); + if let Err(err) = workspace.interactor.schedule(op(selection)) { + error_dialog.push_error("tr-module-invoker", format!("{}", err)); + } } let opts = self.autorouter_options; if actions.edit.remove_bands.consume_key_triggered(ctx, ui) { - schedule(workspace, |selection| { + schedule(error_dialog, workspace, |selection| { Command::RemoveBands(selection.band_selection) }) } else if actions.route.topo_autoroute.consume_key_triggered(ctx, ui) { @@ -325,15 +330,17 @@ impl MenuBar { .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, + schedule(error_dialog, 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(workspace, |selection| { + schedule(error_dialog, workspace, |selection| { Command::Autoroute(selection.pin_selection, opts) }); } else if actions @@ -341,7 +348,7 @@ impl MenuBar { .compare_detours .consume_key_triggered(ctx, ui) { - schedule(workspace, |selection| { + schedule(error_dialog, workspace, |selection| { Command::CompareDetours(selection.pin_selection, opts) }); } else if actions @@ -349,7 +356,7 @@ impl MenuBar { .measure_length .consume_key_triggered(ctx, ui) { - schedule(workspace, |selection| { + schedule(error_dialog, workspace, |selection| { Command::MeasureLength(selection.band_selection) }); } else if actions diff --git a/src/autorouter/autorouter.rs b/src/autorouter/autorouter.rs index 784a8b9..7d2e02b 100644 --- a/src/autorouter/autorouter.rs +++ b/src/autorouter/autorouter.rs @@ -44,8 +44,8 @@ pub enum AutorouterError { Navmesh(#[from] NavmeshError), #[error("routing failed: {0}")] Thetastar(#[from] ThetastarError), - #[error(transparent)] - Spade(#[from] spade::InsertionError), + #[error("TopoNavmesh generation failed: {0}")] + TopoNavmeshGeneration(#[from] ng::NavmeshCalculationError), #[error("could not place via")] CouldNotPlaceVia(#[from] Infringement), #[error("could not remove band")] diff --git a/src/autorouter/invoker.rs b/src/autorouter/invoker.rs index 7d0bbbb..d8889d3 100644 --- a/src/autorouter/invoker.rs +++ b/src/autorouter/invoker.rs @@ -176,9 +176,9 @@ impl Invoker { &mut self, command: Command, ) -> Result, InvokerError> { - let execute = self.dispatch_command(&command); + let execute = self.dispatch_command(&command)?; self.ongoing_command = Some(command); - execute + Ok(execute) } #[debug_requires(self.ongoing_command.is_none())] diff --git a/src/layout/layout.rs b/src/layout/layout.rs index a8c4f84..b6d3b68 100644 --- a/src/layout/layout.rs +++ b/src/layout/layout.rs @@ -7,7 +7,7 @@ use core::iter; use contracts_try::debug_ensures; use derive_getters::Getters; use enum_dispatch::enum_dispatch; -use geo::Point; +use geo::{Coord, Line, Point}; use planar_incr_embed::RelaxedPath; use rstar::AABB; @@ -42,7 +42,7 @@ use crate::{ poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight}, via::{Via, ViaWeight}, }, - math::{LineIntersection, NormalLine, RotationSense}, + math::{intersect_linestring_and_beam, LineIntersection, NormalLine, RotationSense}, }; /// Represents a weight for various compounds @@ -379,20 +379,12 @@ 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`. - pub fn bands_between_nodes( + fn bands_between_positions_internal( &self, layer: usize, - left: NodeIndex, - right: NodeIndex, - ) -> impl Iterator { - assert_ne!(left, right); - let left_pos = self.node_shape(left).center(); - let right_pos = self.node_shape(right).center(); + left_pos: Point, + right_pos: Point, + ) -> impl Iterator + '_ { let ltr_line = geo::Line { start: left_pos.into(), end: right_pos.into(), @@ -406,11 +398,10 @@ impl Layout { orig_hline.make_normal_unit(); let orig_hline = orig_hline; let location_denom = orig_hline.segment_interval(<r_line); - let location_start = location_denom.start(); - let location_denom = location_denom.end() - location_denom.start(); + let location_start = *location_denom.start(); + let location_denom = *location_denom.end() - *location_denom.start(); - let mut bands: Vec<_> = self - .drawing + self.drawing .rtree() .locate_in_envelope_intersecting(&{ let aabb_init = AABB::from_corners( @@ -432,7 +423,7 @@ impl Layout { let shape = prim.primitive(&self.drawing).shape(); (loose, shape) }) - .filter_map(|(loose, shape)| { + .filter_map(move |(loose, shape)| { let band_uid = self.drawing.loose_band_uid(loose).ok()?; let loose_hline = orig_hline.orthogonal_through(&match shape { PrimitiveShape::Seg(seg) => { @@ -461,6 +452,22 @@ impl Layout { .contains(&location) .then_some((location, band_uid, loose)) }) + } + + /// 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( + &self, + layer: usize, + left: NodeIndex, + right: NodeIndex, + ) -> impl Iterator { + assert_ne!(left, right); + let left_pos = self.node_shape(left).center(); + let right_pos = self.node_shape(right).center(); + let mut bands: Vec<_> = self + .bands_between_positions_internal(layer, left_pos, right_pos) .filter(|(_, band_uid, _)| { // filter entries which are connected to either lhs or rhs (and possibly both) let (bts1, bts2) = band_uid.into(); @@ -482,6 +489,49 @@ impl Layout { .map(|(_, band_uid, loose)| (band_uid, loose)) } + /// Finds all bands on `layer` between direction `left` and node `right` + /// and returns them ordered from `left` to `right`. + pub fn bands_between_node_and_boundary( + &self, + layer: usize, + left: Coord, + right: NodeIndex, + ) -> Option> { + // First, decode the `left` direction into a point on the boundary + let right_pos = self.node_shape(right).center(); + let left_pos = intersect_linestring_and_beam( + self.drawing.boundary().exterior(), + &Line { + start: right_pos.0, + end: right_pos.0 + left, + }, + )?; + + let mut bands: Vec<_> = self + .bands_between_positions_internal(layer, left_pos, right_pos) + .filter(|(_, band_uid, _)| { + // filter entries which are connected to rhs + let (bts1, bts2) = band_uid.into(); + let (bts1, bts2) = (bts1.petgraph_index(), bts2.petgraph_index()); + let geometry = self.drawing.geometry(); + [(bts1, right), (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)); + + // TODO: handle "loops" of bands, or multiple primitives from the band crossing the segment + // 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. + + Some( + 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 { diff --git a/src/math/mod.rs b/src/math/mod.rs index 713d524..27cbe25 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT use geo::algorithm::line_measures::{Distance, Euclidean}; -use geo::{geometry::Point, point, Line}; +use geo::{point, Line, LineString, Point}; pub use specctra_core::math::{Circle, PointWithRotation}; mod cyclic_search; @@ -102,8 +102,8 @@ impl NormalLine { let apt = geo::point! { x: a.x, y: a.y }; let bpt = geo::point! { x: b.x, y: b.y }; let det = perp_dot_product(apt, bpt); - let rpx = -b.y * a.offset + a.y * b.offset; - let rpy = b.x * a.offset - a.x * b.offset; + let rpx = b.y * a.offset - a.y * b.offset; + let rpy = -b.x * a.offset + a.x * b.offset; if det.abs() > ALMOST_ZERO { LineIntersection::Point(geo::point! { x: rpx, y: rpy } / det) @@ -302,6 +302,16 @@ pub fn intersect_line_and_beam(line1: &Line, beam2: &Line) -> Option { } } +/// Returns `Some(p)` when `p` lies in the intersection of a linestring and a beam +pub fn intersect_linestring_and_beam(linestring: &LineString, beam: &Line) -> Option { + for line in linestring.lines() { + if let Some(pt) = intersect_line_and_beam(&line, beam) { + return Some(pt); + } + } + None +} + /// Returns `true` the point `p` is between the supporting lines of vectors /// `from` and `to`. pub fn between_vectors(p: Point, from: Point, to: Point) -> bool { @@ -446,4 +456,22 @@ mod tests { None ); } + + #[test] + fn intersect_line_and_beam02() { + let pt = intersect_line_and_beam( + &Line { + start: geo::coord! { x: 140., y: -110. }, + end: geo::coord! { x: 160., y: -110. }, + }, + &Line { + start: geo::coord! { x: 148., y: -106. }, + end: geo::coord! { x: 148., y: -109. }, + }, + ) + .unwrap(); + + approx::assert_abs_diff_eq!(pt.x(), 148.); + approx::assert_abs_diff_eq!(pt.y(), -110.); + } } diff --git a/src/router/ng/mod.rs b/src/router/ng/mod.rs index 7241fa4..1535042 100644 --- a/src/router/ng/mod.rs +++ b/src/router/ng/mod.rs @@ -8,7 +8,7 @@ use pie::{ }; pub use planar_incr_embed as pie; -use geo::geometry::{LineString, Point}; +use geo::{Coord, LineString, Point}; use rstar::AABB; use std::{ collections::{BTreeMap, BTreeSet}, @@ -274,10 +274,27 @@ impl EvalException { } } +#[derive(Clone, Debug, thiserror::Error)] +pub enum NavmeshCalculationError { + #[error("Layer contains too few nodes to generate meaningful navmesh")] + NotEnoughNodes, + + #[error("Unable to find boundary from node {node:?}, direction {direction:?}")] + UnableToFindBoundary { + node: FixedDotIndex, + direction: Coord, + }, + + #[error(transparent)] + Insertion(#[from] spade::InsertionError), +} + +/// NOTE: this only works if the layer has ≥ 3 nodes +// TODO: handle the case with 2 nodes on the layer specifically. pub fn calculate_navmesh( board: &Board, active_layer: usize, -) -> Result { +) -> Result { use pie::NavmeshIndex::*; use spade::Triangulation; @@ -302,37 +319,142 @@ pub fn calculate_navmesh( .collect(), )?; + if triangulation.num_inner_faces() == 0 { + log::warn!("calculate_navmesh: not enough nodes"); + return Err(NavmeshCalculationError::NotEnoughNodes); + } + 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::>(); + let barrier0: Arc<[RelaxedPath<_, _>]> = Arc::from(vec![]); - if bands != *barrier2 { - log::debug!("navmesh generated with {:?} = {:?}", value, &bands); - value.1 = Arc::from(bands); + log::debug!("boundary = {:?}", board.layout().drawing().boundary()); + + // populate Dual*-Dual* routed traces + for (key, value) in &mut navmesh.edges { + let wrap = |dot| GenericNode::Primitive(PrimitiveIndex::FixedDot(dot)); + match (value.0.lhs, value.0.rhs) { + (Some(lhs), Some(rhs)) => { + value.1 = barrier2.clone(); + 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); + } + } + (None, Some(rhs)) => { + value.1 = barrier0.clone(); + let direction = { + let (prev_key, next_key) = key.into(); + let prev_dir = navmesh.nodes[prev_key] + .open_direction + .expect("expected DualOuter entry"); + let next_dir = navmesh.nodes[next_key] + .open_direction + .expect("expected DualOuter entry"); + Coord { + x: (prev_dir.x + next_dir.x) / 2.0, + y: (prev_dir.y + next_dir.y) / 2.0, + } + }; + let bands = match board.layout().bands_between_node_and_boundary( + active_layer, + direction, + wrap(rhs), + ) { + None => { + log::warn!("calculate_navmesh: unable to find boundary from node {:?}, direction {:?}", rhs, direction); + continue; + /* + return Err(NavmeshCalculationError::UnableToFindBoundary { + node: rhs, + direction, + }); + */ + } + Some(x) => { + log::debug!("calculate_navmesh: successfully found boundary from node {:?}, direction {:?}", rhs, direction); + x.map(|(band_uid, _)| { + RelaxedPath::Normal( + *board.bands_by_id().get_by_right(&band_uid).unwrap(), + ) + }) + .collect::>() + } + }; + + if bands != *barrier0 { + log::debug!("navmesh generated with {:?} = {:?}", value, &bands); + value.1 = Arc::from(bands); + } + } + (Some(lhs), None) => { + value.1 = barrier0.clone(); + let direction = { + let (prev_key, next_key) = key.into(); + let prev_dir = navmesh.nodes[prev_key] + .open_direction + .expect("expected DualOuter entry"); + let next_dir = navmesh.nodes[next_key] + .open_direction + .expect("expected DualOuter entry"); + Coord { + x: (prev_dir.x + next_dir.x) / 2.0, + y: (prev_dir.y + next_dir.y) / 2.0, + } + }; + let mut bands = match board.layout().bands_between_node_and_boundary( + active_layer, + direction, + wrap(lhs), + ) { + None => { + log::warn!("calculate_navmesh: unable to find boundary from node {:?}, direction {:?}", lhs, direction); + continue; + /* + return Err(NavmeshCalculationError::UnableToFindBoundary { + node: rhs, + direction, + }); + */ + } + Some(x) => { + log::debug!("calculate_navmesh: successfully found boundary from node {:?}, direction {:?}", lhs, direction); + x.map(|(band_uid, _)| { + RelaxedPath::Normal( + *board.bands_by_id().get_by_right(&band_uid).unwrap(), + ) + }) + .collect::>() + } + }; + bands.reverse(); + + if bands != *barrier0 { + log::debug!("navmesh generated with {:?} = {:?}", value, &bands); + value.1 = Arc::from(bands); + } + } + (None, None) => { + // nothing to do } } } - // 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 + + // TODO: insert fixed routed traces/bands into the navmesh // populate Primal-Dual* routed traces let dual_ends: BTreeMap<_, _> = navmesh