From d1def2c4b968206feeb7d559f1b42571e2e2b665 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 6 Oct 2026 23:07:09 -0600 Subject: [PATCH] Window boards: never search a connection ending outside the window With items away from the window left off its board, a stub from the window edge out to a rerouted net's far pin crossed copper the exact check never saw: 38 shorts on th-eda's flight controller. A connection with an end outside the bounds is now left unrouted, and a window whose new copper leaves the window is refused. --- src/freeroute/route/grid_router.py | 24 ++++++++++++++++++++++++ src/freeroute/route/window.py | 5 +++++ tests/route/test_window_reroute.py | 16 ++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index 5079cf9..8c27dbc 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -211,6 +211,10 @@ class GridRouter: # these nets get connections, and the grid covers only ``bounds`` # (cells outside it do not exist, so nothing is routed there). self.only_nets = frozenset(only_nets) if only_nets is not None else None + self._bounds = bounds + # connections with an end outside ``bounds``: never searched, since + # the grid has no cells to reach that end by + self._outside: set[int] = set() # Multi-pin nets: pair a net's pin groups by a minimum spanning tree, # and let a connection start or end on copper its net already routed # (a T into the tree), not only on one chosen pin. @@ -717,6 +721,8 @@ class GridRouter: def _search( self, conn: _Connection, allow_ripup: bool, conns: list[_Connection] ) -> list[Node] | None: + if conn.index in self._outside: + return None half = self._conn_half(conn) self._cur_half, self._spread = half, self._spread_for(half) try: @@ -1872,6 +1878,11 @@ class GridRouter: self._net_conns: dict[int, list[_Connection]] = {} for conn in conns: self._net_conns.setdefault(conn.net_no, []).append(conn) + if self._bounds is not None: + self._outside = { + c.index for c in conns + if not (_group_inside(c.a_group, self._bounds) and _group_inside(c.b_group, self._bounds)) + } return conns def _fixed_pieces(self, net_no: int) -> list[_FixedPiece]: @@ -2017,6 +2028,19 @@ def _group_label(group) -> str: return "/".join(parts) or "?" +def _group_inside(group, bounds: IntBox) -> bool: + """A group has a pin inside ``bounds``, or a zone reaching into it.""" + pins, zones = group + if any(bounds.ll.x <= p.location.x <= bounds.ur.x and bounds.ll.y <= p.location.y <= bounds.ur.y + for p in pins): + return True + for zone in zones: + box = zone.bounding_box() + if box.ll.x <= bounds.ur.x and bounds.ll.x <= box.ur.x and box.ll.y <= bounds.ur.y and bounds.ll.y <= box.ur.y: + return True + return False + + def _ends_of(conn) -> tuple[str, str]: """A readable description of what a connection joins.""" return _group_label(conn.a_group), _group_label(conn.b_group) diff --git a/src/freeroute/route/window.py b/src/freeroute/route/window.py index e61050d..7fb0b47 100644 --- a/src/freeroute/route/window.py +++ b/src/freeroute/route/window.py @@ -206,6 +206,11 @@ def window_reroute( after = _open(f for f in sub.failed if f[0] in affected) if len(after) >= len(before): continue + # the window board left out what lies away from it, so copper + # reaching out of the window was never checked against it + if any(not _inside((sub.wires.get(n, []), sub.vias.get(n, [])), window) + for n in affected if sub.wires.get(n) or sub.vias.get(n)): + continue # a net keeping some of its copper is written at one width: the # window's new copper has to have gone in at that width too if any( diff --git a/tests/route/test_window_reroute.py b/tests/route/test_window_reroute.py index 0a18f2d..9584535 100644 --- a/tests/route/test_window_reroute.py +++ b/tests/route/test_window_reroute.py @@ -110,3 +110,19 @@ def test_window_board_keeps_far_items_of_the_rerouted_nets_only(): refs = {p.component_ref for p in sub.get_pins()} assert {"P1", "P2"} <= refs # net N's pins, though outside the view assert not refs & {"Q1", "Q2"} # net M's, far away, left out + + +def test_a_connection_ending_outside_the_window_gets_no_copper(): + """Its far end has no grid cells; a stub out to it would cross items the + window board never saw.""" + from freeroute.route.exact_router import route_board_exact + + dsn = parse_dsn(_slot(far=True)) + s = dsn.resolution.value + window = IntBox(70000, -70000, 130000, -30000) # the slot only; net M's pins are far off + result = route_board_exact( + build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s), + clearance=round(_rule_clearance_dsn(dsn) * s), only_nets=frozenset({2}), bounds=window, + ).result + assert [f[0] for f in result.failed] == [2] + assert not result.wires.get(2) and not result.vias.get(2)