From 20dc5fd96aa07ef34aa90f9008931ad1cf046cd9 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 13 Jul 2026 12:47:06 -0600 Subject: [PATCH] Support exact octagon tiles in the search tree clearance checks Add optional exact/seg fields to TreeShape for a diagonal trace tile: tile holds the bounding box for broad-phase bucketing, exact holds the tight octagon copper, and seg=(a,b,half_width) rebuilds the clearance region on demand. Every orthogonal tile leaves both None and takes the identical box code path, so the box-only tree stays byte-for-byte unchanged (whole suite green). clearance_conflict now tests a stored diagonal tile with an exact 2-D overlap; clearance_conflict_shape checks a diagonal trace's clearance octagon against stored copper; has_violation runs the exact octagon test whenever either tile is diagonal and the plain box overlap otherwise. All exact-integer via Simplex intersection. --- src/freeroute/board/search_tree.py | 53 ++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/src/freeroute/board/search_tree.py b/src/freeroute/board/search_tree.py index 9706aa3..caaa49b 100644 --- a/src/freeroute/board/search_tree.py +++ b/src/freeroute/board/search_tree.py @@ -18,7 +18,7 @@ from __future__ import annotations from dataclasses import dataclass -from freeroute.geometry import IntBox +from freeroute.geometry import IntBox, Simplex, overlaps_2d, segment_octagon __all__ = ["TreeShape", "ShapeSearchTree"] @@ -31,6 +31,13 @@ class TreeShape: 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 @@ -38,10 +45,22 @@ class TreeShape: layers: frozenset[int] tile: IntBox routed: bool = False + exact: Simplex | None = None + seg: tuple | None = None # (a: IntPoint, b: IntPoint, half_width: int) 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.""" @@ -110,7 +129,27 @@ class ShapeSearchTree: for shape in self.overlapping(expanded): if shape.net_no == net_no or not shape.on_layer(layer): continue - if shape.tile.overlaps(expanded): + 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 @@ -124,6 +163,7 @@ class ShapeSearchTree: """ 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 @@ -131,6 +171,13 @@ class ShapeSearchTree: continue # static input pair (e.g. pad vs pad) — not a routing DRC if not (shape.layers & other.layers): continue - if other.tile.overlaps(expanded): + 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