mirror of https://codeberg.org/topola/topola.git
feat: Implement TopoNavmesh DualOuter-DualOuter extraction
This also fixes a sign bug in LineIntersection calculation
This commit is contained in:
parent
cfde2eac20
commit
d3dc826be4
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<Result<SpecctraDesign, SpecctraLoadingError>>,
|
||||
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<F: FnOnce(Selection) -> 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 {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -176,9 +176,9 @@ impl<M: AccessMesadata + Clone> Invoker<M> {
|
|||
&mut self,
|
||||
command: Command,
|
||||
) -> Result<ExecutionStepper<M>, 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())]
|
||||
|
|
|
|||
|
|
@ -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<R: AccessRules> Layout<R> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: computation of bands between outer node and direction towards "outside"
|
||||
|
||||
/// Finds all bands on `layer` between `left` and `right`
|
||||
/// (usually assuming `left` and `right` are neighbors in a Delaunay triangulation)
|
||||
/// and returns them ordered from `left` to `right`.
|
||||
pub fn bands_between_nodes(
|
||||
fn bands_between_positions_internal(
|
||||
&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();
|
||||
left_pos: Point,
|
||||
right_pos: Point,
|
||||
) -> impl Iterator<Item = (f64, BandUid, LooseIndex)> + '_ {
|
||||
let ltr_line = geo::Line {
|
||||
start: left_pos.into(),
|
||||
end: right_pos.into(),
|
||||
|
|
@ -406,11 +398,10 @@ impl<R: AccessRules> Layout<R> {
|
|||
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<R: AccessRules> Layout<R> {
|
|||
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<R: AccessRules> Layout<R> {
|
|||
.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<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 entries which are connected to either lhs or rhs (and possibly both)
|
||||
let (bts1, bts2) = band_uid.into();
|
||||
|
|
@ -482,6 +489,49 @@ impl<R: AccessRules> Layout<R> {
|
|||
.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 {
|
||||
let core: PrimitiveIndex = core.into();
|
||||
match primary {
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
/// `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.);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<R: AccessRules>(
|
||||
board: &Board<R>,
|
||||
active_layer: usize,
|
||||
) -> Result<PieNavmesh, spade::InsertionError> {
|
||||
) -> Result<PieNavmesh, NavmeshCalculationError> {
|
||||
use pie::NavmeshIndex::*;
|
||||
use spade::Triangulation;
|
||||
|
||||
|
|
@ -302,24 +319,34 @@ pub fn calculate_navmesh<R: AccessRules>(
|
|||
.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 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 barrier0: Arc<[RelaxedPath<_, _>]> = Arc::from(vec![]);
|
||||
|
||||
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())
|
||||
}
|
||||
RelaxedPath::Normal(band_uid) => RelaxedPath::Normal(
|
||||
*board.bands_by_id().get_by_right(&band_uid).unwrap(),
|
||||
),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
|
@ -328,11 +355,106 @@ pub fn calculate_navmesh<R: AccessRules>(
|
|||
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
|
||||
// due to not handling outer routed traces/bends,
|
||||
// the above code might produce an inconsistent navmesh
|
||||
};
|
||||
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::<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
|
||||
let dual_ends: BTreeMap<_, _> = navmesh
|
||||
|
|
|
|||
Loading…
Reference in New Issue