From e67a11c833879abe11e973508d152662dffc5d38 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 13 Jul 2026 10:27:01 -0600 Subject: [PATCH] Add shared orthogonal shove primitives module Factor the exact router's shove geometry into route/shove.py: the conflicting-segment finder and displacement ladder are re-exported from exact_router (kept byte-unchanged so its shove count and geometry stay identical), and a room-oriented trace placement helper is added that returns the inserted tile owner ids for a journaled rollback. --- src/freeroute/route/shove.py | 124 +++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 src/freeroute/route/shove.py diff --git a/src/freeroute/route/shove.py b/src/freeroute/route/shove.py new file mode 100644 index 0000000..76a2ca3 --- /dev/null +++ b/src/freeroute/route/shove.py @@ -0,0 +1,124 @@ +"""Shared orthogonal shove primitives for the exact and room routers. + +The exact-geometry router (:mod:`freeroute.route.exact_router`) grew the first +shove implementation; the continuous room router reuses its geometry so both +tracks agree on how a blocking trace's conflicting segment is found, how the +perpendicular displacement ladder is generated, and how a reshaped trace is +verified before it is committed. + +To keep the exact track byte-for-byte identical (its tests assert an exact shove +count and geometry), the two low-level geometry helpers still live in +``exact_router`` and are re-exported here rather than moved. This module adds the +room-specific placement helper, which returns the inserted tile owner ids: the +room engine mutates already-committed nets, so it needs those ids for a real +journaled rollback (the exact track's rollback is a no-op because it only ever +verifies a shoved trace in isolation). +""" + +from __future__ import annotations + +from collections.abc import Iterator + +from freeroute.board.search_tree import ShapeSearchTree, TreeShape +from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape + +from .exact_router import _conflicting_segment as conflicting_segment + +__all__ = [ + "conflicting_segment", + "trace_tiles", + "shove_displacements", + "place_trace_owners", + "force_place_owners", +] + + +def trace_tiles(corners: list[IntPoint], half_width: int) -> list[IntBox]: + """The copper tiles of the trace centreline ``corners`` at ``half_width``.""" + return PolylineShape(Polyline(corners), half_width).tiles() + + +def shove_displacements( + corners: list[IntPoint], + seg_index: int, + horizontal: bool, + d_corners: list[IntPoint], + half_width: int, + clearance: int, +) -> list[tuple[int, int]]: + """The perpendicular displacement ladder for shoving segment ``seg_index``. + + Mirrors the exact router's ladder: displace the conflicting segment away + from the crossing net at ``d_corners`` (its straight copper) by just enough + to clear it (``2*half_width + clearance`` centre-to-centre), then step out in + ``half_width + clearance`` increments. The "away" side is tried first (past + the far edge of the crossing net), then the near side, so the search is + deterministic and integer-only. + """ + d0, d1 = d_corners[0], d_corners[1] + margin = 2 * half_width + clearance + fine = half_width + clearance + ref = corners[seg_index] + out: list[tuple[int, int]] = [] + if horizontal: # displace in y to clear the crossing net's y-span + lo, hi = min(d0.y, d1.y), max(d0.y, d1.y) + for extra in range(6): + out.append((0, (hi + margin) - ref.y + extra * fine)) + out.append((0, (lo - margin) - ref.y - extra * fine)) + else: # displace in x + lo, hi = min(d0.x, d1.x), max(d0.x, d1.x) + for extra in range(6): + out.append(((hi + margin) - ref.x + extra * fine, 0)) + out.append(((lo - margin) - ref.x - extra * fine, 0)) + return out + + +def place_trace_owners( + net_no: int, + layer: int, + corners: list[IntPoint], + tree: ShapeSearchTree, + owner: Iterator[int], + half_width: int, + clearance: int, + outline: IntBox, +) -> list[int] | None: + """Verify a trace's copper and insert it, returning the inserted owner ids. + + The trace is accepted only if every tile stays inside ``outline`` and clears + every other net exactly (via ``tree.clearance_conflict``). On rejection + nothing is inserted and ``None`` is returned. + """ + boxes = PolylineShape(Polyline(corners), half_width).tiles() + for box in boxes: + if not outline.contains_box(box): + return None + if tree.clearance_conflict(box, net_no, layer, clearance): + return None + owners: list[int] = [] + for box in boxes: + oid = next(owner) + tree.insert(TreeShape(oid, net_no, frozenset({layer}), box, routed=True)) + owners.append(oid) + return owners + + +def force_place_owners( + net_no: int, + layer: int, + corners: list[IntPoint], + tree: ShapeSearchTree, + owner: Iterator[int], + half_width: int, +) -> list[int]: + """Insert a trace's copper without any check, returning the owner ids. + + Used only to restore an already-verified original trace during rollback: its + tiles were clean before the attempt, so no re-verification is needed. + """ + 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)) + owners.append(oid) + return owners