feat(router/prenavmesh): Have fillets as prenavnodes instead of long vertex chains

This commit is contained in:
Mikolaj Wielgus 2025-08-31 14:18:31 +02:00
parent 521bb0598a
commit 3e466960fa
9 changed files with 156 additions and 89 deletions

View File

@ -158,10 +158,11 @@ impl<M: AccessMesadata> Board<M> {
weight: PolyWeight, weight: PolyWeight,
maybe_pin: Option<String>, maybe_pin: Option<String>,
nodes: &[PrimitiveIndex], nodes: &[PrimitiveIndex],
fillets: &[FixedDotIndex],
) -> GenericIndex<PolyWeight> { ) -> GenericIndex<PolyWeight> {
let (poly, apex) = let (poly, apex) =
self.layout self.layout
.add_poly_with_nodes(&mut recorder.layout_edit, weight, nodes); .add_poly_with_nodes(&mut recorder.layout_edit, weight, nodes, fillets);
if let Some(pin) = maybe_pin { if let Some(pin) = maybe_pin {
for i in nodes { for i in nodes {

View File

@ -295,7 +295,7 @@ impl<CW, Cel, R> GetOuterGears for FixedDot<'_, CW, Cel, R> {
impl<CW: Clone, Cel: Copy, R: AccessRules> GetPrevNextInChain for FixedDot<'_, CW, Cel, R> { impl<CW: Clone, Cel: Copy, R: AccessRules> GetPrevNextInChain for FixedDot<'_, CW, Cel, R> {
fn next_in_chain(&self, maybe_prev: Option<GearIndex>) -> Option<GearIndex> { fn next_in_chain(&self, maybe_prev: Option<GearIndex>) -> Option<GearIndex> {
self.drawing self.drawing
.clearance_intersectors(self.index.into()) .overlapees(self.index.into())
.find_map(|infringement| { .find_map(|infringement| {
let PrimitiveIndex::FixedDot(intersectee) = infringement.1 else { let PrimitiveIndex::FixedDot(intersectee) = infringement.1 else {
return None; return None;

View File

@ -151,7 +151,7 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
infringer: PrimitiveIndex, infringer: PrimitiveIndex,
it: impl Iterator<Item = PrimitiveIndex> + 'a, it: impl Iterator<Item = PrimitiveIndex> + 'a,
) -> impl Iterator<Item = Infringement> + 'a { ) -> impl Iterator<Item = Infringement> + 'a {
self.clearance_intersectors_among(infringer, it) self.overlapees_among(infringer, it)
.filter(move |infringement| { .filter(move |infringement| {
// Infringement with loose dots resulted in false positives for // Infringement with loose dots resulted in false positives for
// line-of-sight paths. // line-of-sight paths.
@ -161,24 +161,25 @@ impl<CW: Clone, Cel: Copy, R: AccessRules> Drawing<CW, Cel, R> {
.filter(move |infringement| !self.are_connectable(infringer, infringement.1)) .filter(move |infringement| !self.are_connectable(infringer, infringement.1))
} }
pub fn clearance_intersectors<'a>( pub fn overlapees<'a>(
&'a self, &'a self,
intersector: PrimitiveIndex, overlapper: PrimitiveIndex,
) -> impl Iterator<Item = Infringement> + 'a { ) -> impl Iterator<Item = Infringement> + 'a {
self.clearance_intersectors_among( self.overlapees_among(
intersector, overlapper,
self.locate_possible_infringees(intersector) self.locate_possible_infringees(overlapper)
.filter_map(move |infringee_node| { .filter_map(move |infringee_node| {
if let GenericNode::Primitive(primitive_node) = infringee_node { if let GenericNode::Primitive(primitive_node) = infringee_node {
Some(primitive_node) Some(primitive_node)
} else { } else {
None None
} }
}), })
.filter(move |&overlapee| overlapper != overlapee),
) )
} }
pub(super) fn clearance_intersectors_among<'a>( pub(super) fn overlapees_among<'a>(
&'a self, &'a self,
intersector: PrimitiveIndex, intersector: PrimitiveIndex,
it: impl Iterator<Item = PrimitiveIndex> + 'a, it: impl Iterator<Item = PrimitiveIndex> + 'a,

View File

@ -53,7 +53,8 @@ pub enum CompoundWeight {
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompoundEntryLabel { pub enum CompoundEntryLabel {
Normal, Normal,
NotInConvexHull, Apex,
Fillet,
} }
/// The alias to differ node types /// The alias to differ node types
@ -254,13 +255,14 @@ impl<R: AccessRules> Layout<R> {
recorder: &mut LayoutEdit, recorder: &mut LayoutEdit,
weight: PolyWeight, weight: PolyWeight,
nodes: &[PrimitiveIndex], nodes: &[PrimitiveIndex],
fillets: &[FixedDotIndex],
) -> (GenericIndex<PolyWeight>, FixedDotIndex) { ) -> (GenericIndex<PolyWeight>, FixedDotIndex) {
let layer = weight.layer(); let layer = weight.layer();
let maybe_net = weight.maybe_net(); let maybe_net = weight.maybe_net();
let poly = self.add_poly(recorder, weight); let poly = self.add_poly(recorder, weight);
( (
poly, poly,
add_poly_with_nodes_intern(self, recorder, poly, nodes, layer, maybe_net), add_poly_with_nodes_intern(self, recorder, poly, nodes, fillets, layer, maybe_net),
) )
} }
@ -361,6 +363,7 @@ impl<R: AccessRules> Layout<R> {
if self if self
.drawing() .drawing()
.geometry() .geometry()
// TODO: Add `.compounds()` method working on `PrimitiveIndex`.
.compounds(GenericIndex::<()>::new(primitive.petgraph_index())) .compounds(GenericIndex::<()>::new(primitive.petgraph_index()))
.next() .next()
.is_some() .is_some()
@ -380,7 +383,7 @@ impl<R: AccessRules> Layout<R> {
let apex = loop { let apex = loop {
// this returns None if the via is not present on this layer // this returns None if the via is not present on this layer
let (entry_label, dot) = dots.next()?; let (entry_label, dot) = dots.next()?;
if entry_label == CompoundEntryLabel::NotInConvexHull { if entry_label == CompoundEntryLabel::Apex {
if let Some((dot, weight)) = handle_fixed_dot(&self.drawing, dot) { if let Some((dot, weight)) = handle_fixed_dot(&self.drawing, dot) {
if weight.layer() == active_layer { if weight.layer() == active_layer {
break dot; break dot;

View File

@ -49,6 +49,7 @@ pub(super) fn add_poly_with_nodes_intern<R: AccessRules>(
recorder: &mut LayoutEdit, recorder: &mut LayoutEdit,
poly: GenericIndex<PolyWeight>, poly: GenericIndex<PolyWeight>,
nodes: &[PrimitiveIndex], nodes: &[PrimitiveIndex],
fillets: &[FixedDotIndex],
layer: usize, layer: usize,
maybe_net: Option<usize>, maybe_net: Option<usize>,
) -> FixedDotIndex { ) -> FixedDotIndex {
@ -123,19 +124,22 @@ pub(super) fn add_poly_with_nodes_intern<R: AccessRules>(
layout.drawing.add_to_compound( layout.drawing.add_to_compound(
recorder, recorder,
GenericIndex::<()>::new(idx.petgraph_index()), GenericIndex::<()>::new(idx.petgraph_index()),
CompoundEntryLabel::NotInConvexHull, CompoundEntryLabel::Apex,
poly_compound, poly_compound,
); );
} }
} }
for fillet in fillets {
layout
.drawing
.add_to_compound(recorder, *fillet, CompoundEntryLabel::Fillet, poly_compound)
}
// maybe this should be a different edge label // maybe this should be a different edge label
layout.drawing.add_to_compound( layout
recorder, .drawing
apex, .add_to_compound(recorder, apex, CompoundEntryLabel::Apex, poly_compound);
CompoundEntryLabel::NotInConvexHull,
poly_compound,
);
assert!(is_apex(&layout.drawing, apex)); assert!(is_apex(&layout.drawing, apex));
apex apex
@ -166,7 +170,7 @@ impl<'a, R> PolyRef<'a, R> {
.geometry() .geometry()
.compound_members(self.index.into()) .compound_members(self.index.into())
.find_map(|(label, primitive_node)| { .find_map(|(label, primitive_node)| {
if label == CompoundEntryLabel::NotInConvexHull { if label == CompoundEntryLabel::Apex {
if let PrimitiveIndex::FixedDot(dot) = primitive_node { if let PrimitiveIndex::FixedDot(dot) = primitive_node {
if is_apex(self.drawing, dot) { if is_apex(self.drawing, dot) {
return Some(dot); return Some(dot);

View File

@ -254,7 +254,7 @@ impl Navmesh {
// The existence of a constraint edge does not (!) guarantee that this // The existence of a constraint edge does not (!) guarantee that this
// edge exactly will be present in the triangulation. It appears that // edge exactly will be present in the triangulation. It appears that
// Spade splits a constraint edge into two if an endpoint of another // Spade splits a constraint edge in two if an endpoint of another
// constraint lies on it. // constraint lies on it.
// //
// So now we go over all the constraints and make sure that // So now we go over all the constraints and make sure that
@ -324,7 +324,7 @@ impl Navmesh {
overlapping_prenavnodes_unions: &mut UnionFind<NodeIndex<usize>>, overlapping_prenavnodes_unions: &mut UnionFind<NodeIndex<usize>>,
prenavnode: PrenavmeshNodeIndex, prenavnode: PrenavmeshNodeIndex,
) { ) {
for overlap in layout.drawing().clearance_intersectors(prenavnode.into()) { for overlap in layout.drawing().overlapees(prenavnode.into()) {
let PrimitiveIndex::FixedDot(overlapee) = overlap.1 else { let PrimitiveIndex::FixedDot(overlapee) = overlap.1 else {
continue; continue;
}; };

View File

@ -19,8 +19,8 @@ use crate::{
Drawing, Drawing,
}, },
geometry::{shape::AccessShape, GetLayer}, geometry::{shape::AccessShape, GetLayer},
graph::GetPetgraphIndex, graph::{GenericIndex, GetPetgraphIndex},
layout::Layout, layout::{CompoundEntryLabel, Layout},
triangulation::{GetTrianvertexNodeIndex, Triangulation}, triangulation::{GetTrianvertexNodeIndex, Triangulation},
}; };
@ -153,32 +153,51 @@ impl Prenavmesh {
for node in layout.drawing().layer_primitive_nodes(layer) { for node in layout.drawing().layer_primitive_nodes(layer) {
let primitive = node.primitive(layout.drawing()); let primitive = node.primitive(layout.drawing());
if let Some(primitive_net) = primitive.maybe_net() { let Some(primitive_net) = primitive.maybe_net() else {
if node == origin.into() continue;
|| node == destination.into() };
|| Some(primitive_net) != maybe_net
{ if node == origin.into()
match node { || node == destination.into()
PrimitiveIndex::FixedDot(dot) => { || Some(primitive_net) != maybe_net
this.triangulation {
.add_vertex(PrenavmeshWeight::new_from_fixed_dot(layout, dot))?; match node {
PrimitiveIndex::FixedDot(dot) => {
layout
.drawing()
// TODO: Add `.compounds()` method working on `PrimitiveIndex`.
.compounds(GenericIndex::<()>::new(dot.petgraph_index()))
.find(|(label, _)| *label == CompoundEntryLabel::Fillet)
.is_some();
// Do not add prenavnodes for primitives that have been filleted.
// For now, we do this by detecting if the primitive overlaps
// a fillet.
// TODO: This method is simplistic and will obviously result in
// false positives in some cases, so in the future, instead of this,
// create a fillet compound type and check for compound membership.
if Self::is_fixed_dot_filleted(layout, dot) {
continue;
} }
PrimitiveIndex::LoneLooseSeg(seg) => {
this.add_constraint(PrenavmeshConstraint::new_from_lone_loose_seg( this.triangulation
layout, seg, .add_vertex(PrenavmeshWeight::new_from_fixed_dot(layout, dot))?;
))?;
}
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))?;
}
_ => (),
} }
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))?;
}
_ => (),
} }
} }
} }
@ -186,36 +205,46 @@ impl Prenavmesh {
for node in layout.drawing().layer_primitive_nodes(layer) { for node in layout.drawing().layer_primitive_nodes(layer) {
let primitive = node.primitive(layout.drawing()); let primitive = node.primitive(layout.drawing());
if let Some(primitive_net) = primitive.maybe_net() { let Some(primitive_net) = primitive.maybe_net() else {
if node == origin.into() continue;
|| 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 if node == origin.into()
.triangulation || node == destination.into()
.intersects_constraint(&constraint.0, &constraint.1) || Some(primitive_net) != maybe_net
{ {
this.add_constraint(constraint); // 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 (from_dot, to_dot) = layout.drawing().primitive(seg).joints();
if Self::is_fixed_dot_filleted(layout, from_dot)
&& Self::is_fixed_dot_filleted(layout, to_dot)
{
continue;
}
let constraint = PrenavmeshConstraint::new_from_fixed_seg(layout, seg);
if !this
.triangulation
.intersects_constraint(&constraint.0, &constraint.1)
{
this.add_constraint(constraint);
} }
_ => (),
} }
_ => (),
} }
} }
} }
@ -223,6 +252,31 @@ impl Prenavmesh {
Ok(this) Ok(this)
} }
fn is_fixed_dot_filleted(layout: &Layout<impl AccessRules>, dot: FixedDotIndex) -> bool {
layout
.drawing()
.compounds(GenericIndex::<()>::new(dot.petgraph_index()))
.find(|(label, _)|
// Fillets fail this test for some reason that I did not investigate, so
// I added this condition.
*label == CompoundEntryLabel::Fillet
// Exclude apices because they may overlap fillets.
|| *label == CompoundEntryLabel::Apex)
.is_none()
&& layout
.drawing()
.overlapees(dot.into())
.find(|overlapee| {
layout
.drawing()
// TODO: Add `.compounds()` method working on `PrimitiveIndex`.
.compounds(GenericIndex::<()>::new(overlapee.1.petgraph_index()))
.find(|(label, _)| *label == CompoundEntryLabel::Fillet)
.is_some()
})
.is_some()
}
fn add_constraint(&mut self, constraint: PrenavmeshConstraint) -> Result<(), InsertionError> { fn add_constraint(&mut self, constraint: PrenavmeshConstraint) -> Result<(), InsertionError> {
self.triangulation self.triangulation
.add_constraint_edge(constraint.0, constraint.1)?; .add_constraint_edge(constraint.0, constraint.1)?;

View File

@ -15,7 +15,7 @@ use specctra_core::math::PointWithRotation;
use crate::{ use crate::{
board::{edit::BoardEdit, AccessMesadata, Board}, board::{edit::BoardEdit, AccessMesadata, Board},
drawing::{ drawing::{
dot::{FixedDotWeight, GeneralDotWeight}, dot::{FixedDotIndex, FixedDotWeight, GeneralDotWeight},
graph::{GetMaybeNet, MakePrimitive}, graph::{GetMaybeNet, MakePrimitive},
primitive::MakePrimitiveShape, primitive::MakePrimitiveShape,
seg::{FixedSegWeight, GeneralSegWeight}, seg::{FixedSegWeight, GeneralSegWeight},
@ -584,6 +584,7 @@ impl SpecctraDesign {
seg3.into(), seg3.into(),
seg4.into(), seg4.into(),
], ],
&[],
); );
} }
@ -728,15 +729,16 @@ impl SpecctraDesign {
.into(), .into(),
); );
let fillets = Self::add_polygon_fillet_circles(
recorder, board, place, pin, coords, width, layer, maybe_net, None, flip,
);
board.add_poly_with_nodes( board.add_poly_with_nodes(
recorder, recorder,
SolidPolyWeight { layer, maybe_net }.into(), SolidPolyWeight { layer, maybe_net }.into(),
maybe_pin, maybe_pin,
&nodes[..], &nodes[..],
); &fillets[..],
Self::add_polygon_fillet_circles(
recorder, board, place, pin, coords, width, layer, maybe_net, None, flip,
); );
} }
@ -751,9 +753,10 @@ impl SpecctraDesign {
maybe_net: Option<usize>, maybe_net: Option<usize>,
_maybe_pin: Option<String>, _maybe_pin: Option<String>,
flip: bool, flip: bool,
) { ) -> Vec<FixedDotIndex> {
let MIN_FIRST_CHAIN_ELEMENT_LENGTH = 100.0; let MIN_FIRST_CHAIN_ELEMENT_LENGTH = 100.0;
let mut maybe_first_chain_segment = None; let mut maybe_first_chain_segment = None;
let mut fillets = vec![];
let first_pos = Self::pos(place, pin, coords[0].x, coords[0].y, flip); let first_pos = Self::pos(place, pin, coords[0].x, coords[0].y, flip);
let last_pos = Self::pos( let last_pos = Self::pos(
@ -790,22 +793,23 @@ impl SpecctraDesign {
if index - first_chain_index >= 3 { if index - first_chain_index >= 3 {
let circle = math::fillet_circle(&first_chain_segment, &curr_segment12); let circle = math::fillet_circle(&first_chain_segment, &curr_segment12);
board.add_fixed_dot_infringably( fillets.push(board.add_fixed_dot_infringably(
recorder, recorder,
FixedDotWeight(GeneralDotWeight { FixedDotWeight(GeneralDotWeight {
circle, circle,
layer, layer,
maybe_net: None, // TODO. maybe_net,
//maybe_net,
}), }),
None, None,
); ));
} }
} }
maybe_first_chain_segment = Some((Line::new(curr_pos1, curr_pos2), index)); maybe_first_chain_segment = Some((Line::new(curr_pos1, curr_pos2), index));
} }
} }
fillets
} }
fn pos(place: PointWithRotation, pin: PointWithRotation, x: f64, y: f64, flip: bool) -> Point { fn pos(place: PointWithRotation, pin: PointWithRotation, x: f64, y: f64, flip: bool) -> Point {

View File

@ -39,7 +39,7 @@ fn test_tht_de9_to_tht_de9() {
#[test] #[test]
fn test_0603_breakout() { fn test_0603_breakout() {
let mut autorouter = common::load_design("tests/single_layer/0603_breakout/0603_breakout.dsn"); let mut autorouter = common::load_design("tests/single_layer/0603_breakout/0603_breakout.dsn");
common::assert_navnode_count(&mut autorouter, "R1-2", "J1-2", 54); common::assert_navnode_count(&mut autorouter, "R1-2", "J1-2", 22);
let mut invoker = common::create_invoker_and_assert(autorouter); let mut invoker = common::create_invoker_and_assert(autorouter);
common::replay_and_assert( common::replay_and_assert(
&mut invoker, &mut invoker,
@ -93,7 +93,7 @@ fn test_4x_3rd_order_smd_lc_filters() {
let mut autorouter = common::load_design( let mut autorouter = common::load_design(
"tests/single_layer/4x_3rd_order_smd_lc_filters/4x_3rd_order_smd_lc_filters.dsn", "tests/single_layer/4x_3rd_order_smd_lc_filters/4x_3rd_order_smd_lc_filters.dsn",
); );
common::assert_navnode_count(&mut autorouter, "J1-1", "L1-1", 2062); common::assert_navnode_count(&mut autorouter, "J1-1", "L1-1", 558);
let mut invoker = common::create_invoker_and_assert(autorouter); let mut invoker = common::create_invoker_and_assert(autorouter);
common::replay_and_assert( common::replay_and_assert(
&mut invoker, &mut invoker,
@ -130,7 +130,7 @@ fn test_tht_3pin_xlr_to_tht_3pin_xlr() {
fn test_vga_dac_breakout() { fn test_vga_dac_breakout() {
let mut autorouter = let mut autorouter =
common::load_design("tests/single_layer/vga_dac_breakout/vga_dac_breakout.dsn"); common::load_design("tests/single_layer/vga_dac_breakout/vga_dac_breakout.dsn");
common::assert_navnode_count(&mut autorouter, "J1-2", "R4-1", 944); common::assert_navnode_count(&mut autorouter, "J1-2", "R4-1", 272);
let mut invoker = common::create_invoker_and_assert(autorouter); let mut invoker = common::create_invoker_and_assert(autorouter);
common::replay_and_assert( common::replay_and_assert(
&mut invoker, &mut invoker,