feat(router/navmesh): Add constraint edges for loose segs

This does not work entirely correctly. I will investigate in subsequent
commits.
This commit is contained in:
Mikolaj Wielgus 2025-07-04 00:45:26 +02:00 committed by mikolaj
parent 274ad166c1
commit f3245b9607
6 changed files with 161 additions and 22 deletions

View File

@ -166,6 +166,7 @@ pub struct ViewActions {
pub zoom_to_fit: Switch, pub zoom_to_fit: Switch,
pub show_ratsnest: Switch, pub show_ratsnest: Switch,
pub show_navmesh: Switch, pub show_navmesh: Switch,
pub show_triangulation: Switch,
pub show_pathfinding_scores: Switch, pub show_pathfinding_scores: Switch,
pub show_topo_navmesh: Switch, pub show_topo_navmesh: Switch,
pub show_bboxes: Switch, pub show_bboxes: Switch,
@ -179,6 +180,8 @@ impl ViewActions {
zoom_to_fit: Action::new_keyless(tr.text("tr-menu-view-zoom-to-fit")).into_switch(), zoom_to_fit: Action::new_keyless(tr.text("tr-menu-view-zoom-to-fit")).into_switch(),
show_ratsnest: Action::new_keyless(tr.text("tr-menu-view-show-ratsnest")).into_switch(), show_ratsnest: Action::new_keyless(tr.text("tr-menu-view-show-ratsnest")).into_switch(),
show_navmesh: Action::new_keyless(tr.text("tr-menu-view-show-navmesh")).into_switch(), show_navmesh: Action::new_keyless(tr.text("tr-menu-view-show-navmesh")).into_switch(),
show_triangulation: Action::new_keyless(tr.text("tr-menu-view-show-triangulation"))
.into_switch(),
show_pathfinding_scores: Action::new_keyless( show_pathfinding_scores: Action::new_keyless(
tr.text("tr-menu-view-show-pathfinding-scores"), tr.text("tr-menu-view-show-pathfinding-scores"),
) )
@ -211,6 +214,8 @@ impl ViewActions {
ui.add_enabled_ui(have_workspace, |ui| { ui.add_enabled_ui(have_workspace, |ui| {
self.show_ratsnest.checkbox(ui, &mut menu_bar.show_ratsnest); self.show_ratsnest.checkbox(ui, &mut menu_bar.show_ratsnest);
self.show_navmesh.checkbox(ui, &mut menu_bar.show_navmesh); self.show_navmesh.checkbox(ui, &mut menu_bar.show_navmesh);
self.show_triangulation
.checkbox(ui, &mut menu_bar.show_triangulation);
self.show_pathfinding_scores self.show_pathfinding_scores
.checkbox(ui, &mut menu_bar.show_pathfinding_scores); .checkbox(ui, &mut menu_bar.show_pathfinding_scores);
self.show_topo_navmesh self.show_topo_navmesh

View File

@ -28,6 +28,7 @@ pub struct MenuBar {
pub is_placing_via: bool, pub is_placing_via: bool,
pub show_ratsnest: bool, pub show_ratsnest: bool,
pub show_navmesh: bool, pub show_navmesh: bool,
pub show_triangulation: bool,
pub show_pathfinding_scores: bool, pub show_pathfinding_scores: bool,
pub show_topo_navmesh: bool, pub show_topo_navmesh: bool,
pub show_bboxes: bool, pub show_bboxes: bool,
@ -50,6 +51,7 @@ impl MenuBar {
is_placing_via: false, is_placing_via: false,
show_ratsnest: true, show_ratsnest: true,
show_navmesh: false, show_navmesh: false,
show_triangulation: false,
show_pathfinding_scores: false, show_pathfinding_scores: false,
show_topo_navmesh: false, show_topo_navmesh: false,
show_bboxes: false, show_bboxes: false,

View File

@ -23,7 +23,10 @@ use topola::{
}, },
layout::poly::MakePolygon, layout::poly::MakePolygon,
math::{Circle, RotationSense}, math::{Circle, RotationSense},
router::{navmesh::NavnodeIndex, ng::pie}, router::{
navmesh::{BinavnodeNodeIndex, NavnodeIndex},
ng::pie,
},
}; };
use crate::{ use crate::{
@ -36,8 +39,6 @@ pub struct Viewport {
/// how much should a single arrow key press scroll /// how much should a single arrow key press scroll
pub kbd_scroll_delta_factor: f32, pub kbd_scroll_delta_factor: f32,
pub scheduled_zoom_to_fit: bool, pub scheduled_zoom_to_fit: bool,
update_counter: f32,
} }
impl Viewport { impl Viewport {
@ -46,7 +47,6 @@ impl Viewport {
transform: egui::emath::TSTransform::new([0.0, 0.0].into(), 0.01), transform: egui::emath::TSTransform::new([0.0, 0.0].into(), 0.01),
kbd_scroll_delta_factor: 5.0, kbd_scroll_delta_factor: 5.0,
scheduled_zoom_to_fit: false, scheduled_zoom_to_fit: false,
update_counter: 0.0,
} }
} }
@ -271,8 +271,8 @@ impl Viewport {
if menu_bar.show_navmesh { if menu_bar.show_navmesh {
if let Some(activity) = workspace.interactor.maybe_activity() { if let Some(activity) = workspace.interactor.maybe_activity() {
if let Some(astar) = activity.maybe_thetastar() { if let Some(thetastar) = activity.maybe_thetastar() {
let navmesh = astar.graph(); let navmesh = thetastar.graph();
for edge in navmesh.edge_references() { for edge in navmesh.edge_references() {
let mut from = PrimitiveIndex::from( let mut from = PrimitiveIndex::from(
@ -376,13 +376,13 @@ impl Viewport {
if menu_bar.show_pathfinding_scores { if menu_bar.show_pathfinding_scores {
//TODO "{astar.scores[index]} ({astar.estimate_scores[index]}) (...)" //TODO "{astar.scores[index]} ({astar.estimate_scores[index]}) (...)"
let score_text = astar let score_text = thetastar
.scores() .scores()
.get(&navnode) .get(&navnode)
.map_or_else(String::new, |s| { .map_or_else(String::new, |s| {
format!("g={:.2}", s) format!("g={:.2}", s)
}); });
let estimate_score_text = astar let estimate_score_text = thetastar
.estimate_scores() .estimate_scores()
.get(&navnode) .get(&navnode)
.map_or_else(String::new, |s| { .map_or_else(String::new, |s| {
@ -406,6 +406,38 @@ impl Viewport {
} }
} }
if menu_bar.show_triangulation {
if let Some(activity) = workspace.interactor.maybe_activity() {
if let Some(thetastar) = activity.maybe_thetastar() {
let navmesh = thetastar.graph();
for edge in navmesh.triangulation().edge_references() {
let from = PrimitiveIndex::from(BinavnodeNodeIndex::from(
edge.source(),
))
.primitive(board.layout().drawing())
.shape()
.center();
let to = PrimitiveIndex::from(BinavnodeNodeIndex::from(
edge.target(),
))
.primitive(board.layout().drawing())
.shape()
.center();
painter.paint_edge(
from,
to,
egui::Stroke::new(
1.0,
egui::Color32::from_rgb(255, 255, 255),
),
);
}
}
}
}
if menu_bar.show_topo_navmesh { if menu_bar.show_topo_navmesh {
if let Some(navmesh) = workspace if let Some(navmesh) = workspace
.interactor .interactor

View File

@ -22,6 +22,7 @@ tr-menu-view = View
tr-menu-view-zoom-to-fit = Zoom to Fit tr-menu-view-zoom-to-fit = Zoom to Fit
tr-menu-view-show-ratsnest = Show Ratsnest tr-menu-view-show-ratsnest = Show Ratsnest
tr-menu-view-show-navmesh = Show Navmesh tr-menu-view-show-navmesh = Show Navmesh
tr-menu-view-show-triangulation = Show Triangulation
tr-menu-view-show-pathfinding-scores = Show Pathfinding Scores tr-menu-view-show-pathfinding-scores = Show Pathfinding Scores
tr-menu-view-show-topo-navmesh = Show Topological Navmesh tr-menu-view-show-topo-navmesh = Show Topological Navmesh
tr-menu-view-show-bboxes = Show BBoxes tr-menu-view-show-bboxes = Show BBoxes

View File

@ -4,6 +4,7 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use derive_getters::Getters;
use enum_dispatch::enum_dispatch; use enum_dispatch::enum_dispatch;
use geo::Point; use geo::Point;
use petgraph::{ use petgraph::{
@ -21,10 +22,10 @@ use thiserror::Error;
use crate::{ use crate::{
drawing::{ drawing::{
bend::{FixedBendIndex, LooseBendIndex}, bend::{FixedBendIndex, LooseBendIndex},
dot::FixedDotIndex, dot::{DotIndex, FixedDotIndex},
gear::{GearIndex, GetNextGear}, gear::{GearIndex, GetNextGear},
graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex}, graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex},
primitive::{GetJoints, MakePrimitiveShape, Primitive}, primitive::{GetCore, GetJoints, MakePrimitiveShape, Primitive},
rules::AccessRules, rules::AccessRules,
Drawing, Drawing,
}, },
@ -92,14 +93,14 @@ impl From<BinavnodeNodeIndex> for GearIndex {
/// The name "trianvertex" is a shortening of "triangulation vertex". /// The name "trianvertex" is a shortening of "triangulation vertex".
#[enum_dispatch(GetPetgraphIndex, MakePrimitive)] #[enum_dispatch(GetPetgraphIndex, MakePrimitive)]
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
enum TrianvertexNodeIndex { pub enum TrianvertexNodeIndex {
FixedDot(FixedDotIndex), FixedDot(FixedDotIndex),
FixedBend(FixedBendIndex), FixedBend(FixedBendIndex),
} }
impl From<TrianvertexNodeIndex> for BinavnodeNodeIndex { impl From<TrianvertexNodeIndex> for BinavnodeNodeIndex {
fn from(vertex: TrianvertexNodeIndex) -> Self { fn from(trianvertex: TrianvertexNodeIndex) -> Self {
match vertex { match trianvertex {
TrianvertexNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot), TrianvertexNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot),
TrianvertexNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend), TrianvertexNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend),
} }
@ -107,7 +108,7 @@ impl From<TrianvertexNodeIndex> for BinavnodeNodeIndex {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct TrianvertexWeight { pub struct TrianvertexWeight {
pub node: TrianvertexNodeIndex, pub node: TrianvertexNodeIndex,
pub pos: Point, pub pos: Point,
} }
@ -154,13 +155,21 @@ pub enum NavmeshError {
/// along-edge crossing. /// along-edge crossing.
/// ///
/// The name "navmesh" is a blend of "navigation mesh". /// The name "navmesh" is a blend of "navigation mesh".
#[derive(Debug, Clone)] #[derive(Clone, Getters)]
pub struct Navmesh { pub struct Navmesh {
graph: UnGraph<NavnodeWeight, (), usize>, graph: UnGraph<NavnodeWeight, (), usize>,
#[getter(skip)]
origin: FixedDotIndex, origin: FixedDotIndex,
#[getter(skip)]
origin_navnode: NavnodeIndex, origin_navnode: NavnodeIndex,
#[getter(skip)]
destination: FixedDotIndex, destination: FixedDotIndex,
#[getter(skip)]
destination_navnode: NavnodeIndex, destination_navnode: NavnodeIndex,
/// Original triangulation stored for debugging purposes.
// XXX: Maybe have a way to compile this out in release?
triangulation: Triangulation<TrianvertexNodeIndex, TrianvertexWeight, ()>,
} }
impl Navmesh { impl Navmesh {
@ -192,7 +201,7 @@ impl Navmesh {
pos: primitive.shape().center(), pos: primitive.shape().center(),
})?; })?;
} }
PrimitiveIndex::FixedSeg(seg) => { PrimitiveIndex::LoneLooseSeg(seg) => {
let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); let (from_dot, to_dot) = layout.drawing().primitive(seg).joints();
triangulation.add_constraint_edge( triangulation.add_constraint_edge(
@ -206,6 +215,32 @@ impl Navmesh {
}, },
)?; )?;
} }
PrimitiveIndex::SeqLooseSeg(seg) => {
let (from_joint, to_joint) = layout.drawing().primitive(seg).joints();
let from_dot = match from_joint {
DotIndex::Fixed(dot) => dot,
DotIndex::Loose(dot) => {
let bend = layout.drawing().primitive(dot).bend();
layout.drawing().primitive(bend).core()
}
};
let to_bend = layout.drawing().primitive(to_joint).bend();
let to_dot = layout.drawing().primitive(to_bend).core();
triangulation.add_constraint_edge(
TrianvertexWeight {
node: from_dot.into(),
pos: from_dot.primitive(layout.drawing()).shape().center(),
},
TrianvertexWeight {
node: to_dot.into(),
pos: to_dot.primitive(layout.drawing()).shape().center(),
},
)?;
}
PrimitiveIndex::FixedBend(bend) => { PrimitiveIndex::FixedBend(bend) => {
triangulation.add_vertex(TrianvertexWeight { triangulation.add_vertex(TrianvertexWeight {
node: bend.into(), node: bend.into(),
@ -218,6 +253,50 @@ impl Navmesh {
} }
} }
for node in layout.drawing().layer_primitive_nodes(layer) {
let primitive = node.primitive(layout.drawing());
if let Some(primitive_net) = primitive.maybe_net() {
if node == origin.into()
|| node == destination.into()
|| Some(primitive_net) != maybe_net
{
// If you have a band that was routed from a polygonal pad,
// upon another routing some of the constraint edges created
// from the loose segs band will intersect some of the
// constraint edges created from the fixed segs constituting
// the pad boundary.
//
// Such constraint intersections are erroneous and cause
// Spade to throw a panic at runtime. So, to prevent this
// from occuring, we iterate over the layout for the second
// time, after all the constraint edges from bands have
// been placed, and only then add constraint edges created
// from fixed segs, but only ones that do not cause an
// intersection.
match node {
PrimitiveIndex::FixedSeg(seg) => {
let (from_dot, to_dot) = layout.drawing().primitive(seg).joints();
let from_weight = TrianvertexWeight {
node: from_dot.into(),
pos: from_dot.primitive(layout.drawing()).shape().center(),
};
let to_weight = TrianvertexWeight {
node: to_dot.into(),
pos: to_dot.primitive(layout.drawing()).shape().center(),
};
if !triangulation.intersects_constraint(&from_weight, &to_weight) {
triangulation.add_constraint_edge(from_weight, to_weight)?;
}
}
_ => (),
}
}
}
}
Self::new_from_triangulation(layout, triangulation, origin, destination, options) Self::new_from_triangulation(layout, triangulation, origin, destination, options)
} }
@ -318,6 +397,7 @@ impl Navmesh {
origin_navnode: NavnodeIndex(origin_navnode.unwrap()), origin_navnode: NavnodeIndex(origin_navnode.unwrap()),
destination, destination,
destination_navnode: NavnodeIndex(destination_navnode.unwrap()), destination_navnode: NavnodeIndex(destination_navnode.unwrap()),
triangulation,
}) })
} }
@ -342,11 +422,6 @@ impl Navmesh {
.push((navnode1, navnode2)); .push((navnode1, navnode2));
} }
/// Returns the navmesh's underlying petgraph graph structure.
pub fn graph(&self) -> &UnGraph<NavnodeWeight, (), usize> {
&self.graph
}
/// Returns the origin node. /// Returns the origin node.
pub fn origin(&self) -> FixedDotIndex { pub fn origin(&self) -> FixedDotIndex {
self.origin self.origin

View File

@ -46,7 +46,31 @@ impl<I: GetPetgraphIndex, VW: GetTrianvertexNodeIndex<I> + HasPosition, EW: Defa
} }
pub fn add_constraint_edge(&mut self, from: VW, to: VW) -> Result<bool, InsertionError> { pub fn add_constraint_edge(&mut self, from: VW, to: VW) -> Result<bool, InsertionError> {
self.cdt.add_constraint_edge(from, to) let from_index = from.node_index().petgraph_index().index();
let to_index = to.node_index().petgraph_index().index();
// It is possible for one or both constraint edge endpoint vertices to
// not exist in the triangulation even after everything has been added.
// This can happen if the constraint was formed from a band wrapped
// over a polygonal pad that is the routing origin or destination, since
// in such situation the vertices of the pad boundary are not added to
// the triangulation.
//
// To prevent this from causing a panic at runtime, we idempotently add
// the constraint edge endpoint vertices to triangulation before adding
// the edge itself.
self.add_vertex(from)?;
self.add_vertex(to)?;
Ok(self.cdt.add_constraint(
self.trianvertex_to_handle[from_index].unwrap(),
self.trianvertex_to_handle[to_index].unwrap(),
))
}
pub fn intersects_constraint(&self, from: &VW, to: &VW) -> bool {
self.cdt
.intersects_constraint(from.position(), to.position())
} }
pub fn weight(&self, vertex: I) -> &VW { pub fn weight(&self, vertex: I) -> &VW {