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.
This commit is contained in:
Ryan Malloy 2026-07-13 10:27:01 -06:00
parent faf2df42bc
commit e67a11c833

View File

@ -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