- 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.
1217 lines
45 KiB
Python
1217 lines
45 KiB
Python
"""Continuous expansion-room routing track (foundation).
|
|
|
|
The grid and exact tracks quantize space into fixed ``width + clearance`` cells.
|
|
This track replaces that grid with FreeRouting's idea of **free-space rooms**:
|
|
per layer, the space *not* occupied by items (inflated by the routing clearance)
|
|
is decomposed into exact convex tiles (here axis-aligned :class:`IntBox` rooms
|
|
via coordinate compression, the orthogonal analogue of
|
|
``CompleteFreeSpaceExpansionRoom``). Adjacent rooms share a **door** on their
|
|
common edge (and a **via door** joins the same footprint on two layers); the
|
|
maze search is an A* over rooms-through-doors, so a path may cross a door at any
|
|
point along its width — continuous, not cell-quantized. The found room sequence
|
|
is realized into an exact orthogonal :class:`Polyline` and verified DRC-clean
|
|
against the :class:`ShapeSearchTree`, exactly like the exact track.
|
|
|
|
**Foundation scope (this is large — the tail is deferred):** rooms are
|
|
axis-aligned rectangles (not general ``Simplex`` tiles); path realization routes
|
|
through door mid-gates with an L-connector and drops a net if the realized trace
|
|
cannot be made exactly clean. Any-angle rooms, optimal gate placement (true
|
|
sub-cell shove), and optimization passes are follow-ups. The grid and exact
|
|
tracks are unchanged; this is opt-in via ``engine="room"``.
|
|
|
|
Maps to FreeRouting: rooms ← ``ExpansionRoom`` /
|
|
``CompleteFreeSpaceExpansionRoom``; doors ← ``ExpansionDoor`` /
|
|
``TargetItemExpansionDoor``; search ← ``MazeSearchAlgo`` / ``AutorouteEngine``;
|
|
realization ← ``LocateFoundConnectionAlgo``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
import heapq
|
|
|
|
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, shove_segment
|
|
|
|
from .exact_router import ExactRouteResult, _routable_nets
|
|
from .grid_router import RouteResult
|
|
from .shove import (
|
|
conflicting_segment,
|
|
force_place_owners,
|
|
place_trace_owners,
|
|
shove_displacements,
|
|
trace_tiles,
|
|
)
|
|
|
|
__all__ = ["route_board_rooms"]
|
|
|
|
|
|
@dataclass
|
|
class _Accepted:
|
|
"""A committed net's per-wire tile owners, for occupancy-aware placement and
|
|
the journaled shove rollback.
|
|
|
|
``wires`` / ``vias`` are the *same* list objects as ``RouteResult.wires`` /
|
|
``.vias`` for this net, so rewriting a shoved wire in place also updates the
|
|
routed output. ``wire_owners`` is parallel to ``wires``: each entry is the
|
|
list of :class:`~freeroute.board.search_tree.TreeShape` owner ids for that
|
|
wire's copper tiles, so a later shove can locate, remove, and roll back a
|
|
specific wire without disturbing a multi-wire net's other connections.
|
|
"""
|
|
|
|
wires: list[tuple[int, list[IntPoint]]]
|
|
vias: list[IntPoint]
|
|
wire_owners: list[list[int]] = field(default_factory=list)
|
|
via_owners: list[int] = field(default_factory=list)
|
|
shove_count: int = 0
|
|
|
|
|
|
@dataclass
|
|
class _ShoveEntry:
|
|
"""One moved blocker wire, enough to restore it exactly on rollback."""
|
|
|
|
blocker_net: int
|
|
acc: _Accepted
|
|
wire_index: int
|
|
old_wire: tuple[int, list[IntPoint]]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _Room:
|
|
"""A convex free-space rectangle on one layer."""
|
|
|
|
id: int
|
|
layer: int
|
|
box: IntBox
|
|
|
|
def center(self) -> IntPoint:
|
|
return IntPoint((self.box.ll.x + self.box.ur.x) // 2, (self.box.ll.y + self.box.ur.y) // 2)
|
|
|
|
|
|
@dataclass
|
|
class _Door:
|
|
"""A passage between two rooms: a shared edge, or a via between layers."""
|
|
|
|
other: int # neighbouring room id
|
|
kind: str # "edge" | "via"
|
|
# gate geometry: for an edge door the shared segment, for a via the overlap box
|
|
lo: IntPoint
|
|
hi: IntPoint
|
|
|
|
|
|
@dataclass
|
|
class _Graph:
|
|
rooms: dict[int, _Room] = field(default_factory=dict)
|
|
doors: dict[int, list[_Door]] = field(default_factory=dict)
|
|
by_layer: dict[int, list[int]] = field(default_factory=dict)
|
|
|
|
|
|
# --- free-space decomposition ------------------------------------------------
|
|
|
|
|
|
def _free_rooms(
|
|
tree: ShapeSearchTree, outline: IntBox, layer: int, net_no: int, margin: int, half_width: int
|
|
) -> list[IntBox]:
|
|
"""Decompose the centreline-free space on ``layer`` into rectangles.
|
|
|
|
Obstacles are every other-net tile on this layer, inflated by ``margin``
|
|
(clearance + half-width) so a centreline inside a room keeps the trace clear.
|
|
The routing region is the outline shrunk by ``half_width``.
|
|
"""
|
|
region = outline.offset(-half_width)
|
|
if region.is_empty():
|
|
return []
|
|
obstacles: list[IntBox] = []
|
|
for shape in tree.all_shapes():
|
|
if net_no in (shape.net_no,) or layer not in shape.layers:
|
|
continue
|
|
infl = shape.tile.offset(margin)
|
|
clipped = infl.intersection(region)
|
|
if not clipped.is_empty() and clipped.dimension() == 2:
|
|
obstacles.append(clipped)
|
|
|
|
xs = sorted(
|
|
{region.ll.x, region.ur.x} | {o.ll.x for o in obstacles} | {o.ur.x for o in obstacles}
|
|
)
|
|
ys = sorted(
|
|
{region.ll.y, region.ur.y} | {o.ll.y for o in obstacles} | {o.ur.y for o in obstacles}
|
|
)
|
|
|
|
rooms: list[IntBox] = []
|
|
for j in range(len(ys) - 1):
|
|
y0, y1 = ys[j], ys[j + 1]
|
|
if y1 <= y0:
|
|
continue
|
|
run: IntBox | None = None
|
|
for i in range(len(xs) - 1):
|
|
x0, x1 = xs[i], xs[i + 1]
|
|
if x1 <= x0:
|
|
continue
|
|
cx, cy = (x0 + x1) // 2, (y0 + y1) // 2
|
|
blocked = any(o.contains_inside(IntPoint(cx, cy)) for o in obstacles)
|
|
if blocked:
|
|
if run is not None:
|
|
rooms.append(run)
|
|
run = None
|
|
continue
|
|
cell = IntBox(x0, y0, x1, y1)
|
|
run = cell if run is None else run.union(cell) # merge along x
|
|
if run is not None:
|
|
rooms.append(run)
|
|
return rooms
|
|
|
|
|
|
# --- doors -------------------------------------------------------------------
|
|
|
|
|
|
def _build_graph(rooms_by_layer: dict[int, list[IntBox]], trace_width: int) -> _Graph:
|
|
graph = _Graph()
|
|
next_id = 0
|
|
layer_rooms: dict[int, list[_Room]] = {}
|
|
for layer, boxes in rooms_by_layer.items():
|
|
layer_rooms[layer] = []
|
|
for box in boxes:
|
|
room = _Room(next_id, layer, box)
|
|
graph.rooms[room.id] = room
|
|
graph.doors[room.id] = []
|
|
graph.by_layer.setdefault(layer, []).append(room.id)
|
|
layer_rooms[layer].append(room)
|
|
next_id += 1
|
|
|
|
# same-layer edge doors (shared edge with overlap >= trace_width)
|
|
for rooms in layer_rooms.values():
|
|
for i in range(len(rooms)):
|
|
a = rooms[i]
|
|
for j in range(i + 1, len(rooms)):
|
|
b = rooms[j]
|
|
door = _edge_door(a.box, b.box, trace_width)
|
|
if door is not None:
|
|
lo, hi = door
|
|
graph.doors[a.id].append(_Door(b.id, "edge", lo, hi))
|
|
graph.doors[b.id].append(_Door(a.id, "edge", lo, hi))
|
|
|
|
# via doors: overlapping rooms on different layers
|
|
layers = sorted(layer_rooms)
|
|
for li in range(len(layers)):
|
|
for lj in range(li + 1, len(layers)):
|
|
for a in layer_rooms[layers[li]]:
|
|
for b in layer_rooms[layers[lj]]:
|
|
inter = a.box.intersection(b.box)
|
|
if not inter.is_empty() and inter.min_width() >= trace_width:
|
|
lo = IntPoint(inter.ll.x, inter.ll.y)
|
|
hi = IntPoint(inter.ur.x, inter.ur.y)
|
|
graph.doors[a.id].append(_Door(b.id, "via", lo, hi))
|
|
graph.doors[b.id].append(_Door(a.id, "via", lo, hi))
|
|
return graph
|
|
|
|
|
|
def _edge_door(a: IntBox, b: IntBox, trace_width: int):
|
|
"""Shared edge segment of two boxes if they abut with enough overlap."""
|
|
if a.ur.x == b.ll.x or b.ur.x == a.ll.x: # vertical shared edge
|
|
x = a.ur.x if a.ur.x == b.ll.x else a.ll.x
|
|
lo_y = max(a.ll.y, b.ll.y)
|
|
hi_y = min(a.ur.y, b.ur.y)
|
|
if hi_y - lo_y >= trace_width:
|
|
return IntPoint(x, lo_y), IntPoint(x, hi_y)
|
|
if a.ur.y == b.ll.y or b.ur.y == a.ll.y: # horizontal shared edge
|
|
y = a.ur.y if a.ur.y == b.ll.y else a.ll.y
|
|
lo_x = max(a.ll.x, b.ll.x)
|
|
hi_x = min(a.ur.x, b.ur.x)
|
|
if hi_x - lo_x >= trace_width:
|
|
return IntPoint(lo_x, y), IntPoint(hi_x, y)
|
|
return None
|
|
|
|
|
|
# --- room A* -----------------------------------------------------------------
|
|
|
|
|
|
def _rooms_containing(graph: _Graph, point: IntPoint, layers) -> list[int]:
|
|
return [
|
|
rid
|
|
for layer in layers
|
|
for rid in graph.by_layer.get(layer, [])
|
|
if graph.rooms[rid].box.contains(point)
|
|
]
|
|
|
|
|
|
def _search_rooms(graph: _Graph, starts, goals, goal_pt, via_cost):
|
|
goal_set = set(goals)
|
|
if not goal_set:
|
|
return None
|
|
open_heap: list[tuple[float, int]] = []
|
|
g_score: dict[int, float] = {}
|
|
came: dict[int, tuple[int, _Door]] = {}
|
|
for s in sorted(starts):
|
|
g_score[s] = 0.0
|
|
heapq.heappush(open_heap, (_dist(graph.rooms[s].center(), goal_pt), s))
|
|
while open_heap:
|
|
_, cur = heapq.heappop(open_heap)
|
|
if cur in goal_set:
|
|
return _reconstruct_rooms(came, cur)
|
|
base = g_score[cur]
|
|
c_center = graph.rooms[cur].center()
|
|
for door in graph.doors[cur]:
|
|
step = _dist(c_center, graph.rooms[door.other].center())
|
|
if door.kind == "via":
|
|
step += via_cost
|
|
tentative = base + step
|
|
if tentative < g_score.get(door.other, float("inf")):
|
|
came[door.other] = (cur, door)
|
|
g_score[door.other] = tentative
|
|
h = _dist(graph.rooms[door.other].center(), goal_pt)
|
|
heapq.heappush(open_heap, (tentative + h, door.other))
|
|
return None
|
|
|
|
|
|
def _reconstruct_rooms(came, cur):
|
|
seq = [(cur, None)]
|
|
while cur in came:
|
|
prev, door = came[cur]
|
|
seq.append((prev, door))
|
|
cur = prev
|
|
seq.reverse()
|
|
return seq # list of (room_id, door_used_to_enter_or_None_for_start)
|
|
|
|
|
|
def _dist(a: IntPoint, b: IntPoint) -> float:
|
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
|
|
|
|
|
# --- path realization --------------------------------------------------------
|
|
|
|
|
|
def _realize(graph, seq, start: IntPoint, goal: IntPoint, half_width, occupancy=None):
|
|
"""Turn a room sequence into per-layer orthogonal wire segments + vias.
|
|
|
|
Routes through each door's gate with an L-connector, splitting the path at
|
|
via doors. Returns ``(wires, vias)`` in board units or ``None``.
|
|
|
|
``occupancy`` (``None`` by default) enables occupancy-aware gate placement:
|
|
when it is ``(tree, net_no, clearance)`` each edge gate is projected into the
|
|
largest other-net-free sub-span of the door instead of the raw midpoint. With
|
|
no other-net routed copper on the door this reduces exactly to the midpoint
|
|
clamp, so the ``occupancy is None`` path stays byte-for-byte the old router.
|
|
"""
|
|
wires: list[tuple[int, list[IntPoint]]] = []
|
|
vias: list[IntPoint] = []
|
|
layer = graph.rooms[seq[0][0]].layer
|
|
pts = [start]
|
|
# seq[i] carries the door from room i to room i+1 (seq[-1] has None)
|
|
for i in range(len(seq) - 1):
|
|
door = seq[i][1]
|
|
if door is None:
|
|
return None
|
|
if door.kind == "edge":
|
|
if occupancy is None:
|
|
gate = _edge_gate(door, pts[-1], half_width)
|
|
else:
|
|
tree, net_no, clearance = occupancy
|
|
gate = _project_gate(door, pts[-1], half_width, clearance, tree, net_no, layer)
|
|
pts.append(gate)
|
|
else: # via: close the current-layer wire, drop a via, start next layer
|
|
gate = _via_gate(door, pts[-1], half_width)
|
|
pts.append(gate)
|
|
wires.append((layer, list(pts)))
|
|
vias.append(gate)
|
|
layer = graph.rooms[door.other].layer
|
|
pts = [gate]
|
|
pts.append(goal)
|
|
wires.append((layer, pts))
|
|
# expand each polyline of gate points into orthogonal L-connected corners
|
|
ortho: list[tuple[int, list[IntPoint]]] = []
|
|
for lyr, poly in wires:
|
|
corners = _orthogonalize(poly)
|
|
if len(corners) >= 2:
|
|
ortho.append((lyr, corners))
|
|
return ortho, vias
|
|
|
|
|
|
def _edge_gate(door: _Door, from_pt: IntPoint, half_width: int) -> IntPoint:
|
|
"""A point on the door edge, kept >= half_width from the door ends."""
|
|
if door.lo.x == door.hi.x: # vertical edge -> pick y
|
|
y = _clamp(from_pt.y, door.lo.y + half_width, door.hi.y - half_width)
|
|
return IntPoint(door.lo.x, y)
|
|
x = _clamp(from_pt.x, door.lo.x + half_width, door.hi.x - half_width)
|
|
return IntPoint(x, door.lo.y)
|
|
|
|
|
|
def _via_gate(door: _Door, from_pt: IntPoint, half_width: int) -> IntPoint:
|
|
x = _clamp(from_pt.x, door.lo.x + half_width, door.hi.x - half_width)
|
|
y = _clamp(from_pt.y, door.lo.y + half_width, door.hi.y - half_width)
|
|
return IntPoint(x, y)
|
|
|
|
|
|
def _project_gate(
|
|
door: _Door, from_pt: IntPoint, half_width: int, clearance: int, tree, net_no: int, layer: int
|
|
) -> IntPoint:
|
|
"""An occupancy-aware gate point on an edge door.
|
|
|
|
Ports ``LocateFoundConnectionAlgo``'s projection of the current from-point
|
|
onto the door shrunk by half the trace width, but subtracts the intervals
|
|
already reserved by other-net routed copper crossing the door (the
|
|
``ExpansionDoor`` section reservations, read straight from the search tree).
|
|
The trace is seated in the largest remaining free sub-span, at the point
|
|
nearest the ideal projection (tie-break: lowest coordinate). With no other-net
|
|
routed tile on the door this returns exactly :func:`_edge_gate`'s clamp.
|
|
"""
|
|
if door.lo.x == door.hi.x: # vertical edge -> choose the y coordinate
|
|
axis_lo, axis_hi, vertical = door.lo.y, door.hi.y, True
|
|
ideal = from_pt.y
|
|
else: # horizontal edge -> choose the x coordinate
|
|
axis_lo, axis_hi, vertical = door.lo.x, door.hi.x, False
|
|
ideal = from_pt.x
|
|
lo = axis_lo + half_width
|
|
hi = axis_hi - half_width
|
|
if lo > hi: # door too narrow for the trace: same midpoint fallback as _clamp
|
|
value = (axis_lo + axis_hi) // 2
|
|
else:
|
|
ideal = max(lo, min(hi, ideal))
|
|
forbidden = _door_forbidden_intervals(
|
|
door, half_width, clearance, tree, net_no, layer, vertical
|
|
)
|
|
value = _best_free_point(lo, hi, forbidden, ideal)
|
|
return IntPoint(door.lo.x, value) if vertical else IntPoint(value, door.lo.y)
|
|
|
|
|
|
def _door_forbidden_intervals(door, half_width, clearance, tree, net_no, layer, vertical):
|
|
"""Axis intervals a trace centre must avoid: one per other-net routed tile
|
|
crossing the door edge, widened by ``clearance + half_width`` on each side."""
|
|
edge_box = IntBox(door.lo.x, door.lo.y, door.hi.x, door.hi.y)
|
|
query = edge_box.offset(clearance + half_width)
|
|
reach = clearance + half_width
|
|
forbidden: list[tuple[int, int]] = []
|
|
for shape in tree.overlapping(query):
|
|
if not shape.routed or shape.net_no == net_no or layer not in shape.layers:
|
|
continue
|
|
tile = shape.tile
|
|
if vertical:
|
|
forbidden.append((tile.ll.y - reach, tile.ur.y + reach))
|
|
else:
|
|
forbidden.append((tile.ll.x - reach, tile.ur.x + reach))
|
|
return forbidden
|
|
|
|
|
|
def _best_free_point(lo: int, hi: int, forbidden, ideal: int) -> int:
|
|
"""The point in the largest free sub-interval of ``[lo, hi]`` nearest ``ideal``.
|
|
|
|
Free sub-intervals are ``[lo, hi]`` minus the (open) ``forbidden`` intervals;
|
|
the largest is chosen (tie-break: lowest coordinate) so a trace leaves the
|
|
most room for the next one, then clamped toward ``ideal`` inside it.
|
|
"""
|
|
free = _free_intervals(lo, hi, forbidden)
|
|
if not free:
|
|
return max(lo, min(hi, ideal))
|
|
a, b = max(free, key=lambda iv: (iv[1] - iv[0], -iv[0]))
|
|
return max(a, min(b, ideal))
|
|
|
|
|
|
def _free_intervals(lo: int, hi: int, forbidden) -> list[tuple[int, int]]:
|
|
"""Closed integer sub-intervals of ``[lo, hi]`` outside every open forbidden
|
|
interval. Interval boundaries stay free: a centre exactly ``clearance`` away
|
|
from a tile is allowed by the exact clearance rule."""
|
|
free: list[tuple[int, int]] = []
|
|
cur = lo
|
|
for f_lo, f_hi in sorted(forbidden):
|
|
seg_hi = min(f_lo, hi)
|
|
if cur <= hi and seg_hi >= cur:
|
|
free.append((cur, seg_hi))
|
|
cur = max(cur, min(f_hi, hi))
|
|
if cur >= hi:
|
|
break
|
|
if cur <= hi:
|
|
free.append((cur, hi))
|
|
return free
|
|
|
|
|
|
def _orthogonalize(points: list[IntPoint]) -> list[IntPoint]:
|
|
"""Connect gate points with axis-aligned segments (L-shaped between each)."""
|
|
out = [points[0]]
|
|
for p in points[1:]:
|
|
last = out[-1]
|
|
if last.x != p.x and last.y != p.y:
|
|
out.append(IntPoint(p.x, last.y)) # horizontal then vertical
|
|
if not (out[-1].x == p.x and out[-1].y == p.y):
|
|
out.append(p)
|
|
return _dedupe(out)
|
|
|
|
|
|
def _dedupe(points: list[IntPoint]) -> list[IntPoint]:
|
|
out: list[IntPoint] = []
|
|
for p in points:
|
|
if out and out[-1].x == p.x and out[-1].y == p.y:
|
|
continue
|
|
if len(out) >= 2:
|
|
a, b = out[-2], out[-1]
|
|
if (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) == 0:
|
|
out[-1] = p
|
|
continue
|
|
out.append(p)
|
|
return out
|
|
|
|
|
|
def _clamp(v: int, lo: int, hi: int) -> int:
|
|
if lo > hi:
|
|
return (lo + hi) // 2
|
|
return max(lo, min(hi, v))
|
|
|
|
|
|
# --- orchestration -----------------------------------------------------------
|
|
|
|
|
|
def route_board_rooms(
|
|
board: BasicBoard,
|
|
*,
|
|
trace_width: int,
|
|
clearance: int,
|
|
layers: list[int] | None = None,
|
|
via_cost: float = 50000.0,
|
|
shove: bool = False,
|
|
pack: bool = False,
|
|
max_shove_depth: int = 5,
|
|
shove_cap: int = 3,
|
|
via_half: int | None = None,
|
|
edge_clearance: int | None = None,
|
|
) -> ExactRouteResult:
|
|
"""Route ``board`` with the continuous expansion-room track.
|
|
|
|
``shove`` (opt-in) turns on two continuous, sub-cell upgrades that the grid
|
|
could not do: occupancy-aware gate placement (seat each trace at the best
|
|
point in a door, not its midpoint) and shove-in-rooms (when a net would be
|
|
dropped, nudge the blocking committed trace(s) aside with the exact shove
|
|
primitive and re-route, rolling every moved trace back on any failure). With
|
|
``shove=False`` every new branch is skipped and the output is byte-for-byte
|
|
the foundation router. ``max_shove_depth`` bounds the distinct blocker nets
|
|
per recovery; ``shove_cap`` bounds how often one trace may be moved.
|
|
|
|
``pack`` (opt-in, independent of ``shove``) runs the coordinated channel
|
|
packer first: groups of 2-pin nets that must cross a common obstacle gap are
|
|
assigned consistent parallel lanes across the gap and routed together, so a
|
|
channel that is physically wide enough for all of them does not drop nets to
|
|
greedy per-net gate collisions. With ``pack=False`` the pre-pass is skipped
|
|
and the output is unchanged.
|
|
"""
|
|
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)
|
|
# 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
|
|
edge = (board.outline, 0 if edge_clearance is None else edge_clearance)
|
|
margin = clearance + half_width
|
|
outline = board.bounding_box
|
|
|
|
tree = ShapeSearchTree()
|
|
owner = _counter()
|
|
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
|
|
for tile in obstacle.tiles:
|
|
b = tile.bounding_box()
|
|
if not b.is_empty():
|
|
tree.insert(TreeShape(next(owner), -1, frozenset({obstacle.layer}), b))
|
|
|
|
result = RouteResult(half_width)
|
|
via_layers = frozenset(signal_layers)
|
|
accepted: dict[int, _Accepted] = {}
|
|
shoves = 0
|
|
packed: set[int] = set()
|
|
if pack:
|
|
packed = _pack_channels(
|
|
board,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_layers,
|
|
accepted,
|
|
shove,
|
|
via_half=via_half, edge=edge)
|
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
|
if net_no in packed:
|
|
continue
|
|
pins = _net_pins(board, net_no, signal_layers)
|
|
for a, b in zip(pins, pins[1:], strict=False):
|
|
shoves += _route_connection(
|
|
net_no,
|
|
a,
|
|
b,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_cost,
|
|
via_layers,
|
|
shove,
|
|
accepted,
|
|
max_shove_depth,
|
|
shove_cap,
|
|
via_half=via_half, edge=edge)
|
|
|
|
drc_clean = tree.has_violation(clearance) is None
|
|
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean, shoves=shoves)
|
|
|
|
|
|
def _route_connection(
|
|
net_no,
|
|
a,
|
|
b,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_cost,
|
|
via_layers,
|
|
shove=False,
|
|
accepted=None,
|
|
max_shove_depth=5,
|
|
shove_cap=3,
|
|
via_half=None,
|
|
edge=None,
|
|
):
|
|
rooms_by_layer = {
|
|
layer: _free_rooms(tree, outline, layer, net_no, margin, half_width)
|
|
for layer in signal_layers
|
|
}
|
|
graph = _build_graph(rooms_by_layer, trace_width)
|
|
a_layers = [layer for layer in a.layers if layer in signal_layers]
|
|
b_layers = [layer for layer in b.layers if layer in signal_layers]
|
|
starts = _rooms_containing(graph, a.location, a_layers)
|
|
goals = _rooms_containing(graph, b.location, b_layers)
|
|
|
|
realized = None
|
|
if starts and goals:
|
|
seq = _search_rooms(graph, starts, set(goals), b.location, via_cost)
|
|
if seq is not None:
|
|
occupancy = (tree, net_no, clearance) if shove else None
|
|
realized = _realize(graph, seq, a.location, b.location, half_width, occupancy)
|
|
|
|
if realized is not None:
|
|
wires, vias = realized
|
|
boxes = _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=via_half, edge=edge)
|
|
if boxes is not None and not any(
|
|
tree.clearance_conflict(box, net_no, layer, clearance)
|
|
for box, box_layers in boxes
|
|
for layer in box_layers
|
|
):
|
|
_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
|
|
if shove:
|
|
return _shove_recover_room(
|
|
net_no,
|
|
a,
|
|
b,
|
|
tree,
|
|
result,
|
|
accepted,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
max_shove_depth,
|
|
shove_cap,
|
|
via_half=via_half, edge=edge)
|
|
return 0
|
|
|
|
|
|
def _collect_boxes(wires, vias, half_width, via_layers, outline, via_half=None, edge=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]]] = []
|
|
for layer, corners in wires:
|
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
|
if not outline.contains_box(box):
|
|
return None
|
|
if edge is not None and edge[0] is not None and not edge[0].box_clear(box, edge[1]):
|
|
return None
|
|
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 _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.
|
|
With ``shove=True`` the tiles are additionally grouped per wire into the
|
|
``accepted`` registry so a later recovery can move or roll back one wire.
|
|
"""
|
|
if not shove:
|
|
for layer, corners in wires:
|
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
|
tree.insert(TreeShape(next(owner), net_no, frozenset({layer}), box, routed=True))
|
|
for loc in vias:
|
|
vbox = IntBox(
|
|
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:
|
|
result.add_wire(net_no, layer, corners)
|
|
for loc in vias:
|
|
result.add_via(net_no, loc)
|
|
return
|
|
|
|
acc = accepted.get(net_no)
|
|
if acc is None:
|
|
acc = _Accepted(
|
|
wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, [])
|
|
)
|
|
accepted[net_no] = acc
|
|
for layer, corners in wires:
|
|
w_owners: list[int] = []
|
|
for box in PolylineShape(Polyline(corners), half_width).tiles():
|
|
oid = next(owner)
|
|
tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True))
|
|
w_owners.append(oid)
|
|
result.add_wire(net_no, layer, corners)
|
|
acc.wire_owners.append(w_owners)
|
|
for loc in vias:
|
|
vbox = IntBox(
|
|
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))
|
|
result.add_via(net_no, loc)
|
|
acc.via_owners.append(oid)
|
|
|
|
|
|
# --- coordinated channel packing ---------------------------------------------
|
|
|
|
|
|
def _long(p: IntPoint, axis: str) -> int:
|
|
"""Coordinate along a channel's length (the axis nets travel)."""
|
|
return p.y if axis == "v" else p.x
|
|
|
|
|
|
def _trans(p: IntPoint, axis: str) -> int:
|
|
"""Coordinate across a channel (the axis lanes spread over)."""
|
|
return p.x if axis == "v" else p.y
|
|
|
|
|
|
def _box_long(b: IntBox, axis: str) -> tuple[int, int]:
|
|
return (b.ll.y, b.ur.y) if axis == "v" else (b.ll.x, b.ur.x)
|
|
|
|
|
|
def _box_trans(b: IntBox, axis: str) -> tuple[int, int]:
|
|
return (b.ll.x, b.ur.x) if axis == "v" else (b.ll.y, b.ur.y)
|
|
|
|
|
|
def _outline_trans(outline: IntBox, axis: str) -> tuple[int, int]:
|
|
return (outline.min_x, outline.max_x) if axis == "v" else (outline.min_y, outline.max_y)
|
|
|
|
|
|
def _axis_point(long_val: int, trans_val: int, axis: str) -> IntPoint:
|
|
"""Build a point from (length-coord, transverse-coord) for the given axis."""
|
|
return IntPoint(trans_val, long_val) if axis == "v" else IntPoint(long_val, trans_val)
|
|
|
|
|
|
def _lane_polyline(
|
|
top: IntPoint, bot: IntPoint, lane: int, trunk_top: int, trunk_bot: int, axis: str
|
|
) -> list[IntPoint]:
|
|
"""A pad -> trunk -> lane -> trunk -> pad orthogonal trace.
|
|
|
|
Each net converges to its lane on a *staggered* trunk line (``trunk_top`` /
|
|
``trunk_bot``, a distinct length-coordinate per net just outside the gap), runs
|
|
straight through the channel in its lane, then diverges to the far pad on the
|
|
other trunk. Distinct trunk lines keep the fan-in horizontals from overlapping
|
|
when pads spread wider than the gap. ``top`` is the pin with the larger
|
|
length-coordinate. Collinear/duplicate points collapse."""
|
|
tt = _trans(top, axis)
|
|
tb = _trans(bot, axis)
|
|
pts = [
|
|
top,
|
|
_axis_point(trunk_top, tt, axis),
|
|
_axis_point(trunk_top, lane, axis),
|
|
_axis_point(trunk_bot, lane, axis),
|
|
_axis_point(trunk_bot, tb, axis),
|
|
bot,
|
|
]
|
|
return _dedupe(pts)
|
|
|
|
|
|
def _wall_rows(obstacles: list[IntBox], axis: str) -> list[list[IntBox]]:
|
|
"""Cluster obstacle boxes into walls whose blocked length-ranges overlap; each
|
|
cluster is one barrier that nets cross transversely through its gaps."""
|
|
boxes = sorted(obstacles, key=lambda b: (_box_long(b, axis), _box_trans(b, axis)))
|
|
used = [False] * len(boxes)
|
|
rows: list[list[IntBox]] = []
|
|
for i, b in enumerate(boxes):
|
|
if used[i]:
|
|
continue
|
|
used[i] = True
|
|
cluster = [b]
|
|
lo, hi = _box_long(b, axis)
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for j, c in enumerate(boxes):
|
|
if used[j]:
|
|
continue
|
|
clo, chi = _box_long(c, axis)
|
|
if clo < hi and chi > lo:
|
|
used[j] = True
|
|
cluster.append(c)
|
|
lo, hi = min(lo, clo), max(hi, chi)
|
|
changed = True
|
|
rows.append(cluster)
|
|
return rows
|
|
|
|
|
|
def _channel_groups(obstacles, cands, axis, trace_width):
|
|
"""Find (gap_lo, gap_hi, [net_no, ...]) channels on one axis: a gap between two
|
|
obstacles in a wall row, and the >= 2 candidate nets that must cross that wall
|
|
and are nearest this gap. Only gaps bounded by an obstacle on both sides count
|
|
(an open board edge is not a channel)."""
|
|
groups = []
|
|
for cluster in _wall_rows(obstacles, axis):
|
|
blo = max(_box_long(b, axis)[0] for b in cluster)
|
|
bhi = min(_box_long(b, axis)[1] for b in cluster)
|
|
if bhi <= blo:
|
|
continue # no length-band the whole cluster blocks in common
|
|
occ = sorted(_box_trans(b, axis) for b in cluster)
|
|
merged: list[tuple[int, int]] = []
|
|
for lo, hi in occ:
|
|
if merged and lo <= merged[-1][1]:
|
|
merged[-1] = (merged[-1][0], max(merged[-1][1], hi))
|
|
else:
|
|
merged.append((lo, hi))
|
|
gaps = [
|
|
(merged[k][1], merged[k + 1][0])
|
|
for k in range(len(merged) - 1)
|
|
if merged[k + 1][0] - merged[k][1] >= trace_width
|
|
]
|
|
if not gaps:
|
|
continue
|
|
crossing = []
|
|
for net_no, (pa, pb) in cands.items():
|
|
la, lb = _long(pa, axis), _long(pb, axis)
|
|
if (la <= blo and lb >= bhi) or (lb <= blo and la >= bhi):
|
|
crossing.append(net_no)
|
|
for gap_lo, gap_hi in gaps:
|
|
members = []
|
|
for net_no in crossing:
|
|
pa, pb = cands[net_no]
|
|
center = (_trans(pa, axis) + _trans(pb, axis)) / 2
|
|
nearest = min(gaps, key=lambda g: abs((g[0] + g[1]) / 2 - center))
|
|
if nearest == (gap_lo, gap_hi):
|
|
members.append(net_no)
|
|
if len(members) >= 2:
|
|
groups.append((gap_lo, gap_hi, blo, bhi, sorted(members)))
|
|
return groups
|
|
|
|
|
|
def _boxes_clear(boxes, net_no, placed, clearance) -> bool:
|
|
"""True if none of ``boxes`` come within ``clearance`` of a different-net box in
|
|
``placed`` (list of ``(net_no, layer, box)``) -- the in-group lane-vs-lane check
|
|
that the search tree cannot do until copper is committed."""
|
|
for box, box_layers in boxes:
|
|
expanded = box.offset(clearance)
|
|
for other_net, other_layer, other_box in placed:
|
|
if other_net == net_no or other_layer not in box_layers:
|
|
continue
|
|
if other_box.overlaps(expanded):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _pack_channels(
|
|
board,
|
|
tree,
|
|
result,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
trace_width,
|
|
via_layers,
|
|
accepted,
|
|
shove,
|
|
via_half=None,
|
|
edge=None,
|
|
) -> set[int]:
|
|
"""Coordinated multi-trace channel packing (opt-in pre-pass).
|
|
|
|
Greedy per-net routing sends each net through a shared constriction on its own
|
|
independently chosen path; the paths collide and some nets drop even when the
|
|
gap is physically wide enough for all of them. This pass groups 2-pin nets that
|
|
must cross a common obstacle gap, assigns each a parallel lane across the gap's
|
|
usable width -- sorted by pad position, spacing >= trace_width + clearance so
|
|
lanes never conflict and, being pad-ordered, never cross -- and realizes each as
|
|
an orthogonal converge/straight/diverge trace verified exactly DRC-clean against
|
|
the search tree. A net whose lane trace is not clean (e.g. it clips a pad) is
|
|
left for the normal loop. Returns the fully-routed net numbers so the caller
|
|
skips them. Scope: single-layer 2-pin nets through one axis-aligned gap."""
|
|
routed: set[int] = set()
|
|
spacing_min = trace_width + clearance
|
|
cands: dict[int, tuple[int, IntPoint, IntPoint]] = {}
|
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
|
pins = _net_pins(board, net_no, signal_layers)
|
|
if len(pins) != 2:
|
|
continue
|
|
shared = [
|
|
layer for layer in pins[0].layers if layer in pins[1].layers and layer in signal_layers
|
|
]
|
|
if not shared:
|
|
continue
|
|
cands[net_no] = (min(shared), pins[0].location, pins[1].location)
|
|
if len(cands) < 2:
|
|
return routed
|
|
|
|
obstacles_by_layer: dict[int, list[IntBox]] = {layer: [] for layer in signal_layers}
|
|
for obstacle in board.get_obstacle_areas():
|
|
if obstacle.layer not in signal_layers:
|
|
continue
|
|
for tile in obstacle.tiles:
|
|
box = tile.bounding_box()
|
|
if not box.is_empty():
|
|
obstacles_by_layer[obstacle.layer].append(box)
|
|
|
|
for layer in sorted(signal_layers):
|
|
obstacles = obstacles_by_layer.get(layer, [])
|
|
if not obstacles:
|
|
continue
|
|
layer_cands = {net_no: (pa, pb) for net_no, (lyr, pa, pb) in cands.items() if lyr == layer}
|
|
if len(layer_cands) < 2:
|
|
continue
|
|
for axis in ("v", "h"):
|
|
for gap_lo, gap_hi, blo, bhi, members in _channel_groups(
|
|
obstacles, layer_cands, axis, trace_width
|
|
):
|
|
members = [n for n in members if n not in routed]
|
|
if len(members) < 2:
|
|
continue
|
|
placed = _pack_group(
|
|
members,
|
|
layer_cands,
|
|
axis,
|
|
layer,
|
|
gap_lo,
|
|
gap_hi,
|
|
blo,
|
|
bhi,
|
|
tree,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
spacing_min,
|
|
via_layers,
|
|
outline,
|
|
via_half=via_half, edge=edge)
|
|
if placed is None:
|
|
continue # could not pack every member cleanly -> leave to greedy
|
|
for net_no, wires in placed:
|
|
_commit(
|
|
net_no,
|
|
wires,
|
|
[],
|
|
tree,
|
|
result,
|
|
owner,
|
|
half_width,
|
|
via_layers,
|
|
shove,
|
|
accepted,
|
|
via_half=via_half)
|
|
routed.add(net_no)
|
|
return routed
|
|
|
|
|
|
def _pack_group(
|
|
members,
|
|
cands,
|
|
axis,
|
|
layer,
|
|
gap_lo,
|
|
gap_hi,
|
|
blo,
|
|
bhi,
|
|
tree,
|
|
half_width,
|
|
clearance,
|
|
margin,
|
|
spacing_min,
|
|
via_layers,
|
|
outline,
|
|
via_half=None,
|
|
edge=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
|
|
DRC-clean, else ``None``. All-or-nothing: a partial pack could leave the
|
|
channel worse than greedy, so the group is committed only if it packs whole."""
|
|
count = len(members)
|
|
lo = gap_lo + margin
|
|
hi = gap_hi - margin
|
|
band = hi - lo
|
|
if band < (count - 1) * spacing_min:
|
|
return None # gap not physically wide enough for this many lanes
|
|
|
|
# per net: (top pin, bottom pin) by length-coordinate; top has the larger value
|
|
ends = {}
|
|
for net_no in members:
|
|
pa, pb = cands[net_no]
|
|
ends[net_no] = (pa, pb) if _long(pa, axis) >= _long(pb, axis) else (pb, pa)
|
|
|
|
# lanes in gap order, preserving pad order so lanes never cross
|
|
lane_order = sorted(members, key=lambda n: (_trans(ends[n][0], axis), n))
|
|
lane_of = {
|
|
net_no: (lo if count == 1 else lo + round(band * i / (count - 1)))
|
|
for i, net_no in enumerate(lane_order)
|
|
}
|
|
|
|
# staggered trunks: the widest fan-in horizontal sits nearest the gap mouth so
|
|
# it passes under the shorter nets' descents (a monotone, crossing-free fan)
|
|
top_rank = {
|
|
n: r
|
|
for r, n in enumerate(
|
|
sorted(members, key=lambda n: (-abs(_trans(ends[n][0], axis) - lane_of[n]), n))
|
|
)
|
|
}
|
|
bot_rank = {
|
|
n: r
|
|
for r, n in enumerate(
|
|
sorted(members, key=lambda n: (-abs(_trans(ends[n][1], axis) - lane_of[n]), n))
|
|
)
|
|
}
|
|
top_pad = min(_long(ends[n][0], axis) for n in members)
|
|
bot_pad = max(_long(ends[n][1], axis) for n in members)
|
|
if bhi + margin + (count - 1) * spacing_min > top_pad - margin:
|
|
return None # not enough room above the gap to stack the fan-in trunks
|
|
if blo - margin - (count - 1) * spacing_min < bot_pad + margin:
|
|
return None # not enough room below the gap
|
|
|
|
placed_boxes: list[tuple[int, int, IntBox]] = []
|
|
to_commit = []
|
|
for net_no in sorted(members):
|
|
top_pt, bot_pt = ends[net_no]
|
|
trunk_top = bhi + margin + top_rank[net_no] * spacing_min
|
|
trunk_bot = blo - margin - bot_rank[net_no] * spacing_min
|
|
corners = _lane_polyline(top_pt, bot_pt, lane_of[net_no], trunk_top, trunk_bot, axis)
|
|
if len(corners) < 2:
|
|
return None
|
|
wires = [(layer, corners)]
|
|
boxes = _collect_boxes(wires, [], half_width, via_layers, outline, via_half=via_half, edge=edge)
|
|
if boxes is None:
|
|
return None
|
|
if any(
|
|
tree.clearance_conflict(box, net_no, box_layer, clearance)
|
|
for box, box_layers in boxes
|
|
for box_layer in box_layers
|
|
):
|
|
return None
|
|
if not _boxes_clear(boxes, net_no, placed_boxes, clearance):
|
|
return None
|
|
for box, box_layers in boxes:
|
|
for box_layer in box_layers:
|
|
placed_boxes.append((net_no, box_layer, box))
|
|
to_commit.append((net_no, wires))
|
|
return to_commit
|
|
|
|
|
|
# --- shove-in-rooms recovery -------------------------------------------------
|
|
|
|
|
|
def _shove_recover_room(
|
|
net_no,
|
|
a,
|
|
b,
|
|
tree,
|
|
result,
|
|
accepted,
|
|
owner,
|
|
outline,
|
|
signal_layers,
|
|
half_width,
|
|
clearance,
|
|
max_shove_depth,
|
|
shove_cap,
|
|
via_half=None,
|
|
edge=None,
|
|
) -> int:
|
|
"""Recover a dropped net by nudging the committed trace(s) that block it.
|
|
|
|
MVP (mirrors the exact track): a straight, axis-aligned 2-pin net on one
|
|
signal layer. Its straight copper is the corridor; every routed different-net
|
|
trace crossing it is shoved perpendicular with the shared exact primitive and
|
|
re-verified against the tree, then the net is placed straight. A static tile
|
|
(pad/keepout) in the corridor, or a blocker that will not move cleanly, aborts
|
|
the whole attempt and rolls every moved trace back to its exact original
|
|
tiles and wires. Returns the number of successful moves (0 = still dropped).
|
|
"""
|
|
if a.location.x != b.location.x and a.location.y != b.location.y:
|
|
return 0 # only straight nets in this MVP
|
|
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 = trace_tiles(corners, half_width)
|
|
|
|
blockers: dict[int, set[int]] = {}
|
|
for box in d_boxes:
|
|
expanded = box.offset(clearance)
|
|
for shape in tree.overlapping(expanded):
|
|
if shape.net_no == net_no or layer not in shape.layers:
|
|
continue
|
|
if not shape.tile.overlaps(expanded):
|
|
continue
|
|
if not shape.routed:
|
|
return 0 # a pad or keepout sits in the corridor: cannot shove it
|
|
blockers.setdefault(shape.net_no, set()).add(shape.owner)
|
|
if not blockers or len(blockers) > max_shove_depth:
|
|
return 0
|
|
|
|
journal: list[_ShoveEntry] = []
|
|
for blocker_net in sorted(blockers):
|
|
acc = accepted.get(blocker_net) if accepted is not None else None
|
|
if acc is None or acc.shove_count >= shove_cap:
|
|
_rollback_room(journal, tree, owner, half_width)
|
|
return 0
|
|
if not _shove_blocker(
|
|
blocker_net, acc, corners, layer, tree, owner, half_width, clearance, outline, journal
|
|
):
|
|
_rollback_room(journal, tree, owner, half_width)
|
|
return 0
|
|
|
|
# the corridor must now be clean and inside the outline before we commit it
|
|
if any(not outline.contains_box(box) for box in d_boxes) or any(
|
|
tree.clearance_conflict(box, net_no, layer, clearance) for box in d_boxes
|
|
):
|
|
_rollback_room(journal, tree, owner, half_width)
|
|
return 0
|
|
|
|
owners = force_place_owners(net_no, layer, corners, tree, owner, half_width)
|
|
# Reuse this net's existing registry if it already committed connections, so
|
|
# ``wire_owners`` stays parallel to ``wires``. Clobbering with a fresh
|
|
# ``_Accepted`` here would leave ``wire_owners`` shorter than ``wires`` (its
|
|
# ``result.wires[net_no]`` list already holds the earlier connections),
|
|
# desyncing the two lists and making a later shove remove the wrong wire's
|
|
# copper from the tree while it stays in the emitted route.
|
|
acc = accepted.get(net_no) if accepted is not None else None
|
|
if acc is None:
|
|
acc = _Accepted(
|
|
wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, [])
|
|
)
|
|
accepted[net_no] = acc
|
|
result.add_wire(net_no, layer, corners)
|
|
acc.wire_owners.append(owners)
|
|
return len(journal) + 1
|
|
|
|
|
|
def _shove_blocker(
|
|
blocker_net, acc, d_corners, layer, tree, owner, half_width, clearance, outline, journal
|
|
) -> bool:
|
|
"""Move ``blocker_net``'s conflicting wire clear of the corridor ``d_corners``.
|
|
|
|
Finds the single wire of the blocker whose axis-aligned run crosses the
|
|
corridor, removes its tiles, and walks the perpendicular displacement ladder
|
|
until the reshaped wire places cleanly (outline + exact clearance). On success
|
|
the move is journaled for rollback; on failure the wire's original tiles are
|
|
restored and ``False`` is returned.
|
|
"""
|
|
d_boxes = trace_tiles(d_corners, half_width)
|
|
for wire_index, (seg_layer, corners) in enumerate(acc.wires):
|
|
if seg_layer != layer:
|
|
continue
|
|
seg_index, horizontal = conflicting_segment(corners, d_boxes, half_width, clearance)
|
|
if seg_index is None:
|
|
continue
|
|
|
|
old_owners = list(acc.wire_owners[wire_index])
|
|
old_wire = (seg_layer, list(corners))
|
|
for oid in old_owners:
|
|
tree.remove_owner(oid)
|
|
|
|
for dx, dy in shove_displacements(
|
|
corners, seg_index, horizontal, d_corners, half_width, clearance
|
|
):
|
|
if dx == 0 and dy == 0:
|
|
continue
|
|
new_corners = shove_segment(list(corners), seg_index, dx, dy)
|
|
new_owners = place_trace_owners(
|
|
blocker_net, seg_layer, new_corners, tree, owner, half_width, clearance, outline
|
|
)
|
|
if new_owners is not None:
|
|
journal.append(_ShoveEntry(blocker_net, acc, wire_index, old_wire))
|
|
acc.wires[wire_index] = (seg_layer, new_corners)
|
|
acc.wire_owners[wire_index] = new_owners
|
|
acc.shove_count += 1
|
|
return True
|
|
|
|
# no clean displacement: restore the wire's original tiles and give up
|
|
acc.wire_owners[wire_index] = force_place_owners(
|
|
blocker_net, seg_layer, list(corners), tree, owner, half_width
|
|
)
|
|
return False
|
|
return False
|
|
|
|
|
|
def _rollback_room(journal, tree, owner, half_width) -> int:
|
|
"""Undo every journaled shove, restoring exact original tiles and wires."""
|
|
for entry in reversed(journal):
|
|
for oid in entry.acc.wire_owners[entry.wire_index]:
|
|
tree.remove_owner(oid)
|
|
layer, old_corners = entry.old_wire
|
|
entry.acc.wire_owners[entry.wire_index] = force_place_owners(
|
|
entry.blocker_net, layer, old_corners, tree, owner, half_width
|
|
)
|
|
entry.acc.wires[entry.wire_index] = (layer, old_corners)
|
|
entry.acc.shove_count -= 1
|
|
return 0
|
|
|
|
|
|
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 _counter():
|
|
i = 0
|
|
while True:
|
|
yield i
|
|
i += 1
|