"""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, max_shove_depth: int = 5, shove_cap: int = 3, ) -> 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. """ 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) accepted: dict[int, _Accepted] = {} shoves = 0 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): 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, ) 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, ): 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) 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) 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, ) return 0 def _collect_boxes(wires, vias, half_width, via_layers, outline): """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 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)) return boxes def _commit(net_no, wires, vias, tree, result, owner, half_width, via_layers, shove, accepted): """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 - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width ) 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 - half_width, loc.y - half_width, loc.x + half_width, loc.y + half_width ) 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) # --- 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, ) -> 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) acc = _Accepted( wires=result.wires.setdefault(net_no, []), vias=result.vias.setdefault(net_no, []) ) result.add_wire(net_no, layer, corners) acc.wire_owners.append(owners) accepted[net_no] = acc 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