Files
rpmroute/src/freeroute/board/search_tree.py
T
rsp2k 354c89a8d5 Route with copper zones, and let vias into no-tracks areas
freeroute ignored the DSN's planes (KiCad's zones), so it never knew that
pads in a pour are joined by it. On the rc4 power stage those pads sit
inside "keep router off" areas as well, and every connection to them was
dropped without a word.

- Read (plane <net> <shape>) and put each zone on the board as a
  ConductionArea of its net. Zones are not obstacles: KiCad refills them
  around other nets' copper.
- The grid groups each net's pads and zone pieces that copper already
  joins (a pad in a zone, two zones sharing a pad) and routes only between
  groups. A connection may end on a zone by a via inside its copper, or by
  a track where tracks are allowed.
- wire_keepout now blocks traces only, as KiCad's "no tracks" rule area
  does; via_keepout only vias; keepout both, in the grid and in the exact
  verifier.

rc4 power stage, exact engine, resolution 2: 2 unconnected, down from 8
(FreeRouting JAR: 12), no DRC errors.
2026-09-27 11:54:04 -06:00

191 lines
8.3 KiB
Python

"""``ShapeSearchTree`` — a spatial index over item shapes with exact overlap.
Ports the query role of ``board/ShapeSearchTree*.java``. It replaces the exact
router's occupancy grid: instead of rasterizing shapes into cells, it indexes
each item's convex tiles (:class:`~freeroute.geometry.IntBox`) and answers
overlap / region queries with a broad phase (a bounding-box spatial hash) and an
**exact** narrow phase (exact ``IntBox`` intersection). Insert and remove keep
the index in step as traces are added.
**Arithmetic model: exact.** The narrow-phase overlap and clearance tests are
exact integer ``IntBox`` intersections; no grid quantization. (The tiles happen
to be axis-aligned boxes because the exact router routes orthogonally; the index
itself is shape-agnostic — any tile exposing ``bounding_box()`` and an exact
``intersection`` works.)
"""
from __future__ import annotations
from dataclasses import dataclass
from freeroute.geometry import IntBox, Simplex, overlaps_2d, segment_octagon
__all__ = ["TreeShape", "ShapeSearchTree"]
@dataclass(frozen=True)
class TreeShape:
"""One convex tile of an item, tagged with its net, layers and owner id.
``routed`` distinguishes router-produced copper (traces, vias) from static
board items (pads, keepouts). The routing DRC only counts violations that
involve at least one routed tile — two source-design pads that are already
closer than the clearance are a property of the input, not of the router.
``exact`` / ``seg`` are set only for a 45-degree (diagonal) trace tile: ``tile``
is then the bounding box (broad-phase only), ``exact`` is the tight octagon
copper :class:`~freeroute.geometry.Simplex`, and ``seg`` is ``(a, b,
half_width)`` so the clearance region can be rebuilt exactly on demand. For
every orthogonal tile both are ``None`` and all code paths are byte-identical
to the box-only tree.
"""
owner: int # a caller-chosen id grouping the tiles of one item/connection
net_no: int
layers: frozenset[int]
tile: IntBox
routed: bool = False
exact: Simplex | None = None
seg: tuple | None = None # (a: IntPoint, b: IntPoint, half_width: int)
#: which copper a keepout tile stops: "all", "wires" (a wire_keepout,
#: vias allowed) or "vias" (a via_keepout)
blocks: str = "all"
def on_layer(self, layer: int) -> bool:
return layer in self.layers
def clearance_region(self, clearance: int):
"""This tile's copper grown outward by ``clearance`` (a convex tile).
Orthogonal: the box offset by ``clearance``. Diagonal: the octagon of the
segment swept by ``half_width + clearance`` (exact integer superset)."""
if self.seg is None:
return self.tile.offset(clearance)
a, b, half_width = self.seg
return segment_octagon(a, b, half_width + clearance)
class ShapeSearchTree:
"""Bounding-box spatial hash of :class:`TreeShape` with exact queries."""
__slots__ = ("_bucket", "_buckets", "_by_owner")
def __init__(self, bucket_size: int = 200_000) -> None:
self._bucket = max(bucket_size, 1)
self._buckets: dict[tuple[int, int], list[TreeShape]] = {}
self._by_owner: dict[int, list[TreeShape]] = {}
# --- broad-phase bucketing ---------------------------------------------
def _cells(self, box: IntBox):
lo_x = box.ll.x // self._bucket
hi_x = box.ur.x // self._bucket
lo_y = box.ll.y // self._bucket
hi_y = box.ur.y // self._bucket
for bx in range(lo_x, hi_x + 1):
for by in range(lo_y, hi_y + 1):
yield bx, by
# --- mutation ----------------------------------------------------------
def insert(self, shape: TreeShape) -> None:
for cell in self._cells(shape.tile):
self._buckets.setdefault(cell, []).append(shape)
self._by_owner.setdefault(shape.owner, []).append(shape)
def remove_owner(self, owner: int) -> None:
"""Remove every tile belonging to ``owner`` (a ripped connection)."""
shapes = self._by_owner.pop(owner, [])
for shape in shapes:
for cell in self._cells(shape.tile):
bucket = self._buckets.get(cell)
if bucket:
self._buckets[cell] = [s for s in bucket if s is not shape]
def all_shapes(self) -> list[TreeShape]:
return [s for shapes in self._by_owner.values() for s in shapes]
# --- queries (exact narrow phase) --------------------------------------
def overlapping(self, box: IntBox) -> list[TreeShape]:
"""All indexed tiles whose exact intersection with ``box`` is non-empty."""
seen: set[int] = set()
result: list[TreeShape] = []
for cell in self._cells(box):
for shape in self._buckets.get(cell, ()):
key = id(shape)
if key in seen:
continue
seen.add(key)
if not shape.tile.intersection(box).is_empty():
result.append(shape)
return result
def clearance_conflict(
self, box: IntBox, net_no: int, layer: int, clearance: int, kind: str = "wire"
) -> bool:
"""True if placing ``box`` (of ``net_no`` on ``layer``) would come *closer
than* ``clearance`` to a different net's tile on that layer (exact).
Uses a strict 2-D overlap of the ``clearance``-expanded box, so tiles
exactly ``clearance`` apart (touching after expansion) are allowed.
"""
expanded = box.offset(clearance)
for shape in self.overlapping(expanded):
if shape.net_no == net_no or not shape.on_layer(layer):
continue
if shape.blocks != "all" and shape.blocks != kind + "s":
continue # a wire-only keepout does not stop a via, and so on
if shape.exact is not None:
if overlaps_2d(shape.exact, expanded):
return True
elif shape.tile.overlaps(expanded):
return True
return False
def clearance_conflict_shape(
self, region, net_no: int, layer: int
) -> bool:
"""True if the convex ``region`` (a clearance-grown copper tile, e.g. a
diagonal trace's octagon) overlaps a different net's copper on ``layer``.
``region`` already carries the clearance, so this tests a bare 2-D overlap
against each candidate's exact tile (or its box). Used to check a diagonal
trace whose growth is not an axis-aligned box."""
for shape in self.overlapping(region.bounding_box()):
if shape.net_no == net_no or not shape.on_layer(layer):
continue
other = shape.exact if shape.exact is not None else shape.tile
if overlaps_2d(region, other):
return True
return False
def has_violation(self, clearance: int) -> TreeShape | None:
"""Return a routed tile involved in a clearance violation, or ``None``.
The routing DRC invariant: for every pair of different-net tiles sharing
a layer where **at least one is routed** (a trace or via), the exact
intersection of their ``clearance``-expanded coppers is empty (no 2-D
overlap). Static pad-vs-pad spacing in the source design is not counted.
"""
for shape in self.all_shapes():
expanded = shape.tile.offset(clearance)
grown = None # exact clearance region, built lazily only for diagonals
for other in self.overlapping(expanded):
if other is shape or other.net_no == shape.net_no:
continue
if not (shape.routed or other.routed):
continue # static input pair (e.g. pad vs pad) — not a routing DRC
if not (shape.layers & other.layers):
continue
if shape.exact is None and other.exact is None:
if other.tile.overlaps(expanded): # byte-identical box path
return shape
continue
if grown is None:
grown = shape.clearance_region(clearance)
other_tile = other.exact if other.exact is not None else other.tile
if overlaps_2d(grown, other_tile):
return shape
return None