Add continuous expansion-room routing track
A third engine (engine='room') that replaces the fixed routing grid with FreeRouting's free-space rooms. Per layer, the space not occupied by items (inflated by the routing clearance) is decomposed into exact rectangles via coordinate compression (the orthogonal analogue of CompleteFreeSpaceExpansionRoom); adjacent rooms share an edge door, and a via door joins overlapping rooms on two layers. The maze search is an A* over rooms-through-doors (cost = geometric distance + via cost), so a path may cross a door anywhere along its width rather than at a quantized cell. The found room sequence is realized into an exact orthogonal Polyline through the door mid-gates and verified DRC-clean against the ShapeSearchTree, dropping the net if it cannot be made exactly clean. Maps to autoroute/: ExpansionRoom / CompleteFreeSpaceExpansionRoom (rooms), ExpansionDoor / TargetItemExpansionDoor (doors), MazeSearchAlgo / AutorouteEngine (search), LocateFoundConnectionAlgo (realization). Foundation scope: rooms are axis-aligned rectangles (not general Simplex tiles); realization uses an L-connector through door mid-gates. 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. Routes simple/crossing (with a via) and all four multi-pin nets of the real KiCad board, DRC-clean.
This commit is contained in:
parent
e58f63e0f1
commit
6a24ffc540
@ -18,11 +18,14 @@ from .exact_router import ExactRouteResult, route_board_exact
|
|||||||
from .grid_router import GridRouter, RouteResult, route_board
|
from .grid_router import GridRouter, RouteResult, route_board
|
||||||
from .pipeline import (
|
from .pipeline import (
|
||||||
build_exact_routing_result,
|
build_exact_routing_result,
|
||||||
|
build_rooms_routing_result,
|
||||||
build_routing_result,
|
build_routing_result,
|
||||||
route,
|
route,
|
||||||
route_dsn_board,
|
route_dsn_board,
|
||||||
route_dsn_board_exact,
|
route_dsn_board_exact,
|
||||||
|
route_dsn_board_rooms,
|
||||||
)
|
)
|
||||||
|
from .room_router import route_board_rooms
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GridRouter",
|
"GridRouter",
|
||||||
@ -35,4 +38,7 @@ __all__ = [
|
|||||||
"route_board_exact",
|
"route_board_exact",
|
||||||
"build_exact_routing_result",
|
"build_exact_routing_result",
|
||||||
"route_dsn_board_exact",
|
"route_dsn_board_exact",
|
||||||
|
"route_board_rooms",
|
||||||
|
"build_rooms_routing_result",
|
||||||
|
"route_dsn_board_rooms",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -18,6 +18,7 @@ from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
|||||||
|
|
||||||
from .exact_router import ExactRouteResult, route_board_exact
|
from .exact_router import ExactRouteResult, route_board_exact
|
||||||
from .grid_router import RouteResult, route_board
|
from .grid_router import RouteResult, route_board
|
||||||
|
from .room_router import route_board_rooms
|
||||||
|
|
||||||
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
||||||
|
|
||||||
@ -139,20 +140,44 @@ def build_exact_routing_result(
|
|||||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|
||||||
|
def route_dsn_board_rooms(
|
||||||
|
dsn: DsnBoard, *, layers: list[int] | None = None
|
||||||
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
|
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
||||||
|
return the result, the scale, and layer names."""
|
||||||
|
board = build_board(dsn)
|
||||||
|
scale = max(dsn.resolution.value, 1)
|
||||||
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
||||||
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
||||||
|
rooms = route_board_rooms(
|
||||||
|
board, trace_width=width_board, clearance=clearance_board, layers=layers
|
||||||
|
)
|
||||||
|
return rooms, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
|
|
||||||
|
def build_rooms_routing_result(dsn: DsnBoard, *, layers: list[int] | None = None) -> RoutingResult:
|
||||||
|
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
||||||
|
rooms, scale, layer_names = route_dsn_board_rooms(dsn, layers=layers)
|
||||||
|
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
||||||
|
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid", shove: bool = False
|
dsn_text: str, *, layers: list[int] | None = None, engine: str = "grid", shove: bool = False
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Route a Specctra DSN string and return the routed SES string.
|
"""Route a Specctra DSN string and return the routed SES string.
|
||||||
|
|
||||||
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
||||||
rip-up grid MVP — highest coverage, kept for backward compatibility) or
|
rip-up grid MVP — highest coverage, kept for backward compatibility),
|
||||||
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
||||||
it succeeds). ``shove`` (exact engine only) tries moving existing traces
|
it succeeds), or ``"room"`` (continuous expansion-room — exact free-space
|
||||||
aside to recover dropped nets.
|
decomposition, routes off-grid channels the grid/exact tracks cannot).
|
||||||
|
``shove`` (exact engine only) tries moving existing traces aside.
|
||||||
"""
|
"""
|
||||||
dsn = parse_dsn(dsn_text)
|
dsn = parse_dsn(dsn_text)
|
||||||
if engine == "exact":
|
if engine == "exact":
|
||||||
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
result = build_exact_routing_result(dsn, layers=layers, shove=shove)
|
||||||
|
elif engine == "room":
|
||||||
|
result = build_rooms_routing_result(dsn, layers=layers)
|
||||||
else:
|
else:
|
||||||
result = build_routing_result(dsn, layers=layers)
|
result = build_routing_result(dsn, layers=layers)
|
||||||
return write_ses(dsn, result)
|
return write_ses(dsn, result)
|
||||||
|
|||||||
470
src/freeroute/route/room_router.py
Normal file
470
src/freeroute/route/room_router.py
Normal file
@ -0,0 +1,470 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
from .exact_router import ExactRouteResult, _routable_nets
|
||||||
|
from .grid_router import RouteResult
|
||||||
|
|
||||||
|
__all__ = ["route_board_rooms"]
|
||||||
|
|
||||||
|
|
||||||
|
@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):
|
||||||
|
"""Turn a room sequence into per-layer orthogonal wire segments + vias.
|
||||||
|
|
||||||
|
Routes through each door's mid-gate with an L-connector, splitting the path
|
||||||
|
at via doors. Returns ``(wires, vias)`` in board units or ``None``.
|
||||||
|
"""
|
||||||
|
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":
|
||||||
|
gate = _edge_gate(door, pts[-1], half_width)
|
||||||
|
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 _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,
|
||||||
|
) -> ExactRouteResult:
|
||||||
|
"""Route ``board`` with the continuous expansion-room track (foundation)."""
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
for net_no in sorted(_routable_nets(board, signal_layers)):
|
||||||
|
pins = _net_pins(board, net_no, signal_layers)
|
||||||
|
for a, b in zip(pins, pins[1:], strict=False):
|
||||||
|
_route_connection(
|
||||||
|
net_no,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
tree,
|
||||||
|
result,
|
||||||
|
owner,
|
||||||
|
outline,
|
||||||
|
signal_layers,
|
||||||
|
half_width,
|
||||||
|
clearance,
|
||||||
|
margin,
|
||||||
|
trace_width,
|
||||||
|
via_cost,
|
||||||
|
via_layers,
|
||||||
|
)
|
||||||
|
|
||||||
|
drc_clean = tree.has_violation(clearance) is None
|
||||||
|
return ExactRouteResult(result=result, tree=tree, drc_clean=drc_clean)
|
||||||
|
|
||||||
|
|
||||||
|
def _route_connection(
|
||||||
|
net_no,
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
tree,
|
||||||
|
result,
|
||||||
|
owner,
|
||||||
|
outline,
|
||||||
|
signal_layers,
|
||||||
|
half_width,
|
||||||
|
clearance,
|
||||||
|
margin,
|
||||||
|
trace_width,
|
||||||
|
via_cost,
|
||||||
|
via_layers,
|
||||||
|
):
|
||||||
|
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)
|
||||||
|
if not starts or not goals:
|
||||||
|
return
|
||||||
|
seq = _search_rooms(graph, starts, set(goals), b.location, via_cost)
|
||||||
|
if seq is None:
|
||||||
|
return
|
||||||
|
realized = _realize(graph, seq, a.location, b.location, half_width)
|
||||||
|
if realized is None:
|
||||||
|
return
|
||||||
|
wires, vias = realized
|
||||||
|
|
||||||
|
# verify every piece is exactly DRC-clean before committing
|
||||||
|
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
|
||||||
|
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
|
||||||
|
)
|
||||||
|
boxes.append((vbox, via_layers))
|
||||||
|
if any(
|
||||||
|
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||||
|
for box, box_layers in boxes
|
||||||
|
for layer in box_layers
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
for box, box_layers in boxes:
|
||||||
|
tree.insert(TreeShape(next(owner), net_no, box_layers, box, routed=True))
|
||||||
|
for layer, corners in wires:
|
||||||
|
result.add_wire(net_no, layer, corners)
|
||||||
|
for loc in vias:
|
||||||
|
result.add_via(net_no, loc)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
Loading…
x
Reference in New Issue
Block a user