From f3245b96071de27dbef6b6f5b71cef29cac8b44b Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Fri, 4 Jul 2025 00:45:26 +0200 Subject: [PATCH] feat(router/navmesh): Add constraint edges for loose segs This does not work entirely correctly. I will investigate in subsequent commits. --- crates/topola-egui/src/actions.rs | 5 ++ crates/topola-egui/src/menu_bar.rs | 2 + crates/topola-egui/src/viewport.rs | 48 +++++++++++--- locales/en-US/main.ftl | 1 + src/router/navmesh.rs | 101 +++++++++++++++++++++++++---- src/triangulation.rs | 26 +++++++- 6 files changed, 161 insertions(+), 22 deletions(-) diff --git a/crates/topola-egui/src/actions.rs b/crates/topola-egui/src/actions.rs index 208453b..9000369 100644 --- a/crates/topola-egui/src/actions.rs +++ b/crates/topola-egui/src/actions.rs @@ -166,6 +166,7 @@ pub struct ViewActions { pub zoom_to_fit: Switch, pub show_ratsnest: Switch, pub show_navmesh: Switch, + pub show_triangulation: Switch, pub show_pathfinding_scores: Switch, pub show_topo_navmesh: 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(), 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_triangulation: Action::new_keyless(tr.text("tr-menu-view-show-triangulation")) + .into_switch(), show_pathfinding_scores: Action::new_keyless( tr.text("tr-menu-view-show-pathfinding-scores"), ) @@ -211,6 +214,8 @@ impl ViewActions { ui.add_enabled_ui(have_workspace, |ui| { self.show_ratsnest.checkbox(ui, &mut menu_bar.show_ratsnest); self.show_navmesh.checkbox(ui, &mut menu_bar.show_navmesh); + self.show_triangulation + .checkbox(ui, &mut menu_bar.show_triangulation); self.show_pathfinding_scores .checkbox(ui, &mut menu_bar.show_pathfinding_scores); self.show_topo_navmesh diff --git a/crates/topola-egui/src/menu_bar.rs b/crates/topola-egui/src/menu_bar.rs index d29ed3b..c50f3da 100644 --- a/crates/topola-egui/src/menu_bar.rs +++ b/crates/topola-egui/src/menu_bar.rs @@ -28,6 +28,7 @@ pub struct MenuBar { pub is_placing_via: bool, pub show_ratsnest: bool, pub show_navmesh: bool, + pub show_triangulation: bool, pub show_pathfinding_scores: bool, pub show_topo_navmesh: bool, pub show_bboxes: bool, @@ -50,6 +51,7 @@ impl MenuBar { is_placing_via: false, show_ratsnest: true, show_navmesh: false, + show_triangulation: false, show_pathfinding_scores: false, show_topo_navmesh: false, show_bboxes: false, diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index d6cb8be..d8a5989 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -23,7 +23,10 @@ use topola::{ }, layout::poly::MakePolygon, math::{Circle, RotationSense}, - router::{navmesh::NavnodeIndex, ng::pie}, + router::{ + navmesh::{BinavnodeNodeIndex, NavnodeIndex}, + ng::pie, + }, }; use crate::{ @@ -36,8 +39,6 @@ pub struct Viewport { /// how much should a single arrow key press scroll pub kbd_scroll_delta_factor: f32, pub scheduled_zoom_to_fit: bool, - - update_counter: f32, } impl Viewport { @@ -46,7 +47,6 @@ impl Viewport { transform: egui::emath::TSTransform::new([0.0, 0.0].into(), 0.01), kbd_scroll_delta_factor: 5.0, scheduled_zoom_to_fit: false, - update_counter: 0.0, } } @@ -271,8 +271,8 @@ impl Viewport { if menu_bar.show_navmesh { if let Some(activity) = workspace.interactor.maybe_activity() { - if let Some(astar) = activity.maybe_thetastar() { - let navmesh = astar.graph(); + if let Some(thetastar) = activity.maybe_thetastar() { + let navmesh = thetastar.graph(); for edge in navmesh.edge_references() { let mut from = PrimitiveIndex::from( @@ -376,13 +376,13 @@ impl Viewport { if menu_bar.show_pathfinding_scores { //TODO "{astar.scores[index]} ({astar.estimate_scores[index]}) (...)" - let score_text = astar + let score_text = thetastar .scores() .get(&navnode) .map_or_else(String::new, |s| { format!("g={:.2}", s) }); - let estimate_score_text = astar + let estimate_score_text = thetastar .estimate_scores() .get(&navnode) .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 let Some(navmesh) = workspace .interactor diff --git a/locales/en-US/main.ftl b/locales/en-US/main.ftl index 1f2fc7a..2a24a9b 100644 --- a/locales/en-US/main.ftl +++ b/locales/en-US/main.ftl @@ -22,6 +22,7 @@ tr-menu-view = View tr-menu-view-zoom-to-fit = Zoom to Fit tr-menu-view-show-ratsnest = Show Ratsnest 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-topo-navmesh = Show Topological Navmesh tr-menu-view-show-bboxes = Show BBoxes diff --git a/src/router/navmesh.rs b/src/router/navmesh.rs index 283a079..c3605c7 100644 --- a/src/router/navmesh.rs +++ b/src/router/navmesh.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; +use derive_getters::Getters; use enum_dispatch::enum_dispatch; use geo::Point; use petgraph::{ @@ -21,10 +22,10 @@ use thiserror::Error; use crate::{ drawing::{ bend::{FixedBendIndex, LooseBendIndex}, - dot::FixedDotIndex, + dot::{DotIndex, FixedDotIndex}, gear::{GearIndex, GetNextGear}, graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex}, - primitive::{GetJoints, MakePrimitiveShape, Primitive}, + primitive::{GetCore, GetJoints, MakePrimitiveShape, Primitive}, rules::AccessRules, Drawing, }, @@ -92,14 +93,14 @@ impl From for GearIndex { /// The name "trianvertex" is a shortening of "triangulation vertex". #[enum_dispatch(GetPetgraphIndex, MakePrimitive)] #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -enum TrianvertexNodeIndex { +pub enum TrianvertexNodeIndex { FixedDot(FixedDotIndex), FixedBend(FixedBendIndex), } impl From for BinavnodeNodeIndex { - fn from(vertex: TrianvertexNodeIndex) -> Self { - match vertex { + fn from(trianvertex: TrianvertexNodeIndex) -> Self { + match trianvertex { TrianvertexNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot), TrianvertexNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend), } @@ -107,7 +108,7 @@ impl From for BinavnodeNodeIndex { } #[derive(Debug, Clone)] -struct TrianvertexWeight { +pub struct TrianvertexWeight { pub node: TrianvertexNodeIndex, pub pos: Point, } @@ -154,13 +155,21 @@ pub enum NavmeshError { /// along-edge crossing. /// /// The name "navmesh" is a blend of "navigation mesh". -#[derive(Debug, Clone)] +#[derive(Clone, Getters)] pub struct Navmesh { graph: UnGraph, + #[getter(skip)] origin: FixedDotIndex, + #[getter(skip)] origin_navnode: NavnodeIndex, + #[getter(skip)] destination: FixedDotIndex, + #[getter(skip)] destination_navnode: NavnodeIndex, + + /// Original triangulation stored for debugging purposes. + // XXX: Maybe have a way to compile this out in release? + triangulation: Triangulation, } impl Navmesh { @@ -192,7 +201,7 @@ impl Navmesh { pos: primitive.shape().center(), })?; } - PrimitiveIndex::FixedSeg(seg) => { + PrimitiveIndex::LoneLooseSeg(seg) => { let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); 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) => { triangulation.add_vertex(TrianvertexWeight { 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) } @@ -318,6 +397,7 @@ impl Navmesh { origin_navnode: NavnodeIndex(origin_navnode.unwrap()), destination, destination_navnode: NavnodeIndex(destination_navnode.unwrap()), + triangulation, }) } @@ -342,11 +422,6 @@ impl Navmesh { .push((navnode1, navnode2)); } - /// Returns the navmesh's underlying petgraph graph structure. - pub fn graph(&self) -> &UnGraph { - &self.graph - } - /// Returns the origin node. pub fn origin(&self) -> FixedDotIndex { self.origin diff --git a/src/triangulation.rs b/src/triangulation.rs index c2c4cab..ed5a805 100644 --- a/src/triangulation.rs +++ b/src/triangulation.rs @@ -46,7 +46,31 @@ impl + HasPosition, EW: Defa } pub fn add_constraint_edge(&mut self, from: VW, to: VW) -> Result { - 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 {