refactor(router/navmesh): Split out navmesh triangulation into "prenavmesh" module

This commit is contained in:
Mikolaj Wielgus 2025-07-09 14:35:32 +02:00 committed by mikolaj
parent 9742740b9e
commit 29dc59df04
5 changed files with 265 additions and 236 deletions

View File

@ -73,6 +73,7 @@ allowed_scopes = [
"router/ng/floating",
"router/ng/poly",
"router/ng/router",
"router/prenavmesh",
"router/route",
"router/router",
"router/thetastar",

View File

@ -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();

View File

@ -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;

View File

@ -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<usize>);
@ -66,6 +67,15 @@ pub enum BinavnodeNodeIndex {
LooseBend(LooseBendIndex),
}
impl From<PrenavmeshNodeIndex> for BinavnodeNodeIndex {
fn from(trianvertex: PrenavmeshNodeIndex) -> Self {
match trianvertex {
PrenavmeshNodeIndex::FixedDot(dot) => BinavnodeNodeIndex::FixedDot(dot),
PrenavmeshNodeIndex::FixedBend(bend) => BinavnodeNodeIndex::FixedBend(bend),
}
}
}
impl From<BinavnodeNodeIndex> for PrimitiveIndex {
fn from(vertex: BinavnodeNodeIndex) -> Self {
match vertex {
@ -86,106 +96,6 @@ impl From<BinavnodeNodeIndex> 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<TrianvertexNodeIndex> 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<impl AccessRules>, dot: FixedDotIndex) -> Self {
Self {
node: dot.into(),
pos: dot.primitive(layout.drawing()).shape().center(),
}
}
fn new_from_fixed_bend(layout: &Layout<impl AccessRules>, bend: FixedBendIndex) -> Self {
Self {
node: bend.into(),
pos: bend.primitive(layout.drawing()).shape().center(),
}
}
}
impl GetTrianvertexNodeIndex<TrianvertexNodeIndex> for TrianvertexWeight {
fn node_index(&self) -> TrianvertexNodeIndex {
self.node
}
}
impl HasPosition for TrianvertexWeight {
type Scalar = f64;
fn position(&self) -> Point2<Self::Scalar> {
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<impl AccessRules>,
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<impl AccessRules>, 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<impl AccessRules>, 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<impl AccessRules>, 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<TrianvertexNodeIndex, TrianvertexWeight, ()>;
/// 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<NavmeshTriangulationConstraint>,
/// Original constrainted triangulation stored for debugging purposes.
prenavmesh: Prenavmesh,
}
impl Navmesh {
@ -245,117 +149,15 @@ impl Navmesh {
destination: FixedDotIndex,
options: RouterOptions,
) -> Result<Self, NavmeshError> {
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<NavmeshTriangulationConstraint>,
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<impl AccessRules>,
triangulation: NavmeshTriangulation,
prenavmesh: Prenavmesh,
origin: FixedDotIndex,
destination: FixedDotIndex,
constraints: Vec<NavmeshTriangulationConstraint>,
options: RouterOptions,
) -> Result<Self, NavmeshError> {
let mut graph: UnGraph<NavnodeWeight, (), usize> = 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<NavnodeWeight, (), usize>,
map: &mut BTreeMap<TrianvertexNodeIndex, Vec<(NodeIndex<usize>, NodeIndex<usize>)>>,
trianvertex: TrianvertexNodeIndex,
map: &mut BTreeMap<PrenavmeshNodeIndex, Vec<(NodeIndex<usize>, NodeIndex<usize>)>>,
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<NavnodeWeight, (), usize>,
map: &BTreeMap<TrianvertexNodeIndex, Vec<(NodeIndex<usize>, NodeIndex<usize>)>>,
from_trianvertex: TrianvertexNodeIndex,
to_trianvertex: TrianvertexNodeIndex,
map: &BTreeMap<PrenavmeshNodeIndex, Vec<(NodeIndex<usize>, NodeIndex<usize>)>>,
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() {

223
src/router/prenavmesh.rs Normal file
View File

@ -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<PrenavmeshNodeIndex> for PrenavmeshWeight {
fn node_index(&self) -> PrenavmeshNodeIndex {
self.node
}
}
impl HasPosition for PrenavmeshWeight {
type Scalar = f64;
fn position(&self) -> Point2<Self::Scalar> {
Point2::new(self.pos.x(), self.pos.y())
}
}
impl PrenavmeshWeight {
pub fn new_from_fixed_dot(layout: &Layout<impl AccessRules>, dot: FixedDotIndex) -> Self {
Self {
node: dot.into(),
pos: dot.primitive(layout.drawing()).shape().center(),
}
}
pub fn new_from_fixed_bend(layout: &Layout<impl AccessRules>, 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<impl AccessRules>,
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<impl AccessRules>,
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<impl AccessRules>,
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<impl AccessRules>, 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<PrenavmeshNodeIndex, PrenavmeshWeight, ()>,
constraints: Vec<PrenavmeshConstraint>,
}
impl Prenavmesh {
pub fn new(
layout: &Layout<impl AccessRules>,
origin: FixedDotIndex,
destination: FixedDotIndex,
_options: RouterOptions,
) -> Result<Self, NavmeshError> {
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(())
}
}