Route to the real board outline, per connection, and out of fine-pitch pins

- The exact engine verified copper per net and dropped a whole net on any
  conflict: a 25-pin +3V3 net lost every connection to one bad segment.
  Verify and accept per ratsnest connection instead.
- Pad and keepout cells were rounded outward, blocking cells up to half a
  step clear of the copper; 62 of the 64 pins of a 0.5 mm pitch LQFP could
  not start a route. Block only cells whose centre is inside the clearance
  zone, and let a net walk along inside its own pad, which adds no copper.
- Routing was bounded by the outline's bounding box, so tracks cut across
  rounded corners. Keep the DSN boundary as a polygon; the grid blocks cells
  too close to it (scanline plus per-edge, precomputed for vias) and the
  exact and room engines reject copper that is.
- --edge-clearance MM sets copper-to-edge clearance, e.g. KiCad's
  min_copper_edge_clearance, which the DSN does not carry. Default 0.

On the TerraHawk rc4 power stage the exact engine now matches the
FreeRouting JAR: same open count, no DRC errors, with a 0.5 mm edge rule.
This commit is contained in:
2026-09-27 07:26:32 -06:00
parent 96c212466a
commit c1df284147
8 changed files with 303 additions and 32 deletions
+22 -7
View File
@@ -91,6 +91,7 @@ def route_board_exact(
max_shove_depth: int = 5,
shove_cap: int = 3,
via_half: int | None = None,
edge_clearance: int | None = None,
) -> ExactRouteResult:
"""Route ``board`` orthogonally and verify the result with exact geometry.
@@ -116,7 +117,10 @@ def route_board_exact(
rip_up=rip_up,
orthogonal=True,
via_half=via_half,
edge_clearance=edge_clearance,
)
edge = 0 if edge_clearance is None else edge_clearance
outline = board.outline
tree = ShapeSearchTree()
owner = _counter()
@@ -143,20 +147,25 @@ def route_board_exact(
via_layers = frozenset(signal_layers)
accepted: dict[int, _Accepted] = {}
for net_no in sorted(set(result.wires) | set(result.vias)):
boxes = _net_boxes(result, net_no, half_width, via_layers, via_half)
# Verify connection by connection, so one conflicting segment of a
# 25-pin supply net costs that one connection, not the whole net.
for net_no, wires, vias in sorted(result.connections, key=lambda c: c[0]):
boxes = _copper_boxes(wires, vias, half_width, via_layers, via_half)
if any(
tree.clearance_conflict(box, net_no, layer, clearance)
for box, box_layers in boxes
for layer in box_layers
):
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
continue # exact-clearance conflict -> drop (may be recovered by shove)
acc = _Accepted(wires=result.wires.get(net_no, []), vias=result.vias.get(net_no, []))
acc = accepted.get(net_no)
if acc is None:
acc = accepted[net_no] = _Accepted(wires=[], vias=[])
acc.wires.extend(wires)
acc.vias.extend(vias)
for box, box_layers in boxes:
oid = next(owner)
tree.insert(TreeShape(oid, net_no, box_layers, box, routed=True))
acc.owners.append(oid)
accepted[net_no] = acc
# a net is dropped if it has a real ratsnest (>= 2 pins on a signal layer)
# but was not accepted — whether the grid failed to route it or its exact
@@ -547,12 +556,18 @@ def _rollback(shoved, accepted, tree, owner) -> int: # noqa: ARG001
def _net_boxes(result, net_no, half_width, via_layers, via_half=None):
return _copper_boxes(
result.wires.get(net_no, []), result.vias.get(net_no, []), half_width, via_layers, via_half
)
def _copper_boxes(wires, vias, half_width, via_layers, via_half=None):
via_half = via_half or half_width
boxes: list[tuple[IntBox, frozenset[int]]] = []
for layer, points in result.wires.get(net_no, []):
for layer, points in wires:
for box in PolylineShape(Polyline(points), half_width).tiles():
boxes.append((box, frozenset({layer})))
for loc in result.vias.get(net_no, []):
for loc in vias:
vbox = IntBox(loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half)
boxes.append((vbox, via_layers))
return boxes