Keepout exits and rule areas
A route may not leave a cell inside a wire keepout as a wire, nor reach a pin there by one: it changes layer instead, and the via goes in the pad. A pin whose every stub enters a keepout is reached by a via only. A structure keepout named rule_area... (mckicad names KiCad rule areas so) is checked for overlap only, as KiCad's DRC does, with the free ends of each wire tested for their round caps.
This commit is contained in:
@@ -333,10 +333,20 @@ def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTran
|
||||
tiles=tiles,
|
||||
blocks_wires=keepout.kind != "via_keepout",
|
||||
blocks_vias=keepout.kind != "wire_keepout",
|
||||
clearance=not is_rule_area(keepout.area.name),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#: a structure keepout named with this prefix is a KiCad rule area (mckicad
|
||||
#: names them on export); it is checked for overlap only
|
||||
RULE_AREA_PREFIX = "rule_area"
|
||||
|
||||
|
||||
def is_rule_area(name: str | None) -> bool:
|
||||
return bool(name) and name.startswith(RULE_AREA_PREFIX)
|
||||
|
||||
|
||||
def _area_border_tiles(shape: Shape, transform: CoordinateTransform) -> list:
|
||||
"""Convex board tiles covering a keepout border shape.
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ class ObstacleArea(Item):
|
||||
#: tracks" rule area, where vias are allowed), via_keepout only vias
|
||||
blocks_wires: bool = True
|
||||
blocks_vias: bool = True
|
||||
#: False for a KiCad rule area: copper may not overlap it but may touch
|
||||
#: it, since KiCad's DRC applies no clearance to a rule area. Every other
|
||||
#: keepout may stand for copper (a net tie, a track kept as it is), so it
|
||||
#: keeps the clearance unless named as a rule area.
|
||||
clearance: bool = True
|
||||
|
||||
def is_obstacle(self) -> bool:
|
||||
return True
|
||||
|
||||
@@ -50,6 +50,8 @@ class TreeShape:
|
||||
#: which copper a keepout tile stops: "all", "wires" (a wire_keepout,
|
||||
#: vias allowed) or "vias" (a via_keepout)
|
||||
blocks: str = "all"
|
||||
#: False for a rule area: copper may touch it, only overlap counts
|
||||
clearance: bool = True
|
||||
|
||||
def on_layer(self, layer: int) -> bool:
|
||||
return layer in self.layers
|
||||
@@ -129,6 +131,8 @@ class ShapeSearchTree:
|
||||
|
||||
Uses a strict 2-D overlap of the ``clearance``-expanded box, so tiles
|
||||
exactly ``clearance`` apart (touching after expansion) are allowed.
|
||||
A rule area (``clearance=False``) only has to be clear of the copper
|
||||
itself.
|
||||
"""
|
||||
expanded = box.offset(clearance)
|
||||
for shape in self.overlapping(expanded):
|
||||
@@ -136,6 +140,10 @@ class ShapeSearchTree:
|
||||
continue
|
||||
if shape.blocks != "all" and shape.blocks != kind + "s":
|
||||
continue # a wire-only keepout does not stop a via, and so on
|
||||
if not shape.clearance:
|
||||
if shape.tile.overlaps(box):
|
||||
return True
|
||||
continue
|
||||
if shape.exact is not None:
|
||||
if overlaps_2d(shape.exact, expanded):
|
||||
return True
|
||||
@@ -143,6 +151,17 @@ class ShapeSearchTree:
|
||||
return True
|
||||
return False
|
||||
|
||||
def rule_area_overlap(self, box: IntBox, layer: int, kind: str = "wire") -> bool:
|
||||
"""True if ``box`` overlaps a rule area on ``layer`` that stops ``kind``."""
|
||||
for shape in self.overlapping(box):
|
||||
if shape.clearance or not shape.on_layer(layer):
|
||||
continue
|
||||
if shape.blocks != "all" and shape.blocks != kind + "s":
|
||||
continue
|
||||
if shape.tile.overlaps(box):
|
||||
return True
|
||||
return False
|
||||
|
||||
def clearance_conflict_shape(
|
||||
self, region, net_no: int, layer: int
|
||||
) -> bool:
|
||||
@@ -178,6 +197,13 @@ class ShapeSearchTree:
|
||||
continue # static input pair (e.g. pad vs pad) — not a routing DRC
|
||||
if not (shape.layers & other.layers):
|
||||
continue
|
||||
if not (shape.clearance and other.clearance):
|
||||
# a rule area: only an overlap is a violation
|
||||
mine = shape.exact if shape.exact is not None else shape.tile
|
||||
theirs = other.exact if other.exact is not None else other.tile
|
||||
if overlaps_2d(mine, theirs):
|
||||
return shape
|
||||
continue
|
||||
if shape.exact is None and other.exact is None:
|
||||
if other.tile.overlaps(expanded): # byte-identical box path
|
||||
return shape
|
||||
|
||||
@@ -42,6 +42,7 @@ from freeroute.geometry import (
|
||||
segment_octagon,
|
||||
shove_segment,
|
||||
)
|
||||
from freeroute.geometry.polyline import corner_box
|
||||
|
||||
from .grid_router import RouteResult, route_board
|
||||
|
||||
@@ -157,7 +158,10 @@ def route_board_exact(
|
||||
box = tile.bounding_box()
|
||||
if not box.is_empty():
|
||||
tree.insert(
|
||||
TreeShape(next(owner), -1, frozenset({obstacle.layer}), box, blocks=blocks)
|
||||
TreeShape(
|
||||
next(owner), -1, frozenset({obstacle.layer}), box,
|
||||
blocks=blocks, clearance=obstacle.clearance,
|
||||
)
|
||||
)
|
||||
|
||||
via_layers = frozenset(signal_layers)
|
||||
@@ -178,7 +182,9 @@ def route_board_exact(
|
||||
)
|
||||
for i, (box, box_layers) in enumerate(boxes)
|
||||
for layer in box_layers
|
||||
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
|
||||
) or _end_in_rule_area(tree, wires, half_width) or (
|
||||
outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)
|
||||
):
|
||||
rejected.append((net_no, *ends))
|
||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||
acc = accepted.get(net_no)
|
||||
@@ -592,6 +598,17 @@ def _net_boxes(result, net_no, half_width, via_layers, via_half=None):
|
||||
)
|
||||
|
||||
|
||||
def _end_in_rule_area(tree, wires, half_width) -> bool:
|
||||
"""A wire's tiles stop at its end points, but a track ends in a round cap
|
||||
half its width beyond them. Against other copper the pad or via there
|
||||
covers it; a rule area has no clearance to absorb it, so check it."""
|
||||
for layer, points in wires:
|
||||
for end in (points[0], points[-1]):
|
||||
if tree.rule_area_overlap(corner_box(end, half_width), layer):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
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]]] = []
|
||||
|
||||
@@ -33,6 +33,7 @@ import random
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
from freeroute.geometry.polyline import corner_box
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
|
||||
@@ -298,7 +299,9 @@ class GridRouter:
|
||||
outline = self.board.outline
|
||||
self._via_edge_block: set[Cell] = set()
|
||||
# wire keepouts as boxes, to check a pin stub the way the exact engine will
|
||||
self._keepout_boxes: dict[int, list[IntBox]] = {}
|
||||
self._keepout_boxes: dict[int, list[tuple[IntBox, bool]]] = {}
|
||||
# pin end nodes no wire can reach without entering a keepout
|
||||
self._via_only: set[Node] = set()
|
||||
if outline is not None:
|
||||
# cells too close to the real outline (rounded corners included):
|
||||
# for a trace on every layer, and separately for a via's wider copper
|
||||
@@ -336,8 +339,12 @@ class GridRouter:
|
||||
continue
|
||||
# the same distance the exact engine verifies against
|
||||
if item.blocks_wires:
|
||||
self._keepout_boxes.setdefault(item.layer, []).append(box)
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
self._keepout_boxes.setdefault(item.layer, []).append(
|
||||
(box, item.clearance)
|
||||
)
|
||||
# a rule area takes no clearance: a trace may touch it
|
||||
reach = margin if item.clearance else self.half_width
|
||||
for cell in self._cells_in_box(box, reach):
|
||||
if item.blocks_wires:
|
||||
self._keepout.add((*cell, item.layer))
|
||||
if item.blocks_vias:
|
||||
@@ -475,12 +482,19 @@ class GridRouter:
|
||||
return path
|
||||
cx, cy, cl = current
|
||||
base = g_score[current]
|
||||
for dx, dy in self._neighbours:
|
||||
# A cell inside a wire keepout (a pin there, or where a via
|
||||
# landed) may only be left by another via: a wire from it would
|
||||
# start inside the keepout.
|
||||
blocked = current in self._keepout or current in self._via_only
|
||||
neighbours = () if blocked else self._neighbours
|
||||
for dx, dy in neighbours:
|
||||
cell = (cx + dx, cy + dy)
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
continue
|
||||
nxt = (*cell, cl)
|
||||
if nxt in goal_stub: # a pin end; a zone end is entered normally
|
||||
if nxt in self._keepout or nxt in self._via_only:
|
||||
continue # a pin inside a keepout is reached by a via only
|
||||
enter = goal_stub[nxt]
|
||||
else:
|
||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||
@@ -528,7 +542,9 @@ class GridRouter:
|
||||
if _point_segment(self._cell_center(c), pin_location, end) < reach
|
||||
]
|
||||
|
||||
def _stub_clear(self, pin: Pin, cell: Cell, layer: int, net_no: int, conns) -> bool:
|
||||
def _stub_clear(
|
||||
self, pin: Pin, cell: Cell, layer: int, net_no: int, conns, keepouts_only: bool = False
|
||||
) -> bool:
|
||||
keepouts = self._keepout_boxes.get(layer)
|
||||
if keepouts:
|
||||
# the stub is off the grid, so check it exactly, as the exact
|
||||
@@ -539,8 +555,14 @@ class GridRouter:
|
||||
if end != pin.location:
|
||||
for tile in PolylineShape(Polyline([pin.location, end]), self.half_width).tiles():
|
||||
grown = tile.offset(self.clearance)
|
||||
if any(k.overlaps(grown) for k in keepouts):
|
||||
if any(k.overlaps(grown if needs else tile) for k, needs in keepouts):
|
||||
return False
|
||||
# the track's round end at the pin, for a rule area
|
||||
cap = corner_box(pin.location, self.half_width)
|
||||
if any(not needs and k.overlaps(cap) for k, needs in keepouts):
|
||||
return False
|
||||
if keepouts_only:
|
||||
return True
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
owner = self._cell_owner.get((*c, layer))
|
||||
if owner is not None and conns[owner].net_no != net_no:
|
||||
@@ -646,6 +668,13 @@ class GridRouter:
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy))
|
||||
if not nodes: # nothing clear: fall back to the nearest cell, as before
|
||||
nodes = {(cx, cy, layer) for layer in layers}
|
||||
if self._keepout_boxes and not any(
|
||||
self._stub_clear(pin, (cx, cy), layer, net_no, conns, keepouts_only=True)
|
||||
for layer in layers
|
||||
):
|
||||
# no wire can reach the pin without entering a keepout: the
|
||||
# route has to arrive by a via, which goes in the pad
|
||||
self._via_only.update(nodes)
|
||||
return nodes
|
||||
|
||||
def _crowded(self, pin: Pin, net_no: int) -> bool:
|
||||
@@ -800,7 +829,9 @@ class GridRouter:
|
||||
if len(wire_segs) > 1:
|
||||
for idx, pin in ((0, conn.a_end), (len(wire_segs) - 1, conn.b_end)):
|
||||
layer, cells = wire_segs[idx]
|
||||
if isinstance(pin, Pin) and len(cells) == 1 and (*cells[0], layer) in self._keepout:
|
||||
node = (*cells[0], layer)
|
||||
walled = node in self._keepout or node in self._via_only
|
||||
if isinstance(pin, Pin) and len(cells) == 1 and walled:
|
||||
in_pad[idx] = pin.location
|
||||
conn.wires = []
|
||||
for idx, (layer, cells) in enumerate(wire_segs):
|
||||
|
||||
Reference in New Issue
Block a user