Honour KiCad's keepouts: keyhole rings, wire_keepout, per-tile rasterizing

On the TerraHawk rc4 power stage every engine misread the board's rule areas:

- A no-track ring along the board edge arrives as one keyhole polygon, and
  keepout polygons were wrapped in a single convex tile, so the ring became
  the whole board. Split keepout polygons into convex pieces instead.
- The grid engine rasterized an area by its overall bounding box, which for
  that ring is again the whole board; it routed nothing. Rasterize per tile.
- "Keep router off" rule areas arrive as wire_keepout, which the reader did
  not know, so tracks ran through them (172 DRC items_not_allowed). Read it
  at board and image level. For now it blocks vias too, which is stricter
  than KiCad and cannot create a violation.
- place_keepout only restricts part placement; stop treating it as copper.

The oracle now runs the JAR headless; it was opening FreeRouting's GUI on
the desktop for every oracle test.
This commit is contained in:
2026-09-27 06:30:43 -06:00
parent b26fdd96d5
commit 3ad5c983f5
6 changed files with 115 additions and 10 deletions
+32 -3
View File
@@ -27,6 +27,7 @@ import math
from freeroute.dsn.model import DsnBoard
from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape
from freeroute.geometry import FloatPoint, IntBox, IntPoint, IntVector, Simplex
from freeroute.geometry.polygon_shape import PolygonShape
from .board import BasicBoard
from .clearance import ClearanceMatrix
@@ -219,11 +220,14 @@ def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTran
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
for keepout in dsn.keepouts:
# A place_keepout only restricts where parts go, not copper.
if keepout.kind == "place_keepout":
continue
area = keepout.area
if area is None or area.border is None:
continue
tile = _area_border_tile(area.border, transform)
if tile is None:
tiles = _area_border_tiles(area.border, transform)
if not tiles:
continue
border_layer = area.border.layer
if border_layer in ("signal", "pcb"):
@@ -237,11 +241,36 @@ def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTran
ObstacleArea(
name=keepout.area.name,
layer=layer_index,
tiles=[tile],
tiles=tiles,
)
)
def _area_border_tiles(shape: Shape, transform: CoordinateTransform) -> list:
"""Convex board tiles covering a keepout border shape.
Polygons are split into convex pieces rather than wrapped in one convex
tile: KiCad exports a rule area with a hole (a no-track ring along the
board edge, say) as a single keyhole polygon, and its convex hull is the
whole board.
"""
if isinstance(shape, (Polygon, Path)):
coords = shape.coords
pts = [
IntPoint(
round(transform.dsn_to_board(coords[i])),
round(transform.dsn_to_board(coords[i + 1])),
)
for i in range(0, len(coords) - 1, 2)
]
if len(pts) >= 3:
pieces = [t for t in PolygonShape(pts).split_to_convex() if not t.is_empty()]
if pieces:
return pieces
tile = _area_border_tile(shape, transform)
return [tile] if tile is not None else []
def _area_border_tile(shape: Shape, transform: CoordinateTransform):
"""Convert an absolute-coordinate keepout border shape to one board tile."""
if isinstance(shape, Rectangle):