From 29dc59df040b9913da36fc2049559d321dc6aed6 Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Wed, 9 Jul 2025 14:35:32 +0200 Subject: [PATCH] refactor(router/navmesh): Split out navmesh triangulation into "prenavmesh" module --- committed.toml | 1 + crates/topola-egui/src/viewport.rs | 11 +- src/router/mod.rs | 1 + src/router/navmesh.rs | 265 ++++------------------------- src/router/prenavmesh.rs | 223 ++++++++++++++++++++++++ 5 files changed, 265 insertions(+), 236 deletions(-) create mode 100644 src/router/prenavmesh.rs diff --git a/committed.toml b/committed.toml index b76774a..5ec9641 100644 --- a/committed.toml +++ b/committed.toml @@ -73,6 +73,7 @@ allowed_scopes = [ "router/ng/floating", "router/ng/poly", "router/ng/router", + "router/prenavmesh", "router/route", "router/router", "router/thetastar", diff --git a/crates/topola-egui/src/viewport.rs b/crates/topola-egui/src/viewport.rs index e410c31..66f98dd 100644 --- a/crates/topola-egui/src/viewport.rs +++ b/crates/topola-egui/src/viewport.rs @@ -24,8 +24,9 @@ use topola::{ layout::poly::MakePolygon, math::{Circle, RotationSense}, router::{ - navmesh::{BinavnodeNodeIndex, NavmeshTriangulationConstraint, NavnodeIndex}, + navmesh::{BinavnodeNodeIndex, NavnodeIndex}, ng::pie, + prenavmesh::PrenavmeshConstraint, }, }; @@ -411,7 +412,9 @@ impl Viewport { if let Some(thetastar) = activity.maybe_thetastar() { let navmesh = thetastar.graph(); - for edge in navmesh.triangulation().edge_references() { + for edge in + navmesh.prenavmesh().triangulation().edge_references() + { let from = PrimitiveIndex::from(BinavnodeNodeIndex::from( edge.source(), )) @@ -443,8 +446,8 @@ impl Viewport { if let Some(thetastar) = activity.maybe_thetastar() { let navmesh = thetastar.graph(); - for NavmeshTriangulationConstraint(from_weight, to_weight) in - navmesh.constraints() + for PrenavmeshConstraint(from_weight, to_weight) in + navmesh.prenavmesh().constraints().iter() { let from = from_weight.pos + [100.0, 100.0].into(); let to = to_weight.pos + [100.0, 100.0].into(); diff --git a/src/router/mod.rs b/src/router/mod.rs index 2bb80b2..dbd1efc 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -7,6 +7,7 @@ pub mod navcord; pub mod navcorder; pub mod navmesh; pub mod ng; +pub mod prenavmesh; mod route; mod router; pub mod thetastar; diff --git a/src/router/navmesh.rs b/src/router/navmesh.rs index 85825f4..9101002 100644 --- a/src/router/navmesh.rs +++ b/src/router/navmesh.rs @@ -6,7 +6,6 @@ use std::collections::BTreeMap; use derive_getters::Getters; use enum_dispatch::enum_dispatch; -use geo::Point; use petgraph::{ data::DataMap, graph::UnGraph, @@ -16,29 +15,31 @@ use petgraph::{ IntoNodeIdentifiers, NodeIndexable, }, }; -use spade::{HasPosition, InsertionError, Point2}; +use spade::InsertionError; use thiserror::Error; use crate::{ drawing::{ bend::{FixedBendIndex, LooseBendIndex}, - dot::{DotIndex, FixedDotIndex}, + dot::FixedDotIndex, gear::{GearIndex, GetNextGear}, graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex}, - primitive::{GetCore, GetJoints, MakePrimitiveShape, Primitive}, + primitive::Primitive, rules::AccessRules, - seg::{FixedSegIndex, LoneLooseSegIndex, SegIndex, SeqLooseSegIndex}, Drawing, }, - geometry::{shape::AccessShape, GetLayer}, + geometry::GetLayer, graph::{GetPetgraphIndex, MakeRef}, layout::Layout, math::RotationSense, router::thetastar::MakeEdgeRef, - triangulation::{GetTrianvertexNodeIndex, Triangulation}, + triangulation::Triangulation, }; -use super::RouterOptions; +use super::{ + prenavmesh::{Prenavmesh, PrenavmeshConstraint, PrenavmeshNodeIndex, PrenavmeshWeight}, + RouterOptions, +}; #[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] pub struct NavnodeIndex(pub NodeIndex); @@ -66,6 +67,15 @@ pub enum BinavnodeNodeIndex { LooseBend(LooseBendIndex), } +impl From for BinavnodeNodeIndex { + fn from(trianvertex: PrenavmeshNodeIndex) -> Self { + match trianvertex { + PrenavmeshNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot), + PrenavmeshNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend), + } + } +} + impl From for PrimitiveIndex { fn from(vertex: BinavnodeNodeIndex) -> Self { match vertex { @@ -86,106 +96,6 @@ impl From for GearIndex { } } -/// Trianvertices are the vertices of the triangulation before it is converted -/// to the navmesh by multiplying each of them into more vertices (called -/// navnodes). Every trianvertex corresponds to one or more binavnodes on -/// the navmesh. -/// -/// The name "trianvertex" is a shortening of "triangulation vertex". -#[enum_dispatch(GetPetgraphIndex, MakePrimitive)] -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub enum TrianvertexNodeIndex { - FixedDot(FixedDotIndex), - FixedBend(FixedBendIndex), -} - -impl From for BinavnodeNodeIndex { - fn from(trianvertex: TrianvertexNodeIndex) -> Self { - match trianvertex { - TrianvertexNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot), - TrianvertexNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend), - } - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TrianvertexWeight { - pub node: TrianvertexNodeIndex, - pub pos: Point, -} - -impl TrianvertexWeight { - fn new_from_fixed_dot(layout: &Layout, dot: FixedDotIndex) -> Self { - Self { - node: dot.into(), - pos: dot.primitive(layout.drawing()).shape().center(), - } - } - - fn new_from_fixed_bend(layout: &Layout, bend: FixedBendIndex) -> Self { - Self { - node: bend.into(), - pos: bend.primitive(layout.drawing()).shape().center(), - } - } -} - -impl GetTrianvertexNodeIndex for TrianvertexWeight { - fn node_index(&self) -> TrianvertexNodeIndex { - self.node - } -} - -impl HasPosition for TrianvertexWeight { - type Scalar = f64; - fn position(&self) -> Point2 { - Point2::new(self.pos.x(), self.pos.y()) - } -} - -#[derive(Clone)] -pub struct NavmeshTriangulationConstraint(pub TrianvertexWeight, pub TrianvertexWeight); - -impl NavmeshTriangulationConstraint { - fn new_from_fixed_dot_pair( - layout: &Layout, - from_dot: FixedDotIndex, - to_dot: FixedDotIndex, - ) -> Self { - Self( - TrianvertexWeight::new_from_fixed_dot(layout, from_dot), - TrianvertexWeight::new_from_fixed_dot(layout, to_dot), - ) - } - - fn new_from_lone_loose_seg(layout: &Layout, seg: LoneLooseSegIndex) -> Self { - let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); - Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) - } - - fn new_from_seq_loose_seg(layout: &Layout, seg: SeqLooseSegIndex) -> Self { - 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(); - Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) - } - - fn new_from_fixed_seg(layout: &Layout, seg: FixedSegIndex) -> Self { - let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); - Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) - } -} - /// The terms "navnode" and "navmesh vertex", "navmesh node", "navigation /// vertex", "navigation node" are all equivalent. /// @@ -208,8 +118,6 @@ pub enum NavmeshError { Insertion(#[from] InsertionError), } -type NavmeshTriangulation = Triangulation; - /// The navmesh holds the entire Topola's search space represented as a graph. /// Topola's routing works by navigating over this graph with a pathfinding /// algorithm such as A* while drawing a track segment (always a cane except @@ -229,12 +137,8 @@ pub struct Navmesh { #[getter(skip)] destination_navnode: NavnodeIndex, - /// Original triangulation stored for debugging purposes. - // XXX: Maybe have a way to compile this out in release? - triangulation: NavmeshTriangulation, - // Original triangulation constraints stored for debugging purposes. - // XXX: Maybe have a way to compile this out in release? - constraints: Vec, + /// Original constrainted triangulation stored for debugging purposes. + prenavmesh: Prenavmesh, } impl Navmesh { @@ -245,117 +149,15 @@ impl Navmesh { destination: FixedDotIndex, options: RouterOptions, ) -> Result { - let mut triangulation: NavmeshTriangulation = - NavmeshTriangulation::new(layout.drawing().geometry().graph().node_bound()); - let mut constraints = vec![]; - - let layer = layout.drawing().primitive(origin).layer(); - let maybe_net = layout.drawing().primitive(origin).maybe_net(); - - 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 - { - match node { - PrimitiveIndex::FixedDot(dot) => { - triangulation - .add_vertex(TrianvertexWeight::new_from_fixed_dot(layout, dot))?; - } - PrimitiveIndex::LoneLooseSeg(seg) => { - Self::add_constraint( - &mut triangulation, - &mut constraints, - NavmeshTriangulationConstraint::new_from_lone_loose_seg( - layout, seg, - ), - )?; - } - PrimitiveIndex::SeqLooseSeg(seg) => { - Self::add_constraint( - &mut triangulation, - &mut constraints, - NavmeshTriangulationConstraint::new_from_seq_loose_seg(layout, seg), - )?; - } - PrimitiveIndex::FixedBend(bend) => { - triangulation - .add_vertex(TrianvertexWeight::new_from_fixed_bend(layout, bend))?; - } - _ => (), - } - } - } - } - - 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, - // when you will start a new routing some of the constraint - // edges created from the loose segs of a 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 that do not cause an intersection. - match node { - PrimitiveIndex::FixedSeg(seg) => { - let constraint = - NavmeshTriangulationConstraint::new_from_fixed_seg(layout, seg); - - if !triangulation.intersects_constraint(&constraint.0, &constraint.1) { - Self::add_constraint( - &mut triangulation, - &mut constraints, - constraint, - ); - } - } - _ => (), - } - } - } - } - - Self::new_from_triangulation( - layout, - triangulation, - origin, - destination, - constraints, - options, - ) + let prenavmesh = Prenavmesh::new(layout, origin, destination, options)?; + Self::new_from_prenavmesh(layout, prenavmesh, origin, destination, options) } - fn add_constraint( - triangulation: &mut NavmeshTriangulation, - constraints: &mut Vec, - constraint: NavmeshTriangulationConstraint, - ) -> Result<(), InsertionError> { - triangulation.add_constraint_edge(constraint.0, constraint.1)?; - constraints.push(constraint); - Ok(()) - } - - fn new_from_triangulation( + fn new_from_prenavmesh( layout: &Layout, - triangulation: NavmeshTriangulation, + prenavmesh: Prenavmesh, origin: FixedDotIndex, destination: FixedDotIndex, - constraints: Vec, options: RouterOptions, ) -> Result { let mut graph: UnGraph = UnGraph::default(); @@ -364,7 +166,7 @@ impl Navmesh { let mut map = BTreeMap::new(); - for trianvertex in triangulation.node_identifiers() { + for trianvertex in prenavmesh.triangulation().node_identifiers() { if trianvertex == origin.into() { let navnode = graph.add_node(NavnodeWeight { node: trianvertex.into(), @@ -431,7 +233,7 @@ impl Navmesh { } } - for edge in triangulation.edge_references() { + for edge in prenavmesh.triangulation().edge_references() { Self::add_trianedge_to_graph_as_quadrinavedge( &mut graph, &map, @@ -447,7 +249,7 @@ impl Navmesh { // // So now we go over all the constraints and make sure that // quadrinavedges exist for every one of them. - for constraint in constraints.iter() { + for constraint in prenavmesh.constraints() { Self::add_trianedge_to_graph_as_quadrinavedge( &mut graph, &map, @@ -462,15 +264,14 @@ impl Navmesh { origin_navnode: NavnodeIndex(origin_navnode.unwrap()), destination, destination_navnode: NavnodeIndex(destination_navnode.unwrap()), - triangulation, - constraints, + prenavmesh, }) } fn add_trianvertex_to_graph_and_map_as_binavnode( graph: &mut UnGraph, - map: &mut BTreeMap, NodeIndex)>>, - trianvertex: TrianvertexNodeIndex, + map: &mut BTreeMap, NodeIndex)>>, + trianvertex: PrenavmeshNodeIndex, node: BinavnodeNodeIndex, ) { let navnode1 = graph.add_node(NavnodeWeight { @@ -490,9 +291,9 @@ impl Navmesh { fn add_trianedge_to_graph_as_quadrinavedge( graph: &mut UnGraph, - map: &BTreeMap, NodeIndex)>>, - from_trianvertex: TrianvertexNodeIndex, - to_trianvertex: TrianvertexNodeIndex, + map: &BTreeMap, NodeIndex)>>, + from_trianvertex: PrenavmeshNodeIndex, + to_trianvertex: PrenavmeshNodeIndex, ) { for (from_navnode1, from_navnode2) in map[&from_trianvertex].iter() { for (to_navnode1, to_navnode2) in map[&to_trianvertex].iter() { diff --git a/src/router/prenavmesh.rs b/src/router/prenavmesh.rs new file mode 100644 index 0000000..c04e503 --- /dev/null +++ b/src/router/prenavmesh.rs @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2025 Topola contributors +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use derive_getters::Getters; +use enum_dispatch::enum_dispatch; +use geo::Point; +use petgraph::{stable_graph::NodeIndex, visit::NodeIndexable}; +use spade::{HasPosition, InsertionError, Point2}; + +use crate::{ + drawing::{ + bend::FixedBendIndex, + dot::{DotIndex, FixedDotIndex}, + graph::{GetMaybeNet, MakePrimitive, PrimitiveIndex}, + primitive::{GetCore, GetJoints, MakePrimitiveShape, Primitive}, + rules::AccessRules, + seg::{FixedSegIndex, LoneLooseSegIndex, SeqLooseSegIndex}, + Drawing, + }, + geometry::{shape::AccessShape, GetLayer}, + graph::GetPetgraphIndex, + layout::Layout, + triangulation::{GetTrianvertexNodeIndex, Triangulation}, +}; + +use super::{navmesh::NavmeshError, RouterOptions}; + +/// Prenavmesh nodes are the vertices of constrained Delaunay triangulation +/// before it is converted to the navmesh, which is done by multiplying each +/// of the prenavmesh nodes into more nodes, called navnodes. +#[enum_dispatch(GetPetgraphIndex, MakePrimitive)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum PrenavmeshNodeIndex { + FixedDot(FixedDotIndex), + FixedBend(FixedBendIndex), +} + +#[derive(Clone, Copy)] +pub struct PrenavmeshWeight { + pub node: PrenavmeshNodeIndex, + pub pos: Point, +} + +impl GetTrianvertexNodeIndex for PrenavmeshWeight { + fn node_index(&self) -> PrenavmeshNodeIndex { + self.node + } +} + +impl HasPosition for PrenavmeshWeight { + type Scalar = f64; + fn position(&self) -> Point2 { + Point2::new(self.pos.x(), self.pos.y()) + } +} + +impl PrenavmeshWeight { + pub fn new_from_fixed_dot(layout: &Layout, dot: FixedDotIndex) -> Self { + Self { + node: dot.into(), + pos: dot.primitive(layout.drawing()).shape().center(), + } + } + + pub fn new_from_fixed_bend(layout: &Layout, bend: FixedBendIndex) -> Self { + Self { + node: bend.into(), + pos: bend.primitive(layout.drawing()).shape().center(), + } + } +} + +#[derive(Clone, Copy)] +pub struct PrenavmeshConstraint(pub PrenavmeshWeight, pub PrenavmeshWeight); + +impl PrenavmeshConstraint { + pub fn new_from_fixed_dot_pair( + layout: &Layout, + from_dot: FixedDotIndex, + to_dot: FixedDotIndex, + ) -> Self { + Self( + PrenavmeshWeight::new_from_fixed_dot(layout, from_dot), + PrenavmeshWeight::new_from_fixed_dot(layout, to_dot), + ) + } + + pub fn new_from_lone_loose_seg( + layout: &Layout, + seg: LoneLooseSegIndex, + ) -> Self { + let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); + Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) + } + + pub fn new_from_seq_loose_seg( + layout: &Layout, + seg: SeqLooseSegIndex, + ) -> Self { + 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(); + Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) + } + + pub fn new_from_fixed_seg(layout: &Layout, seg: FixedSegIndex) -> Self { + let (from_dot, to_dot) = layout.drawing().primitive(seg).joints(); + Self::new_from_fixed_dot_pair(layout, from_dot, to_dot) + } +} + +#[derive(Clone, Getters)] +pub struct Prenavmesh { + triangulation: Triangulation, + constraints: Vec, +} + +impl Prenavmesh { + pub fn new( + layout: &Layout, + origin: FixedDotIndex, + destination: FixedDotIndex, + _options: RouterOptions, + ) -> Result { + let mut this = Self { + triangulation: Triangulation::new(layout.drawing().geometry().graph().node_bound()), + constraints: vec![], + }; + + let layer = layout.drawing().primitive(origin).layer(); + let maybe_net = layout.drawing().primitive(origin).maybe_net(); + + 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 + { + match node { + PrimitiveIndex::FixedDot(dot) => { + this.triangulation + .add_vertex(PrenavmeshWeight::new_from_fixed_dot(layout, dot))?; + } + PrimitiveIndex::LoneLooseSeg(seg) => { + this.add_constraint(PrenavmeshConstraint::new_from_lone_loose_seg( + layout, seg, + ))?; + } + PrimitiveIndex::SeqLooseSeg(seg) => { + this.add_constraint(PrenavmeshConstraint::new_from_seq_loose_seg( + layout, seg, + ))?; + } + PrimitiveIndex::FixedBend(bend) => { + this.triangulation + .add_vertex(PrenavmeshWeight::new_from_fixed_bend(layout, bend))?; + } + _ => (), + } + } + } + } + + 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, + // when you will start a new routing some of the constraint + // edges created from the loose segs of a 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 that do not cause an intersection. + match node { + PrimitiveIndex::FixedSeg(seg) => { + let constraint = PrenavmeshConstraint::new_from_fixed_seg(layout, seg); + + if !this + .triangulation + .intersects_constraint(&constraint.0, &constraint.1) + { + this.add_constraint(constraint); + } + } + _ => (), + } + } + } + } + + Ok(this) + } + + fn add_constraint(&mut self, constraint: PrenavmeshConstraint) -> Result<(), InsertionError> { + self.triangulation + .add_constraint_edge(constraint.0, constraint.1)?; + self.constraints.push(constraint); + Ok(()) + } +}