feat: Implement TopoNavmesh DualOuter-DualOuter extraction

This also fixes a sign bug in LineIntersection calculation
This commit is contained in:
Ellen Emilia Anna Zscheile 2025-06-19 21:33:31 +02:00
parent cfde2eac20
commit d3dc826be4
7 changed files with 268 additions and 60 deletions

View File

@ -176,6 +176,7 @@ impl eframe::App for App {
ctx, ctx,
&mut self.translator, &mut self.translator,
self.content_channel.0.clone(), self.content_channel.0.clone(),
&mut self.error_dialog,
&mut self.viewport, &mut self.viewport,
self.maybe_workspace.as_mut(), self.maybe_workspace.as_mut(),
); );

View File

@ -17,6 +17,7 @@ use topola::{
use crate::{ use crate::{
actions::Actions, actions::Actions,
app::{execute, handle_file}, app::{execute, handle_file},
error_dialog::ErrorDialog,
translator::Translator, translator::Translator,
viewport::Viewport, viewport::Viewport,
workspace::Workspace, workspace::Workspace,
@ -63,6 +64,7 @@ impl MenuBar {
ctx: &egui::Context, ctx: &egui::Context,
tr: &mut Translator, tr: &mut Translator,
content_sender: Sender<Result<SpecctraDesign, SpecctraLoadingError>>, content_sender: Sender<Result<SpecctraDesign, SpecctraLoadingError>>,
error_dialog: &mut ErrorDialog,
viewport: &mut Viewport, viewport: &mut Viewport,
maybe_workspace: Option<&mut Workspace>, maybe_workspace: Option<&mut Workspace>,
) -> Result<(), InvokerError> { ) -> Result<(), InvokerError> {
@ -287,6 +289,7 @@ impl MenuBar {
) { ) {
} else if workspace_activities_enabled { } else if workspace_activities_enabled {
fn schedule<F: FnOnce(Selection) -> Command>( fn schedule<F: FnOnce(Selection) -> Command>(
error_dialog: &mut ErrorDialog,
workspace: &mut Workspace, workspace: &mut Workspace,
op: F, op: F,
) { ) {
@ -306,11 +309,13 @@ impl MenuBar {
.0 .0
.retain(|i| i.layer == active_layer); .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; let opts = self.autorouter_options;
if actions.edit.remove_bands.consume_key_triggered(ctx, ui) { if actions.edit.remove_bands.consume_key_triggered(ctx, ui) {
schedule(workspace, |selection| { schedule(error_dialog, workspace, |selection| {
Command::RemoveBands(selection.band_selection) Command::RemoveBands(selection.band_selection)
}) })
} else if actions.route.topo_autoroute.consume_key_triggered(ctx, ui) { } else if actions.route.topo_autoroute.consume_key_triggered(ctx, ui) {
@ -325,15 +330,17 @@ impl MenuBar {
.layer_layername(active_layer) .layer_layername(active_layer)
.expect("unknown active layer") .expect("unknown active layer")
.to_string(); .to_string();
schedule(workspace, |selection| Command::TopoAutoroute { schedule(error_dialog, workspace, |selection| {
Command::TopoAutoroute {
selection: selection.pin_selection, selection: selection.pin_selection,
allowed_edges: BTreeSet::new(), allowed_edges: BTreeSet::new(),
active_layer, active_layer,
routed_band_width: opts.router_options.routed_band_width, routed_band_width: opts.router_options.routed_band_width,
}
}); });
} }
} else if actions.route.autoroute.consume_key_triggered(ctx, ui) { } else if actions.route.autoroute.consume_key_triggered(ctx, ui) {
schedule(workspace, |selection| { schedule(error_dialog, workspace, |selection| {
Command::Autoroute(selection.pin_selection, opts) Command::Autoroute(selection.pin_selection, opts)
}); });
} else if actions } else if actions
@ -341,7 +348,7 @@ impl MenuBar {
.compare_detours .compare_detours
.consume_key_triggered(ctx, ui) .consume_key_triggered(ctx, ui)
{ {
schedule(workspace, |selection| { schedule(error_dialog, workspace, |selection| {
Command::CompareDetours(selection.pin_selection, opts) Command::CompareDetours(selection.pin_selection, opts)
}); });
} else if actions } else if actions
@ -349,7 +356,7 @@ impl MenuBar {
.measure_length .measure_length
.consume_key_triggered(ctx, ui) .consume_key_triggered(ctx, ui)
{ {
schedule(workspace, |selection| { schedule(error_dialog, workspace, |selection| {
Command::MeasureLength(selection.band_selection) Command::MeasureLength(selection.band_selection)
}); });
} else if actions } else if actions

View File

@ -44,8 +44,8 @@ pub enum AutorouterError {
Navmesh(#[from] NavmeshError), Navmesh(#[from] NavmeshError),
#[error("routing failed: {0}")] #[error("routing failed: {0}")]
Thetastar(#[from] ThetastarError), Thetastar(#[from] ThetastarError),
#[error(transparent)] #[error("TopoNavmesh generation failed: {0}")]
Spade(#[from] spade::InsertionError), TopoNavmeshGeneration(#[from] ng::NavmeshCalculationError),
#[error("could not place via")] #[error("could not place via")]
CouldNotPlaceVia(#[from] Infringement), CouldNotPlaceVia(#[from] Infringement),
#[error("could not remove band")] #[error("could not remove band")]

View File

@ -176,9 +176,9 @@ impl<M: AccessMesadata + Clone> Invoker<M> {
&mut self, &mut self,
command: Command, command: Command,
) -> Result<ExecutionStepper<M>, InvokerError> { ) -> Result<ExecutionStepper<M>, InvokerError> {
let execute = self.dispatch_command(&command); let execute = self.dispatch_command(&command)?;
self.ongoing_command = Some(command); self.ongoing_command = Some(command);
execute Ok(execute)
} }
#[debug_requires(self.ongoing_command.is_none())] #[debug_requires(self.ongoing_command.is_none())]

View File

@ -7,7 +7,7 @@ use core::iter;
use contracts_try::debug_ensures; use contracts_try::debug_ensures;
use derive_getters::Getters; use derive_getters::Getters;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use geo::Point; use geo::{Coord, Line, Point};
use planar_incr_embed::RelaxedPath; use planar_incr_embed::RelaxedPath;
use rstar::AABB; use rstar::AABB;
@ -42,7 +42,7 @@ use crate::{
poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight}, poly::{add_poly_with_nodes_intern, MakePolygon, PolyWeight},
via::{Via, ViaWeight}, via::{Via, ViaWeight},
}, },
math::{LineIntersection, NormalLine, RotationSense}, math::{intersect_linestring_and_beam, LineIntersection, NormalLine, RotationSense},
}; };
/// Represents a weight for various compounds /// Represents a weight for various compounds
@ -379,20 +379,12 @@ impl<R: AccessRules> Layout<R> {
} }
} }
// TODO: computation of bands between outer node and direction towards "outside" fn bands_between_positions_internal(
/// 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, &self,
layer: usize, layer: usize,
left: NodeIndex, left_pos: Point,
right: NodeIndex, right_pos: Point,
) -> impl Iterator<Item = (BandUid, LooseIndex)> { ) -> impl Iterator<Item = (f64, BandUid, LooseIndex)> + '_ {
assert_ne!(left, right);
let left_pos = self.node_shape(left).center();
let right_pos = self.node_shape(right).center();
let ltr_line = geo::Line { let ltr_line = geo::Line {
start: left_pos.into(), start: left_pos.into(),
end: right_pos.into(), end: right_pos.into(),
@ -406,11 +398,10 @@ impl<R: AccessRules> Layout<R> {
orig_hline.make_normal_unit(); orig_hline.make_normal_unit();
let orig_hline = orig_hline; let orig_hline = orig_hline;
let location_denom = orig_hline.segment_interval(&ltr_line); let location_denom = orig_hline.segment_interval(&ltr_line);
let location_start = location_denom.start(); let location_start = *location_denom.start();
let location_denom = location_denom.end() - location_denom.start(); let location_denom = *location_denom.end() - *location_denom.start();
let mut bands: Vec<_> = self self.drawing
.drawing
.rtree() .rtree()
.locate_in_envelope_intersecting(&{ .locate_in_envelope_intersecting(&{
let aabb_init = AABB::from_corners( let aabb_init = AABB::from_corners(
@ -432,7 +423,7 @@ impl<R: AccessRules> Layout<R> {
let shape = prim.primitive(&self.drawing).shape(); let shape = prim.primitive(&self.drawing).shape();
(loose, shape) (loose, shape)
}) })
.filter_map(|(loose, shape)| { .filter_map(move |(loose, shape)| {
let band_uid = self.drawing.loose_band_uid(loose).ok()?; let band_uid = self.drawing.loose_band_uid(loose).ok()?;
let loose_hline = orig_hline.orthogonal_through(&match shape { let loose_hline = orig_hline.orthogonal_through(&match shape {
PrimitiveShape::Seg(seg) => { PrimitiveShape::Seg(seg) => {
@ -461,6 +452,22 @@ impl<R: AccessRules> Layout<R> {
.contains(&location) .contains(&location)
.then_some((location, band_uid, loose)) .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<Item = (BandUid, LooseIndex)> {
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(|(_, band_uid, _)| {
// filter entries which are connected to either lhs or rhs (and possibly both) // filter entries which are connected to either lhs or rhs (and possibly both)
let (bts1, bts2) = band_uid.into(); let (bts1, bts2) = band_uid.into();
@ -482,6 +489,49 @@ impl<R: AccessRules> Layout<R> {
.map(|(_, band_uid, loose)| (band_uid, loose)) .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<impl Iterator<Item = (BandUid, LooseIndex)>> {
// 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 { fn does_compound_have_core(&self, primary: NodeIndex, core: DotIndex) -> bool {
let core: PrimitiveIndex = core.into(); let core: PrimitiveIndex = core.into();
match primary { match primary {

View File

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use geo::algorithm::line_measures::{Distance, Euclidean}; 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}; pub use specctra_core::math::{Circle, PointWithRotation};
mod cyclic_search; mod cyclic_search;
@ -102,8 +102,8 @@ impl NormalLine {
let apt = geo::point! { x: a.x, y: a.y }; let apt = geo::point! { x: a.x, y: a.y };
let bpt = geo::point! { x: b.x, y: b.y }; let bpt = geo::point! { x: b.x, y: b.y };
let det = perp_dot_product(apt, bpt); let det = perp_dot_product(apt, bpt);
let rpx = -b.y * a.offset + a.y * b.offset; let rpx = b.y * a.offset - a.y * b.offset;
let rpy = b.x * a.offset - a.x * b.offset; let rpy = -b.x * a.offset + a.x * b.offset;
if det.abs() > ALMOST_ZERO { if det.abs() > ALMOST_ZERO {
LineIntersection::Point(geo::point! { x: rpx, y: rpy } / det) LineIntersection::Point(geo::point! { x: rpx, y: rpy } / det)
@ -302,6 +302,16 @@ pub fn intersect_line_and_beam(line1: &Line, beam2: &Line) -> Option<Point> {
} }
} }
/// 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<Point> {
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 /// Returns `true` the point `p` is between the supporting lines of vectors
/// `from` and `to`. /// `from` and `to`.
pub fn between_vectors(p: Point, from: Point, to: Point) -> bool { pub fn between_vectors(p: Point, from: Point, to: Point) -> bool {
@ -446,4 +456,22 @@ mod tests {
None 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.);
}
} }

View File

@ -8,7 +8,7 @@ use pie::{
}; };
pub use planar_incr_embed as pie; pub use planar_incr_embed as pie;
use geo::geometry::{LineString, Point}; use geo::{Coord, LineString, Point};
use rstar::AABB; use rstar::AABB;
use std::{ use std::{
collections::{BTreeMap, BTreeSet}, 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<R: AccessRules>( pub fn calculate_navmesh<R: AccessRules>(
board: &Board<R>, board: &Board<R>,
active_layer: usize, active_layer: usize,
) -> Result<PieNavmesh, spade::InsertionError> { ) -> Result<PieNavmesh, NavmeshCalculationError> {
use pie::NavmeshIndex::*; use pie::NavmeshIndex::*;
use spade::Triangulation; use spade::Triangulation;
@ -302,24 +319,34 @@ pub fn calculate_navmesh<R: AccessRules>(
.collect(), .collect(),
)?; )?;
if triangulation.num_inner_faces() == 0 {
log::warn!("calculate_navmesh: not enough nodes");
return Err(NavmeshCalculationError::NotEnoughNodes);
}
let mut navmesh = navmesh::NavmeshSer::<PieNavmeshBase>::from_triangulation(&triangulation); let mut navmesh = navmesh::NavmeshSer::<PieNavmeshBase>::from_triangulation(&triangulation);
let barrier2: Arc<[RelaxedPath<_, _>]> = let barrier2: Arc<[RelaxedPath<_, _>]> =
Arc::from(vec![RelaxedPath::Weak(()), RelaxedPath::Weak(())]); Arc::from(vec![RelaxedPath::Weak(()), RelaxedPath::Weak(())]);
// populate DualInner-Dual* routed traces let barrier0: Arc<[RelaxedPath<_, _>]> = Arc::from(vec![]);
for value in navmesh.edges.values_mut() {
if let (Some(lhs), Some(rhs)) = (value.0.lhs, value.0.rhs) { log::debug!("boundary = {:?}", board.layout().drawing().boundary());
value.1 = barrier2.clone();
// populate Dual*-Dual* routed traces
for (key, value) in &mut navmesh.edges {
let wrap = |dot| GenericNode::Primitive(PrimitiveIndex::FixedDot(dot)); 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 let bands = board
.layout() .layout()
.bands_between_nodes_with_alignment(active_layer, wrap(lhs), wrap(rhs)) .bands_between_nodes_with_alignment(active_layer, wrap(lhs), wrap(rhs))
.map(|i| match i { .map(|i| match i {
RelaxedPath::Weak(()) => RelaxedPath::Weak(()), RelaxedPath::Weak(()) => RelaxedPath::Weak(()),
RelaxedPath::Normal(band_uid) => { RelaxedPath::Normal(band_uid) => RelaxedPath::Normal(
RelaxedPath::Normal(*board.bands_by_id().get_by_right(&band_uid).unwrap()) *board.bands_by_id().get_by_right(&band_uid).unwrap(),
} ),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -328,11 +355,106 @@ pub fn calculate_navmesh<R: AccessRules>(
value.1 = Arc::from(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,
} }
// TODO: insert fixed and outer routed traces/bands into the navmesh };
// see also: https://codeberg.org/topola/topola/issues/166 let bands = match board.layout().bands_between_node_and_boundary(
// due to not handling outer routed traces/bends, active_layer,
// the above code might produce an inconsistent navmesh 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::<Vec<_>>()
}
};
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::<Vec<_>>()
}
};
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 routed traces/bands into the navmesh
// populate Primal-Dual* routed traces // populate Primal-Dual* routed traces
let dual_ends: BTreeMap<_, _> = navmesh let dual_ends: BTreeMap<_, _> = navmesh