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.
191 lines
8.3 KiB
Python
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
|