Say which connections were not routed

freeroute exited 0 and said nothing when it dropped connections, which
cost real diagnosis time: on rc4 two simple two-pin nets came back with no
copper and no explanation. The grid now records each connection it could
not route, the exact check adds each it rejects, and the CLI lists them on
stderr (the SES is still written; the exit code stays 0, as the JAR's).

A connection whose ends both sit in one zone of its net is listed apart,
as left to the zone fill: freeroute does not route through a zone it
cannot trust to be solid, but KiCad's fill often joins those pads anyway.
This commit is contained in:
2026-09-27 12:34:20 -06:00
parent c49c79604d
commit cdb3c7b568
6 changed files with 95 additions and 5 deletions
+46 -4
View File
@@ -59,7 +59,7 @@ class RouteResult:
``routed_net_numbers`` are the nets that got any wire or via.
"""
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections")
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections", "connection_ends", "failed")
def __init__(self, half_width: int) -> None:
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
@@ -69,6 +69,11 @@ class RouteResult:
self.unrouted = 0
#: the same copper per routed connection: (net_no, wires, vias)
self.connections: list[tuple[int, list[tuple[int, list[IntPoint]]], list[IntPoint]]] = []
#: what each routed connection joins, and the zone both ends sit in
#: (or ""), parallel to ``connections``
self.connection_ends: list[tuple[str, str, str]] = []
#: connections left unrouted: (net_no, one end, the other end)
self.failed: list[tuple[int, str, str]] = []
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
self.wires.setdefault(net_no, []).append((layer, path))
@@ -523,6 +528,16 @@ class GridRouter:
covered = sum((*cell, zone.layer) in self._keepout for cell in cells)
return covered >= coverage * len(cells)
def _left_to_zone(self, conn) -> str:
"""The zone both ends of an unrouted connection sit in, if any: its
fill may well join them, even when it is not trusted for routing."""
for zone in self._every_zone_by_net.get(conn.net_no, []):
if any(_pin_touches_zone(p, zone) for p in conn.a_group[0]) and any(
_pin_touches_zone(p, zone) for p in conn.b_group[0]
):
return zone.name or "zone"
return ""
def _group_ends(self, group, net_no: int, conns) -> dict[Node, Pin | None]:
"""End nodes of a group, each mapped to its pin (None for a zone cell)."""
ends: dict[Node, Pin | None] = {}
@@ -668,6 +683,7 @@ class GridRouter:
def route(self) -> RouteResult:
"""Route all ratsnest connections, using rip-up-and-retry across passes."""
conns = self._build_connections()
self._conns = conns
if not conns:
return RouteResult(self.half_width)
@@ -709,6 +725,10 @@ class GridRouter:
def _build_connections(self) -> list[_Connection]:
pins_by_net = self._pins_by_net()
zones_by_net: dict[int, list] = {}
self._every_zone_by_net: dict[int, list] = {}
for zone in self.board.get_conduction_areas():
if zone.layer in self.layers and zone.net_nos:
self._every_zone_by_net.setdefault(zone.net_nos[0], []).append(zone)
for zone in self.board.get_conduction_areas():
if zone.layer in self.layers and zone.net_nos and self._zone_is_solid(zone):
zones_by_net.setdefault(zone.net_nos[0], []).append(zone)
@@ -732,9 +752,10 @@ class GridRouter:
by_net.setdefault(net_no, []).append(pin)
return by_net
def _snapshot(self, conns: list[_Connection]) -> list[tuple[int, list, list]]:
def _snapshot(self, conns: list[_Connection]) -> list[tuple]:
return [
(c.net_no, [(layer, list(pts)) for layer, pts in c.wires], list(c.vias))
(c.net_no, [(layer, list(pts)) for layer, pts in c.wires], list(c.vias),
(*_ends_of(c), self._left_to_zone(c)))
for c in conns
if c.routed
]
@@ -742,12 +763,19 @@ class GridRouter:
def _result_from(self, snapshot, unrouted: int) -> RouteResult:
result = RouteResult(self.half_width)
result.unrouted = unrouted
for net_no, wires, vias in snapshot:
for net_no, wires, vias, ends in snapshot:
for layer, points in wires:
result.add_wire(net_no, layer, points)
for location in vias:
result.add_via(net_no, location)
result.connections.append((net_no, wires, vias))
result.connection_ends.append(ends)
routed = {ends[:2] + (net_no,) for net_no, _w, _v, ends in snapshot}
result.failed = [
(c.net_no, *_ends_of(c), self._left_to_zone(c))
for c in self._conns
if _ends_of(c) + (c.net_no,) not in routed
]
return result
@@ -755,6 +783,20 @@ def _first(items):
return items[0] if items else None
def _group_label(group) -> str:
pins, zones = group
parts = [f"{p.component_ref}-{p.name}" for p in pins[:3]]
if len(pins) > 3:
parts.append(f"+{len(pins) - 3} pins")
parts += [f"zone {z.name}" for z in zones[:1]]
return "/".join(parts) or "?"
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)
def _pin_touches_zone(pin: Pin, zone) -> bool:
"""A pad on the zone's layer whose centre or a corner lies in the zone."""
if zone.layer not in pin.layers or zone.polygon is None: