Model pads and vias as KiCad places them
Measured against KiCad's own pads on the TerraHawk rc4 power stage: - Back-side parts kept their pads on F.Cu (32 pads). Mirror the copper layer for a back-side part. - Pad outlines were copied unrotated (29 pads on rotated parts). Place the outline through the same chain as the pin: pin rotation, back-side mirror, part rotation. Oval pads, a path with a width, came out as a zero-height sliver; pad them out by the aperture. - Multi-shape padstacks used the first shape; use the largest. - Footprint-level keepouts were parsed but never placed. KiCad exports a mounting hole's plated ring that way, so the router could not see it. - Vias were sized like a trace (0.25 mm square for a 0.6 mm via), and every remaining short and clearance error was a via. Read the size from the DSN's via padstack and use it in all three engines; the grid engine now reserves the cells a via's copper reaches. With these the exact and room engines route that board with no shorts, clearance or other DRC errors. Pins now carry their component reference.
This commit is contained in:
@@ -53,6 +53,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
||||
|
||||
_insert_pins(dsn, board, transform, terminal_to_net)
|
||||
_insert_keepouts(dsn, board, transform)
|
||||
_insert_image_keepouts(dsn, board, transform)
|
||||
return board
|
||||
|
||||
|
||||
@@ -133,7 +134,7 @@ def _insert_pins(
|
||||
location = _pin_location(place, dsn_pin, transform)
|
||||
padstack = dsn.padstack(dsn_pin.padstack_name)
|
||||
shape, layers = _pad_shape_and_layers(
|
||||
padstack, location, transform, layer_no, signal_layers
|
||||
padstack, place, dsn_pin, location, transform, layer_no, signal_layers
|
||||
)
|
||||
net_no = terminal_to_net.get((place.name, dsn_pin.name))
|
||||
pin = Pin(
|
||||
@@ -141,6 +142,7 @@ def _insert_pins(
|
||||
component_no=id(place) & 0xFFFFFF,
|
||||
fixed=True,
|
||||
name=dsn_pin.name,
|
||||
component_ref=place.name,
|
||||
padstack_name=dsn_pin.padstack_name,
|
||||
location=location,
|
||||
layers=layers,
|
||||
@@ -149,22 +151,44 @@ def _insert_pins(
|
||||
board.insert(pin)
|
||||
|
||||
|
||||
def _to_board_offset(place, x: float, y: float, pin_rotation: float = 0.0) -> FloatPoint:
|
||||
"""Image-relative DSN offset -> board-oriented DSN offset for a placed part.
|
||||
|
||||
The chain is: the pin's own rotation (pad shapes only), then the mirror of
|
||||
a back-side part (x -> -x), then the part's rotation. Pin positions use the
|
||||
same chain without the pin rotation, so pads and positions always agree.
|
||||
"""
|
||||
p = FloatPoint(x, y)
|
||||
if pin_rotation:
|
||||
p = p.rotate(math.radians(pin_rotation), FloatPoint(0.0, 0.0))
|
||||
if not place.is_front:
|
||||
p = FloatPoint(-p.x, p.y)
|
||||
if place.rotation:
|
||||
p = p.rotate(math.radians(place.rotation), FloatPoint(0.0, 0.0))
|
||||
return p
|
||||
|
||||
|
||||
def _pin_location(place, dsn_pin, transform: CoordinateTransform) -> IntPoint:
|
||||
"""Absolute board location of a pin: component origin + rotated pin offset."""
|
||||
rel_x = -dsn_pin.x if not place.is_front else dsn_pin.x # back side mirrors in x
|
||||
rel = FloatPoint(rel_x, dsn_pin.y)
|
||||
rotated = rel.rotate(math.radians(place.rotation), FloatPoint(0.0, 0.0))
|
||||
return transform.dsn_to_board_point(place.x + rotated.x, place.y + rotated.y)
|
||||
"""Absolute board location of a pin: component origin + placed pin offset."""
|
||||
rel = _to_board_offset(place, dsn_pin.x, dsn_pin.y)
|
||||
return transform.dsn_to_board_point(place.x + rel.x, place.y + rel.y)
|
||||
|
||||
|
||||
def _pad_shape_and_layers(
|
||||
padstack,
|
||||
place,
|
||||
dsn_pin,
|
||||
location: IntPoint,
|
||||
transform: CoordinateTransform,
|
||||
layer_no: dict[str, int],
|
||||
signal_layers: list[int],
|
||||
) -> tuple[object, list[int]]:
|
||||
"""A pad tile centred on ``location`` plus the layer indices it occupies."""
|
||||
"""A pad tile around ``location`` plus the layer indices it occupies.
|
||||
|
||||
A back-side part's pads move to the mirrored copper layer (F.Cu -> B.Cu on
|
||||
a two-layer board), and the pad outline is placed through the same
|
||||
rotation and mirror as the pin itself.
|
||||
"""
|
||||
if padstack is None or not padstack.shapes:
|
||||
# degenerate: a zero-size box at the location (still contains it)
|
||||
return IntBox(location.x, location.y, location.x, location.y), list(signal_layers)
|
||||
@@ -177,12 +201,63 @@ def _pad_shape_and_layers(
|
||||
layers.add(layer_no[shape.layer])
|
||||
if not layers:
|
||||
layers = set(signal_layers)
|
||||
if not place.is_front and signal_layers:
|
||||
top, bottom = min(signal_layers), max(signal_layers)
|
||||
layers = {top + bottom - layer if layer in signal_layers else layer for layer in layers}
|
||||
|
||||
tile = _pad_tile(padstack.shapes[0], transform)
|
||||
# The largest of the padstack's shapes: per-layer shapes of a through-hole
|
||||
# pad usually match, and when they do not the bigger one is the safe side.
|
||||
shape = max(padstack.shapes, key=_shape_extent)
|
||||
tile = _placed_pad_tile(shape, place, dsn_pin, transform)
|
||||
tile = tile.translate_by(IntVector(location.x, location.y))
|
||||
return tile, sorted(layers)
|
||||
|
||||
|
||||
def _shape_extent(shape: Shape) -> float:
|
||||
coords = _shape_coords(shape)
|
||||
if isinstance(shape, Circle):
|
||||
return shape.diameter ** 2
|
||||
if len(coords) < 4:
|
||||
return 0.0
|
||||
xs, ys = coords[0::2], coords[1::2]
|
||||
return (max(xs) - min(xs)) * (max(ys) - min(ys))
|
||||
|
||||
|
||||
def _placed_pad_tile(shape: Shape, place, dsn_pin, transform: CoordinateTransform):
|
||||
"""The pad outline, rotated and mirrored with its part, relative to the pin."""
|
||||
if isinstance(shape, Circle):
|
||||
centre = _to_board_offset(place, shape.center_x, shape.center_y, dsn_pin.rotation)
|
||||
r = round(transform.dsn_to_board(shape.diameter / 2))
|
||||
cx = round(transform.dsn_to_board(centre.x))
|
||||
cy = round(transform.dsn_to_board(centre.y))
|
||||
return IntBox(cx - r, cy - r, cx + r, cy + r)
|
||||
if isinstance(shape, Rectangle):
|
||||
x0, y0, x1, y1 = shape.coords[:4]
|
||||
corners = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
|
||||
elif isinstance(shape, (Polygon, Path)):
|
||||
c = shape.coords
|
||||
corners = [(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||
else:
|
||||
return _pad_tile(shape, transform)
|
||||
# An oval pad is a path with a width (the aperture). Pad the centreline
|
||||
# out by half the width in every direction; the square ends cover more
|
||||
# than the round ones do, which is the safe side.
|
||||
half = getattr(shape, "width", 0.0) / 2 if isinstance(shape, Path) else 0.0
|
||||
if half:
|
||||
corners = [(x + dx, y + dy) for x, y in corners for dx in (-half, half) for dy in (-half, half)]
|
||||
pts = []
|
||||
for x, y in corners:
|
||||
p = _to_board_offset(place, x, y, dsn_pin.rotation)
|
||||
pts.append(IntPoint(round(transform.dsn_to_board(p.x)), round(transform.dsn_to_board(p.y))))
|
||||
if len(pts) >= 3:
|
||||
simplex = Simplex.from_corners(pts)
|
||||
if not simplex.is_empty():
|
||||
return simplex
|
||||
xs = [p.x for p in pts] or [0]
|
||||
ys = [p.y for p in pts] or [0]
|
||||
return IntBox(min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
def _pad_tile(shape: Shape, transform: CoordinateTransform):
|
||||
"""A board-relative pad tile from a DSN padstack shape (centred near origin)."""
|
||||
if isinstance(shape, Rectangle):
|
||||
@@ -298,3 +373,64 @@ def _area_border_tile(shape: Shape, transform: CoordinateTransform):
|
||||
ys = [p.y for p in pts] or [0]
|
||||
return IntBox(min(xs), min(ys), max(xs), max(ys))
|
||||
return None
|
||||
|
||||
|
||||
def _insert_image_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform) -> None:
|
||||
"""Keepouts declared inside a footprint, placed with each instance.
|
||||
|
||||
KiCad exports copper that has no pin number this way, for instance the
|
||||
plated ring of a mounting hole, so skipping them leaves copper the router
|
||||
cannot see.
|
||||
"""
|
||||
images = {img.name: img for img in dsn.images}
|
||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
||||
for placement in dsn.placements:
|
||||
image = images.get(placement.lib_name)
|
||||
if image is None:
|
||||
continue
|
||||
areas = [*image.keepouts, *image.via_keepouts, *image.wire_keepouts]
|
||||
if not areas:
|
||||
continue
|
||||
for place in placement.places:
|
||||
if place.x is None or place.y is None:
|
||||
continue
|
||||
for area in areas:
|
||||
border = area.border
|
||||
if border is None:
|
||||
continue
|
||||
tiles = _placed_area_tiles(border, place, transform)
|
||||
if not tiles:
|
||||
continue
|
||||
if border.layer in ("signal", "pcb") or border.layer not in layer_no:
|
||||
layers = list(signal_layers)
|
||||
else:
|
||||
layers = [layer_no[border.layer]]
|
||||
if not place.is_front and signal_layers:
|
||||
top, bottom = min(signal_layers), max(signal_layers)
|
||||
layers = [top + bottom - layer for layer in layers]
|
||||
for layer_index in layers:
|
||||
board.insert(ObstacleArea(name=area.name, layer=layer_index, tiles=tiles))
|
||||
|
||||
|
||||
def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> list:
|
||||
"""An image-relative area border placed with its part, as convex board tiles."""
|
||||
def to_board(x: float, y: float) -> IntPoint:
|
||||
p = _to_board_offset(place, x, y)
|
||||
return transform.dsn_to_board_point(place.x + p.x, place.y + p.y)
|
||||
|
||||
if isinstance(shape, Circle):
|
||||
c = to_board(shape.center_x, shape.center_y)
|
||||
r = round(transform.dsn_to_board(shape.diameter / 2))
|
||||
return [IntBox(c.x - r, c.y - r, c.x + r, c.y + r)]
|
||||
if isinstance(shape, Rectangle):
|
||||
x0, y0, x1, y1 = shape.coords[:4]
|
||||
pts = [to_board(x, y) for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1))]
|
||||
elif isinstance(shape, (Polygon, Path)):
|
||||
c = shape.coords
|
||||
pts = [to_board(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||
else:
|
||||
return []
|
||||
if len(pts) < 3:
|
||||
return []
|
||||
return [tile for tile in PolygonShape(pts).split_to_convex() if not tile.is_empty()]
|
||||
|
||||
@@ -48,6 +48,8 @@ class Pin(Item):
|
||||
"""A component pin: a pad shape on one or more layers at a fixed location."""
|
||||
|
||||
name: str = ""
|
||||
#: reference designator of the owning component, for diagnostics
|
||||
component_ref: str = ""
|
||||
padstack_name: str = ""
|
||||
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
|
||||
#: layer indices the pad occupies (all signal layers when spanning)
|
||||
|
||||
@@ -90,6 +90,7 @@ def route_board_exact(
|
||||
diagonal: bool = False,
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
via_half: int | None = None,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -102,6 +103,7 @@ def route_board_exact(
|
||||
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||
]
|
||||
half_width = max(trace_width // 2, 1)
|
||||
via_half = via_half or half_width
|
||||
step = trace_width + clearance
|
||||
|
||||
result = route_board(
|
||||
@@ -113,6 +115,7 @@ def route_board_exact(
|
||||
max_passes=max_passes,
|
||||
rip_up=rip_up,
|
||||
orthogonal=True,
|
||||
via_half=via_half,
|
||||
)
|
||||
|
||||
tree = ShapeSearchTree()
|
||||
@@ -141,7 +144,7 @@ def route_board_exact(
|
||||
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)
|
||||
boxes = _net_boxes(result, net_no, half_width, via_layers, via_half)
|
||||
if any(
|
||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
@@ -543,15 +546,14 @@ def _rollback(shoved, accepted, tree, owner) -> int: # noqa: ARG001
|
||||
# --- helpers -----------------------------------------------------------------
|
||||
|
||||
|
||||
def _net_boxes(result, net_no, half_width, via_layers):
|
||||
def _net_boxes(result, net_no, 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 box in PolylineShape(Polyline(points), half_width).tiles():
|
||||
boxes.append((box, frozenset({layer})))
|
||||
for loc in result.vias.get(net_no, []):
|
||||
vbox = IntBox(
|
||||
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ class _Connection:
|
||||
"vias",
|
||||
"trace_cells",
|
||||
"via_cells",
|
||||
"via_halo",
|
||||
)
|
||||
|
||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||
@@ -111,6 +112,7 @@ class _Connection:
|
||||
self.vias: list[IntPoint] = []
|
||||
self.trace_cells: list[tuple[Cell, int]] = []
|
||||
self.via_cells: list[Cell] = []
|
||||
self.via_halo: list[Cell] = []
|
||||
|
||||
|
||||
class GridRouter:
|
||||
@@ -129,6 +131,7 @@ class GridRouter:
|
||||
rip_cap: int = 4,
|
||||
rip_penalty: float = 30.0,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
) -> None:
|
||||
self.board = board
|
||||
self.trace_width = max(trace_width, 1)
|
||||
@@ -144,6 +147,12 @@ class GridRouter:
|
||||
self._neighbours = _NEIGHBOURS[:4] if orthogonal else _NEIGHBOURS
|
||||
# cell size must fit a trace plus its clearance to a neighbouring trace
|
||||
self.step = max(self.trace_width + self.clearance, 1)
|
||||
# A via's copper is wider than a trace. Another net's trace needs its
|
||||
# centre via_half + clearance + half_width from the via's centre, so a
|
||||
# via also claims every cell within via_reach of its own.
|
||||
self.via_half = via_half or self.half_width
|
||||
need = self.via_half + self.clearance + self.half_width
|
||||
self.via_reach = max(0, math.ceil(need / self.step) - 1)
|
||||
|
||||
if layers is None:
|
||||
layers = [i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal]
|
||||
@@ -243,10 +252,22 @@ class GridRouter:
|
||||
return None
|
||||
return self.rip_penalty * (1 + rip_count)
|
||||
|
||||
def _via_cells(self, cell: Cell) -> list[Cell]:
|
||||
"""The cell a via sits in plus the neighbours its copper reaches."""
|
||||
r = self.via_reach
|
||||
cx, cy = cell
|
||||
return [
|
||||
(x, y)
|
||||
for x in range(max(cx - r, 0), min(cx + r, self.cols - 1) + 1)
|
||||
for y in range(max(cy - r, 0), min(cy + r, self.rows - 1) + 1)
|
||||
]
|
||||
|
||||
def _via_placeable(self, cell: Cell, net_no: int, conns: list[_Connection]) -> bool:
|
||||
"""A through via needs every signal layer strictly free of other nets."""
|
||||
"""A through via needs its whole footprint free of other nets on every layer."""
|
||||
return all(
|
||||
self._enter_cost(cell, layer, net_no, False, conns) == 0.0 for layer in self.layers
|
||||
self._enter_cost(c, layer, net_no, False, conns) == 0.0
|
||||
for c in self._via_cells(cell)
|
||||
for layer in self.layers
|
||||
)
|
||||
|
||||
# --- A* search ----------------------------------------------------------
|
||||
@@ -321,8 +342,12 @@ class GridRouter:
|
||||
conn.via_cells = list(via_cells)
|
||||
for cell, layer in conn.trace_cells:
|
||||
self._cell_owner[(*cell, layer)] = conn.index
|
||||
conn.via_halo = []
|
||||
for cell in conn.via_cells:
|
||||
self._via_owner[cell] = conn.index
|
||||
for c in self._via_cells(cell):
|
||||
if c not in self._via_owner:
|
||||
self._via_owner[c] = conn.index
|
||||
conn.via_halo.append(c)
|
||||
conn.wires = []
|
||||
for idx, (layer, cells) in enumerate(wire_segs):
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
@@ -340,9 +365,10 @@ class GridRouter:
|
||||
for cell, layer in conn.trace_cells:
|
||||
if self._cell_owner.get((*cell, layer)) == conn.index:
|
||||
del self._cell_owner[(*cell, layer)]
|
||||
for cell in conn.via_cells:
|
||||
for cell in conn.via_halo:
|
||||
if self._via_owner.get(cell) == conn.index:
|
||||
del self._via_owner[cell]
|
||||
conn.via_halo = []
|
||||
conn.trace_cells = []
|
||||
conn.via_cells = []
|
||||
conn.wires = []
|
||||
@@ -493,6 +519,7 @@ def route_board(
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -504,5 +531,6 @@ def route_board(
|
||||
rip_up=rip_up,
|
||||
max_passes=max_passes,
|
||||
orthogonal=orthogonal,
|
||||
via_half=via_half,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -45,6 +45,25 @@ def _via_padstack(dsn: DsnBoard) -> str:
|
||||
return dsn.via_padstack_names[0] if dsn.via_padstack_names else _DEFAULT_VIA
|
||||
|
||||
|
||||
def _via_half_board(dsn: DsnBoard, scale: int) -> int | None:
|
||||
"""Half the via pad's size in board units, from the DSN's via padstack.
|
||||
|
||||
None when the DSN names no via padstack with a shape, so the engines fall
|
||||
back to their old estimate.
|
||||
"""
|
||||
padstack = dsn.padstack(_via_padstack(dsn))
|
||||
if padstack is None or not padstack.shapes:
|
||||
return None
|
||||
half = 0.0
|
||||
for shape in padstack.shapes:
|
||||
if hasattr(shape, "diameter"):
|
||||
half = max(half, shape.diameter / 2)
|
||||
elif len(getattr(shape, "coords", [])) >= 4:
|
||||
xs, ys = shape.coords[0::2], shape.coords[1::2]
|
||||
half = max(half, (max(xs) - min(xs)) / 2, (max(ys) - min(ys)) / 2)
|
||||
return round(half * scale) or None
|
||||
|
||||
|
||||
def route_dsn_board(
|
||||
dsn: DsnBoard,
|
||||
*,
|
||||
@@ -62,6 +81,7 @@ def route_dsn_board(
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
result = route_board(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
@@ -130,6 +150,7 @@ def route_dsn_board_exact(
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
exact = route_board_exact(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
@@ -175,6 +196,7 @@ def route_dsn_board_rooms(
|
||||
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||
rooms = route_board_rooms(
|
||||
board,
|
||||
via_half=_via_half_board(dsn, scale),
|
||||
trace_width=width_board,
|
||||
clearance=clearance_board,
|
||||
layers=layers,
|
||||
|
||||
@@ -471,6 +471,7 @@ def route_board_rooms(
|
||||
pack: bool = False,
|
||||
max_shove_depth: int = 5,
|
||||
shove_cap: int = 3,
|
||||
via_half: int | None = None,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` with the continuous expansion-room track.
|
||||
|
||||
@@ -494,6 +495,9 @@ def route_board_rooms(
|
||||
i for i, layer in enumerate(board.layer_structure.arr) if layer.is_signal
|
||||
]
|
||||
half_width = max(trace_width // 2, 1)
|
||||
# half the via pad's size: a via is not a trace, and sizing it like one
|
||||
# let routes pass within a via's real copper
|
||||
via_half = via_half or half_width
|
||||
margin = clearance + half_width
|
||||
outline = board.bounding_box
|
||||
|
||||
@@ -537,7 +541,7 @@ def route_board_rooms(
|
||||
via_layers,
|
||||
accepted,
|
||||
shove,
|
||||
)
|
||||
via_half=via_half)
|
||||
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||
if net_no in packed:
|
||||
continue
|
||||
@@ -562,7 +566,7 @@ def route_board_rooms(
|
||||
accepted,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
)
|
||||
via_half=via_half)
|
||||
|
||||
drc_clean = tree.has_violation(clearance) is None
|
||||
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
||||
@@ -587,6 +591,7 @@ def _route_connection(
|
||||
accepted=None,
|
||||
max_shove_depth=5,
|
||||
shove_cap=3,
|
||||
via_half=None,
|
||||
):
|
||||
rooms_by_layer = {
|
||||
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
|
||||
@@ -607,7 +612,7 @@ def _route_connection(
|
||||
|
||||
if realized is not None:
|
||||
wires, vias = realized
|
||||
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline)
|
||||
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=via_half)
|
||||
if boxes is not None and not any(
|
||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
@@ -615,7 +620,7 @@ def _route_connection(
|
||||
):
|
||||
_commit(
|
||||
net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted
|
||||
)
|
||||
, via_half=via_half)
|
||||
return 0
|
||||
|
||||
# the net would be dropped: try a transactional shove-in-rooms recovery
|
||||
@@ -634,11 +639,12 @@ def _route_connection(
|
||||
clearance,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
)
|
||||
via_half=via_half)
|
||||
return 0
|
||||
|
||||
|
||||
def _collect_boxes(wires, vias, half_width, via_layers, outline):
|
||||
def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None):
|
||||
via_half = via_half or half_width
|
||||
"""The ``(box, layers)`` copper tiles of a realized route, or ``None`` if any
|
||||
tile leaves the board outline (the same early-out as the old inline check)."""
|
||||
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||
@@ -649,13 +655,16 @@ def _collect_boxes(wires, vias, half_width, via_layers, outline):
|
||||
boxes.append((box, frozenset({layer})))
|
||||
for loc in vias:
|
||||
vbox = IntBox(
|
||||
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||
loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
||||
)
|
||||
boxes.append((vbox, via_layers))
|
||||
return boxes
|
||||
|
||||
|
||||
def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted):
|
||||
def _commit(
|
||||
net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted, via_half=None
|
||||
):
|
||||
via_half = via_half or half_width
|
||||
"""Insert a verified route's copper and record it in the routed output.
|
||||
|
||||
With ``shove=False`` this is the foundation router's exact commit sequence.
|
||||
@@ -668,7 +677,7 @@ def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, sh
|
||||
tree.insert(TreeShape(next(owner), net_no, frozenset({layer}), box, routed=True))
|
||||
for loc in vias:
|
||||
vbox = IntBox(
|
||||
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||
loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
||||
)
|
||||
tree.insert(TreeShape(next(owner), net_no, via_layers, vbox, routed=True))
|
||||
for layer, corners in wires:
|
||||
@@ -693,7 +702,7 @@ def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, sh
|
||||
acc.wire_owners.append(w_owners)
|
||||
for loc in vias:
|
||||
vbox = IntBox(
|
||||
loc.x - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width
|
||||
loc.x - via_half, loc.y - via_half, loc.x + via_half, loc.y + via_half
|
||||
)
|
||||
oid = next(owner)
|
||||
tree.insert(TreeShape(oid, net_no, via_layers, vbox, routed=True))
|
||||
@@ -854,6 +863,7 @@ def _pack_channels(
|
||||
via_layers,
|
||||
accepted,
|
||||
shove,
|
||||
via_half=None,
|
||||
) -> set[int]:
|
||||
"""Coordinated multi-trace channel packing (opt-in pre-pass).
|
||||
|
||||
@@ -922,7 +932,7 @@ def _pack_channels(
|
||||
spacing_min,
|
||||
via_layers,
|
||||
outline,
|
||||
)
|
||||
via_half=via_half)
|
||||
if placed is None:
|
||||
continue # could not pack every member cleanly -> leave to greedy
|
||||
for net_no, wires in placed:
|
||||
@@ -937,7 +947,7 @@ def _pack_channels(
|
||||
via_layers,
|
||||
shove,
|
||||
accepted,
|
||||
)
|
||||
via_half=via_half)
|
||||
routed.add(net_no)
|
||||
return routed
|
||||
|
||||
@@ -958,6 +968,7 @@ def _pack_group(
|
||||
spacing_min,
|
||||
via_layers,
|
||||
outline,
|
||||
via_half=None,
|
||||
):
|
||||
"""Assign parallel lanes + staggered fan-in trunks to every net crossing one
|
||||
gap, and return ``[(net_no, wires), ...]`` if ALL of them realize exactly
|
||||
@@ -1014,7 +1025,7 @@ def _pack_group(
|
||||
if len(corners) < 2:
|
||||
return None
|
||||
wires = [(layer, corners)]
|
||||
boxes = _collect_boxes(wires, [], half_width, via_layers, outline)
|
||||
boxes = _collect_boxes(wires, [], half_width, via_layers, outline, via_half=via_half)
|
||||
if boxes is None:
|
||||
return None
|
||||
if any(
|
||||
@@ -1049,6 +1060,7 @@ def _shove_recover_room(
|
||||
clearance,
|
||||
max_shove_depth,
|
||||
shove_cap,
|
||||
via_half=None,
|
||||
) -> int:
|
||||
"""Recover a dropped net by nudging the committed trace(s) that block it.
|
||||
|
||||
|
||||
@@ -63,11 +63,20 @@ def test_pin_count_matches_placement(dsn, board):
|
||||
|
||||
|
||||
def test_obstacle_count_matches_keepouts(dsn, board):
|
||||
# each keepout expands to one obstacle per applicable layer; with no keepouts
|
||||
# in this fixture the count is zero
|
||||
assert len(board.get_obstacle_areas()) >= len(dsn.keepouts)
|
||||
# Board-level keepouts plus every placed instance of a footprint-level one.
|
||||
# This fixture has none of the first and six plated mounting holes, whose
|
||||
# copper KiCad exports as a keepout on each layer.
|
||||
images = {img.name: img for img in dsn.images}
|
||||
placed = 0
|
||||
for placement in dsn.placements:
|
||||
image = images.get(placement.lib_name)
|
||||
if image is None:
|
||||
continue
|
||||
per_part = len(image.keepouts) + len(image.via_keepouts) + len(image.wire_keepouts)
|
||||
placed += per_part * sum(p.x is not None for p in placement.places)
|
||||
assert len(dsn.keepouts) == 0
|
||||
assert board.get_obstacle_areas() == []
|
||||
assert placed == 12
|
||||
assert len(board.get_obstacle_areas()) == placed
|
||||
|
||||
|
||||
# --- per-item invariants ----------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user