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,
maybe_pin: Option<String>,
nodes: &[PrimitiveIndex],
fillets: &[FixedDotIndex],
) -> GenericIndex<PolyWeight> {
let (poly, apex) =
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 {
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> {
fn next_in_chain(&self, maybe_prev: Option<GearIndex>) -> Option<GearIndex> {
self.drawing
.clearance_intersectors(self.index.into())
.overlapees(self.index.into())
.find_map(|infringement| {
let PrimitiveIndex::FixedDot(intersectee) = infringement.1 else {
return None;

View File

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

View File

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

View File

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

View File

@ -254,7 +254,7 @@ impl Navmesh {
// The existence of a constraint edge does not (!) guarantee that this
// 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.
//
// 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>>,
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 {
continue;
};

View File

@ -19,8 +19,8 @@ use crate::{
Drawing,
},
geometry::{shape::AccessShape, GetLayer},
graph::GetPetgraphIndex,
layout::Layout,
graph::{GenericIndex, GetPetgraphIndex},
layout::{CompoundEntryLabel, Layout},
triangulation::{GetTrianvertexNodeIndex, Triangulation},
};
@ -153,13 +153,33 @@ impl Prenavmesh {
for node in layout.drawing().layer_primitive_nodes(layer) {
let primitive = node.primitive(layout.drawing());
if let Some(primitive_net) = primitive.maybe_net() {
let Some(primitive_net) = primitive.maybe_net() else {
continue;
};
if node == origin.into()
|| node == destination.into()
|| Some(primitive_net) != maybe_net
{
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;
}
this.triangulation
.add_vertex(PrenavmeshWeight::new_from_fixed_dot(layout, dot))?;
}
@ -181,12 +201,14 @@ impl Prenavmesh {
}
}
}
}
for node in layout.drawing().layer_primitive_nodes(layer) {
let primitive = node.primitive(layout.drawing());
if let Some(primitive_net) = primitive.maybe_net() {
let Some(primitive_net) = primitive.maybe_net() else {
continue;
};
if node == origin.into()
|| node == destination.into()
|| Some(primitive_net) != maybe_net
@ -205,6 +227,14 @@ impl Prenavmesh {
// 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
@ -218,11 +248,35 @@ impl Prenavmesh {
}
}
}
}
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> {
self.triangulation
.add_constraint_edge(constraint.0, constraint.1)?;

View File

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

View File

@ -39,7 +39,7 @@ fn test_tht_de9_to_tht_de9() {
#[test]
fn test_0603_breakout() {
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);
common::replay_and_assert(
&mut invoker,
@ -93,7 +93,7 @@ fn test_4x_3rd_order_smd_lc_filters() {
let mut autorouter = common::load_design(
"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);
common::replay_and_assert(
&mut invoker,
@ -130,7 +130,7 @@ fn test_tht_3pin_xlr_to_tht_3pin_xlr() {
fn test_vga_dac_breakout() {
let mut autorouter =
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);
common::replay_and_assert(
&mut invoker,