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.
625 lines
22 KiB
Python
625 lines
22 KiB
Python
"""Exact-geometry routing track (orthogonal), built on the search tree.
|
|
|
|
The grid MVP (:mod:`freeroute.route.grid_router`) approximates clearance with
|
|
cell occupancy and emits centrelines with no width-aware DRC check. This track
|
|
keeps the grid's rip-up/multi-layer/via *topology* search (in orthogonal mode,
|
|
so segments are axis-aligned) but replaces the occupancy check with an **exact**
|
|
one: every item's copper is an exact :class:`~freeroute.geometry.IntBox` tile
|
|
indexed in a :class:`~freeroute.board.search_tree.ShapeSearchTree`, and the
|
|
routed board is verified to have **no clearance violation** by exact tile
|
|
intersection. A net whose copper would violate is dropped, so the output is
|
|
DRC-clean by construction.
|
|
|
|
**Shove (opt-in, ``shove=True``).** Before a dropped net is abandoned, the
|
|
router tries to make room by *moving an existing trace aside* geometrically
|
|
(:func:`~freeroute.geometry.shove_segment`) rather than dropping the incoming
|
|
net. A shove is accepted only if the moved trace still connects its pads, clears
|
|
every other item exactly (via the tree), and stays in the board outline — so
|
|
cleanliness is preserved. Shoves are bounded (``max_shove_depth`` and a
|
|
per-trace cap) and deterministic.
|
|
|
|
This is an MVP shove for orthogonal traces: it recovers a straight-line net by
|
|
shoving the traces that cross it perpendicular. On this grid-based track its
|
|
extra reach over rip-up is limited (rip-up already reroutes); the larger density
|
|
win needs a continuous expansion-room router (the next gap). Also deferred:
|
|
45-degree / ``IntOctagon`` trace caps.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
import math
|
|
|
|
from freeroute.board.board import BasicBoard
|
|
from freeroute.board.items import Pin
|
|
from freeroute.board.search_tree import ShapeSearchTree, TreeShape
|
|
from freeroute.geometry import (
|
|
IntBox,
|
|
IntPoint,
|
|
Polyline,
|
|
PolylineShape,
|
|
segment_box,
|
|
segment_octagon,
|
|
shove_segment,
|
|
)
|
|
|
|
from .grid_router import RouteResult, route_board
|
|
|
|
__all__ = ["ExactRouteResult", "route_board_exact"]
|
|
|
|
|
|
@dataclass
|
|
class _Accepted:
|
|
"""A committed net's per-net geometry and its tile owners in the tree."""
|
|
|
|
wires: list[tuple[int, list[IntPoint]]] = field(default_factory=list)
|
|
vias: list[IntPoint] = field(default_factory=list)
|
|
owners: list[int] = field(default_factory=list)
|
|
shove_count: int = 0
|
|
|
|
|
|
@dataclass
|
|
class ExactRouteResult:
|
|
"""The routed geometry plus the exact spatial index it was verified against."""
|
|
|
|
result: RouteResult
|
|
tree: ShapeSearchTree
|
|
#: ``True`` if the routed board has no different-net clearance violation
|
|
drc_clean: bool
|
|
#: number of successful shoves performed
|
|
shoves: int = 0
|
|
|
|
@property
|
|
def routed_net_numbers(self):
|
|
return self.result.routed_net_numbers
|
|
|
|
def via_count(self) -> int:
|
|
return self.result.via_count()
|
|
|
|
|
|
def route_board_exact(
|
|
board: BasicBoard,
|
|
*,
|
|
trace_width: int,
|
|
clearance: int,
|
|
layers: list[int] | None = None,
|
|
via_cost: float = 10.0,
|
|
max_passes: int = 10,
|
|
rip_up: bool = True,
|
|
shove: bool = False,
|
|
diagonal: bool = False,
|
|
max_shove_depth: int = 5,
|
|
shove_cap: int = 3,
|
|
via_half: int | None = None,
|
|
edge_clearance: int | None = None,
|
|
resolution: int = 1,
|
|
heuristic_weight: float = 1.0,
|
|
) -> ExactRouteResult:
|
|
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
|
|
|
``diagonal`` (opt-in) adds a 45-degree recovery pass: a still-dropped 2-pin net
|
|
is retried as an octilinear (0/45/90/135-degree) trace whose diagonal copper is
|
|
covered by an exact integer octagon, so it fits diagonal corridors the
|
|
orthogonal L cannot. With ``diagonal=False`` the pass is skipped and the output
|
|
is byte-for-byte the orthogonal router."""
|
|
signal_layers = layers or [
|
|
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(
|
|
board,
|
|
trace_width=trace_width,
|
|
clearance=clearance,
|
|
layers=signal_layers,
|
|
via_cost=via_cost,
|
|
max_passes=max_passes,
|
|
rip_up=rip_up,
|
|
orthogonal=True,
|
|
via_half=via_half,
|
|
edge_clearance=edge_clearance,
|
|
resolution=resolution,
|
|
heuristic_weight=heuristic_weight,
|
|
)
|
|
edge = 0 if edge_clearance is None else edge_clearance
|
|
outline = board.outline
|
|
|
|
tree = ShapeSearchTree()
|
|
owner = _counter()
|
|
|
|
# static items: pad and keepout copper (routed=False)
|
|
for pin in board.get_pins():
|
|
if pin.shape is None:
|
|
continue
|
|
box = pin.shape.bounding_box()
|
|
if box.is_empty():
|
|
continue
|
|
net = pin.net_nos[0] if pin.net_nos else -1
|
|
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
|
|
if pin_layers:
|
|
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
|
for obstacle in board.get_obstacle_areas():
|
|
if obstacle.layer not in signal_layers:
|
|
continue
|
|
blocks = (
|
|
"all" if obstacle.blocks_wires and obstacle.blocks_vias
|
|
else "wires" if obstacle.blocks_wires else "vias"
|
|
)
|
|
for tile in obstacle.tiles:
|
|
box = tile.bounding_box()
|
|
if not box.is_empty():
|
|
tree.insert(
|
|
TreeShape(next(owner), -1, frozenset({obstacle.layer}), box, blocks=blocks)
|
|
)
|
|
|
|
via_layers = frozenset(signal_layers)
|
|
accepted: dict[int, _Accepted] = {}
|
|
|
|
# Verify connection by connection, so one conflicting segment of a
|
|
# 25-pin supply net costs that one connection, not the whole net.
|
|
rejected: list[tuple[int, str, str]] = []
|
|
ends_list = result.connection_ends or [("?", "?", "")] * len(result.connections)
|
|
for (net_no, wires, vias), ends in sorted(
|
|
zip(result.connections, ends_list, strict=False), key=lambda c: c[0][0]
|
|
):
|
|
boxes = _copper_boxes(wires, vias, half_width, via_layers, via_half)
|
|
wire_count = len(_copper_boxes(wires, [], half_width, via_layers, via_half))
|
|
if any(
|
|
tree.clearance_conflict(
|
|
box, net_no, layer, clearance, "wire" if i < wire_count else "via"
|
|
)
|
|
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)):
|
|
rejected.append((net_no, *ends))
|
|
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
|
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_before_recovery = set(accepted)
|
|
# 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
|
|
# copper conflicted.
|
|
dropped = sorted(_routable_nets(board, signal_layers) - set(accepted))
|
|
|
|
shoves = 0
|
|
if shove and dropped:
|
|
outline = board.bounding_box
|
|
for net_no in dropped:
|
|
done = _shove_recover(
|
|
net_no,
|
|
board,
|
|
tree,
|
|
accepted,
|
|
owner,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
step,
|
|
outline,
|
|
max_shove_depth,
|
|
shove_cap,
|
|
)
|
|
if done:
|
|
shoves += done
|
|
|
|
if diagonal:
|
|
outline = board.bounding_box
|
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
|
_diagonalize(
|
|
net_no, board, tree, accepted, owner, signal_layers, half_width, clearance, outline
|
|
)
|
|
|
|
clean = RouteResult(half_width)
|
|
# what is still missing: what the grid could not route, plus what the
|
|
# exact check rejected, less any net shove or the diagonal pass recovered
|
|
clean.failed = [
|
|
*result.failed,
|
|
*(r for r in rejected if not (r[0] in accepted and r[0] not in accepted_before_recovery)),
|
|
]
|
|
for net_no, acc in accepted.items():
|
|
clean.wires[net_no] = acc.wires
|
|
clean.vias[net_no] = acc.vias
|
|
|
|
drc_clean = tree.has_violation(clearance) is None
|
|
return ExactRouteResult(result=clean, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
|
|
|
|
|
# --- 45-degree (diagonal) recovery -------------------------------------------
|
|
|
|
|
|
def _octilinear_candidates(a: IntPoint, b: IntPoint) -> list[list[IntPoint]]:
|
|
"""Octilinear (0/45/90/135-degree) polylines from ``a`` to ``b``.
|
|
|
|
A direct diagonal when the offset is exactly 45-degree, otherwise the two
|
|
two-segment paths that spend ``min(|dx|, |dy|)`` on a 45-degree run and the
|
|
rest on an axis run (diagonal-first and diagonal-last)."""
|
|
dx, dy = b.x - a.x, b.y - a.y
|
|
sx = (dx > 0) - (dx < 0)
|
|
sy = (dy > 0) - (dy < 0)
|
|
adx, ady = abs(dx), abs(dy)
|
|
if adx == ady:
|
|
raw = [[a, b]]
|
|
else:
|
|
m = min(adx, ady)
|
|
raw = [
|
|
[a, IntPoint(a.x + sx * m, a.y + sy * m), b], # diagonal first
|
|
[a, IntPoint(b.x - sx * m, b.y - sy * m), b], # diagonal last
|
|
]
|
|
out: list[list[IntPoint]] = []
|
|
for corners in raw:
|
|
deduped: list[IntPoint] = []
|
|
for p in corners:
|
|
if deduped and deduped[-1].x == p.x and deduped[-1].y == p.y:
|
|
continue
|
|
deduped.append(p)
|
|
if len(deduped) >= 2:
|
|
out.append(deduped)
|
|
return out
|
|
|
|
|
|
def _candidate_tiles(corners, net_no, layer, tree, half_width, clearance, outline):
|
|
"""``(length, tiles)`` if this octilinear candidate is exactly clean, else
|
|
``None``. ``tiles`` are ``(box, exact, seg)`` triples ready to insert: a plain
|
|
box for an orthogonal segment or corner, a bounding box + octagon copper + seg
|
|
for a diagonal segment."""
|
|
tiles: list[tuple[IntBox, object, object]] = []
|
|
length = 0.0
|
|
for p, q in zip(corners, corners[1:], strict=False):
|
|
dx, dy = q.x - p.x, q.y - p.y
|
|
length += math.hypot(dx, dy)
|
|
if dx == 0 or dy == 0: # orthogonal
|
|
box = segment_box(p, q, half_width)
|
|
if not outline.contains_box(box):
|
|
return None
|
|
if tree.clearance_conflict(box, net_no, layer, clearance):
|
|
return None
|
|
tiles.append((box, None, None))
|
|
elif abs(dx) == abs(dy): # 45-degree diagonal
|
|
copper = segment_octagon(p, q, half_width)
|
|
if not outline.contains_box(copper.bounding_box()):
|
|
return None
|
|
grown = segment_octagon(p, q, half_width + clearance)
|
|
if tree.clearance_conflict_shape(grown, net_no, layer):
|
|
return None
|
|
tiles.append((copper.bounding_box(), copper, (p, q, half_width)))
|
|
else: # not octilinear -- reject
|
|
return None
|
|
for corner in corners[1:-1]:
|
|
square = IntBox(
|
|
corner.x - half_width,
|
|
corner.y - half_width,
|
|
corner.x + half_width,
|
|
corner.y + half_width,
|
|
)
|
|
if not outline.contains_box(square):
|
|
return None
|
|
if tree.clearance_conflict(square, net_no, layer, clearance):
|
|
return None
|
|
tiles.append((square, None, None))
|
|
return length, tiles
|
|
|
|
|
|
def _wire_length(wires) -> float:
|
|
total = 0.0
|
|
for _layer, corners in wires:
|
|
for p, q in zip(corners, corners[1:], strict=False):
|
|
total += math.hypot(q.x - p.x, q.y - p.y)
|
|
return total
|
|
|
|
|
|
def _diagonalize(
|
|
net_no, board, tree, accepted, owner, signal_layers, half_width, clearance, outline
|
|
) -> bool:
|
|
"""Route (or shorten) a 2-pin net with the shortest exactly-clean octilinear
|
|
trace. Recovers a dropped net; replaces an orthogonal route only when the
|
|
diagonal is strictly shorter. Endpoints stay on the pads; diagonal copper is
|
|
stored as an exact octagon so later nets clear it precisely. Same-net copper is
|
|
ignored by the clearance check, so the candidate is verified against every
|
|
*other* net before the old copper is removed. Returns ``True`` on a change."""
|
|
pins = _net_pins(board, net_no, signal_layers)
|
|
if len(pins) != 2:
|
|
return False
|
|
a, b = pins
|
|
layer = next(iter(set(a.layers) & set(b.layers) & set(signal_layers)), None)
|
|
if layer is None:
|
|
return False
|
|
|
|
acc = accepted.get(net_no)
|
|
if acc is not None and acc.vias:
|
|
return False # keep a multi-layer orthogonal route; diagonal MVP is single-layer
|
|
|
|
best = None
|
|
for corners in _octilinear_candidates(a.location, b.location):
|
|
found = _candidate_tiles(corners, net_no, layer, tree, half_width, clearance, outline)
|
|
if found is None:
|
|
continue
|
|
length, tiles = found
|
|
if best is None or length < best[0]:
|
|
best = (length, corners, tiles)
|
|
if best is None:
|
|
return False
|
|
length, corners, tiles = best
|
|
|
|
if acc is not None:
|
|
if length >= _wire_length(acc.wires):
|
|
return False # diagonal is not shorter -- keep the orthogonal route
|
|
for oid in acc.owners:
|
|
tree.remove_owner(oid) # rip the orthogonal copper; diagonal already clean of others
|
|
|
|
new = _Accepted(wires=[(layer, corners)], vias=[])
|
|
for box, exact, seg in tiles:
|
|
oid = next(owner)
|
|
tree.insert(
|
|
TreeShape(oid, net_no, frozenset({layer}), box, routed=True, exact=exact, seg=seg)
|
|
)
|
|
new.owners.append(oid)
|
|
accepted[net_no] = new
|
|
return True
|
|
|
|
|
|
# --- shove recovery ----------------------------------------------------------
|
|
|
|
|
|
def _shove_recover(
|
|
net_no,
|
|
board,
|
|
tree,
|
|
accepted,
|
|
owner,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
step,
|
|
outline,
|
|
max_shove_depth,
|
|
shove_cap,
|
|
) -> int:
|
|
"""Try to route dropped net ``net_no`` by shoving crossing traces aside.
|
|
|
|
MVP: a straight orthogonal candidate for a 2-pin net, shoving each single-
|
|
segment trace that crosses it perpendicular until it clears. Returns the
|
|
number of successful shoves (0 if the net stays dropped).
|
|
"""
|
|
pins = _net_pins(board, net_no, signal_layers)
|
|
if len(pins) != 2:
|
|
return 0
|
|
a, b = pins
|
|
if a.location.x != b.location.x and a.location.y != b.location.y:
|
|
return 0 # MVP only recovers straight (axis-aligned) nets
|
|
layer = next(iter(set(a.layers) & set(b.layers) & set(signal_layers)), None)
|
|
if layer is None:
|
|
return 0
|
|
corners = [a.location, b.location]
|
|
d_boxes = PolylineShape(Polyline(corners), half_width).tiles()
|
|
|
|
# which accepted traces cross this net's straight copper?
|
|
blockers: dict[int, list[int]] = {}
|
|
for box in d_boxes:
|
|
for shape in tree.overlapping(box.offset(clearance)):
|
|
if not shape.routed or shape.net_no == net_no or layer not in shape.layers:
|
|
continue
|
|
if shape.tile.overlaps(box.offset(clearance)):
|
|
blockers.setdefault(shape.net_no, []).append(shape.owner)
|
|
|
|
if len(blockers) > max_shove_depth:
|
|
return 0
|
|
|
|
shoved: list[int] = []
|
|
for blocker_net in sorted(blockers):
|
|
acc = accepted.get(blocker_net)
|
|
if acc is None or acc.shove_count >= shove_cap:
|
|
return _rollback(shoved, accepted, tree, owner)
|
|
if not _try_shove_trace(
|
|
blocker_net,
|
|
corners,
|
|
layer,
|
|
board,
|
|
tree,
|
|
accepted,
|
|
owner,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
step,
|
|
outline,
|
|
):
|
|
return _rollback(shoved, accepted, tree, owner)
|
|
shoved.append(blocker_net)
|
|
|
|
# re-verify the dropped net is now clean, then accept it
|
|
if any(tree.clearance_conflict(box, net_no, layer, clearance) for box in d_boxes):
|
|
return _rollback(shoved, accepted, tree, owner)
|
|
acc = _Accepted(wires=[(layer, corners)], vias=[])
|
|
for box in d_boxes:
|
|
oid = next(owner)
|
|
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
|
acc.owners.append(oid)
|
|
accepted[net_no] = acc
|
|
return len(shoved) + 1
|
|
|
|
|
|
def _try_shove_trace(
|
|
blocker_net,
|
|
d_corners,
|
|
layer,
|
|
board,
|
|
tree,
|
|
accepted,
|
|
owner,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
step,
|
|
outline,
|
|
) -> bool:
|
|
"""Shove ``blocker_net``'s trace clear of the straight net at ``d_corners``.
|
|
|
|
Only the single-segment straight-crossing case is handled (MVP). Tries
|
|
increasing perpendicular displacements away from the crossing net until the
|
|
moved trace clears everything exactly.
|
|
"""
|
|
acc = accepted[blocker_net]
|
|
if len(acc.wires) != 1:
|
|
return False
|
|
seg_layer, corners = acc.wires[0]
|
|
if seg_layer != layer:
|
|
return False
|
|
|
|
# find the blocker's axis-aligned segment (run) that conflicts with the
|
|
# crossing net, and the perpendicular displacements to clear it
|
|
d_boxes = PolylineShape(Polyline(d_corners), half_width).tiles()
|
|
seg_index, horizontal = _conflicting_segment(corners, d_boxes, half_width, clearance)
|
|
if seg_index is None:
|
|
return False
|
|
|
|
d0, d1 = d_corners[0], d_corners[1]
|
|
margin = 2 * half_width + clearance
|
|
fine = half_width + clearance
|
|
ref = corners[seg_index]
|
|
displacements: list[tuple[int, int]] = []
|
|
if horizontal: # displace in y; clear the crossing net's y-span
|
|
lo, hi = min(d0.y, d1.y), max(d0.y, d1.y)
|
|
for extra in range(6):
|
|
displacements.append((0, (hi + margin) - ref.y + extra * fine))
|
|
displacements.append((0, (lo - margin) - ref.y - extra * fine))
|
|
else: # displace in x
|
|
lo, hi = min(d0.x, d1.x), max(d0.x, d1.x)
|
|
for extra in range(6):
|
|
displacements.append(((hi + margin) - ref.x + extra * fine, 0))
|
|
displacements.append(((lo - margin) - ref.x - extra * fine, 0))
|
|
|
|
# remove the blocker's current tiles so its shoved copy can be verified alone
|
|
for oid in acc.owners:
|
|
tree.remove_owner(oid)
|
|
|
|
for dx, dy in displacements:
|
|
if dx == 0 and dy == 0:
|
|
continue
|
|
new_corners = shove_segment(list(corners), seg_index, dx, dy)
|
|
if _place_trace(
|
|
blocker_net,
|
|
seg_layer,
|
|
new_corners,
|
|
tree,
|
|
acc,
|
|
owner,
|
|
half_width,
|
|
clearance,
|
|
outline,
|
|
):
|
|
acc.wires = [(seg_layer, new_corners)]
|
|
acc.shove_count += 1
|
|
return True
|
|
|
|
# could not shove cleanly: restore the original tiles
|
|
_place_trace(
|
|
blocker_net,
|
|
seg_layer,
|
|
corners,
|
|
tree,
|
|
acc,
|
|
owner,
|
|
half_width,
|
|
clearance,
|
|
outline,
|
|
force=True,
|
|
)
|
|
return False
|
|
|
|
|
|
def _conflicting_segment(corners, d_boxes, half_width, clearance):
|
|
"""Index + orientation of the axis-aligned segment of ``corners`` that
|
|
conflicts with the crossing net's copper ``d_boxes``, or ``(None, False)``."""
|
|
for k in range(len(corners) - 1):
|
|
a, b = corners[k], corners[k + 1]
|
|
horizontal = a.y == b.y
|
|
if not horizontal and a.x != b.x:
|
|
continue # skip diagonal grid stubs
|
|
from freeroute.geometry import segment_box
|
|
|
|
seg = segment_box(a, b, half_width).offset(clearance)
|
|
if any(seg.overlaps(box) for box in d_boxes):
|
|
return k, horizontal
|
|
return None, False
|
|
|
|
|
|
def _place_trace(
|
|
net_no, layer, corners, tree, acc, owner, half_width, clearance, outline, force=False
|
|
) -> bool:
|
|
"""Verify + insert a trace's copper; ``force`` skips the DRC/outline check."""
|
|
boxes = PolylineShape(Polyline(corners), half_width).tiles()
|
|
if not force:
|
|
for box in boxes:
|
|
if not outline.contains_box(box):
|
|
return False
|
|
if tree.clearance_conflict(box, net_no, layer, clearance):
|
|
return False
|
|
acc.owners = []
|
|
for box in boxes:
|
|
oid = next(owner)
|
|
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
|
acc.owners.append(oid)
|
|
return True
|
|
|
|
|
|
def _rollback(shoved, accepted, tree, owner) -> int: # noqa: ARG001
|
|
# shoved traces were verified individually; nothing else to undo for the MVP
|
|
return 0
|
|
|
|
|
|
# --- helpers -----------------------------------------------------------------
|
|
|
|
|
|
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 wires:
|
|
for box in PolylineShape(Polyline(points), half_width).tiles():
|
|
boxes.append((box, frozenset({layer})))
|
|
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
|
|
|
|
|
|
def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]:
|
|
return [
|
|
pin
|
|
for pin in board.get_pins()
|
|
if net_no in pin.net_nos and any(layer in signal_layers for layer in pin.layers)
|
|
]
|
|
|
|
|
|
def _routable_nets(board: BasicBoard, signal_layers) -> set[int]:
|
|
"""Net numbers with at least two pins on a signal layer (a real ratsnest)."""
|
|
counts: dict[int, int] = {}
|
|
for pin in board.get_pins():
|
|
if not any(layer in signal_layers for layer in pin.layers):
|
|
continue
|
|
for net_no in pin.net_nos:
|
|
counts[net_no] = counts.get(net_no, 0) + 1
|
|
return {net_no for net_no, c in counts.items() if c >= 2}
|
|
|
|
|
|
def _counter():
|
|
i = 0
|
|
while True:
|
|
yield i
|
|
i += 1
|