From 245cd91c0ea81de75511aa9198e4cb16c87bb56c Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 28 Sep 2026 19:55:07 -0600 Subject: [PATCH] Don't cost a pin stub for a route that ends on a plane A plane end is marked with its zone, not a pin, so a route from a pad outside the plane's outline crashed looking for the pin's location (the flight-controller board's first placement). --- src/freeroute/route/grid_router.py | 5 +++-- tests/route/test_real_board_geometry.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index 797ea31..81bfdd9 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -451,10 +451,11 @@ class GridRouter: g_score: dict[Node, float] = {} came_from: dict[Node, Node] = {} # a stub costs its length, so the nearest cell wins unless it is blocked - goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if p is not None} + # a pin end costs its stub; a zone or plane end is entered normally + goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if isinstance(p, Pin)} for s in sorted(starts): pin = starts[s] - g_score[s] = self._stub_cost(pin, s) if pin is not None else 0.0 + g_score[s] = self._stub_cost(pin, s) if isinstance(pin, Pin) else 0.0 heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s)) # Via footprints are checked lazily: a layer change goes on the heap as diff --git a/tests/route/test_real_board_geometry.py b/tests/route/test_real_board_geometry.py index 5d208b2..bd519b7 100644 --- a/tests/route/test_real_board_geometry.py +++ b/tests/route/test_real_board_geometry.py @@ -202,6 +202,18 @@ def test_smd_pads_reach_an_inner_plane_by_their_own_via(): assert all(names[layer] in ("F.Cu", "B.Cu") for segs in result.result.wires.values() for layer, _ in segs) +def test_a_pad_outside_the_plane_routes_to_it(): + # G1 sits left of a plane that covers only the right half, so its route + # has to reach the plane's cells: those ends are the plane, not a pin + dsn_text = _four_layer_dsn().replace( + "(plane GND (polygon In1.Cu 0 500 -500 29500 -500 29500 -29500 500 -29500 500 -500))", + "(plane GND (polygon In1.Cu 0 15000 -500 29500 -500 29500 -29500 15000 -29500 15000 -500))", + ) + result, _scale, _names = route_dsn_board_exact(parse_dsn(dsn_text)) + gnd = next(i + 1 for i, n in enumerate(parse_dsn(dsn_text).nets) if n.name == "GND") + assert gnd in result.result.routed_net_numbers + + def test_a_through_hole_pad_inside_a_plane_needs_no_route(): from freeroute.route.grid_router import _connected_groups