Files
rpmroute/src/freeroute/board/outline.py
T
rsp2k c1df284147 Route to the real board outline, per connection, and out of fine-pitch pins
- The exact engine verified copper per net and dropped a whole net on any
  conflict: a 25-pin +3V3 net lost every connection to one bad segment.
  Verify and accept per ratsnest connection instead.
- Pad and keepout cells were rounded outward, blocking cells up to half a
  step clear of the copper; 62 of the 64 pins of a 0.5 mm pitch LQFP could
  not start a route. Block only cells whose centre is inside the clearance
  zone, and let a net walk along inside its own pad, which adds no copper.
- Routing was bounded by the outline's bounding box, so tracks cut across
  rounded corners. Keep the DSN boundary as a polygon; the grid blocks cells
  too close to it (scanline plus per-edge, precomputed for vias) and the
  exact and room engines reject copper that is.
- --edge-clearance MM sets copper-to-edge clearance, e.g. KiCad's
  min_copper_edge_clearance, which the DSN does not carry. Default 0.

On the TerraHawk rc4 power stage the exact engine now matches the
FreeRouting JAR: same open count, no DRC errors, with a 0.5 mm edge rule.
2026-09-27 07:26:32 -06:00

123 lines
4.1 KiB
Python

"""The board outline as a polygon, and clearance tests against its edge.
The DSN boundary is the real Edge.Cuts outline, rounded corners and all
(arcs come as short segments). Routing inside its bounding box instead lets
copper cut across a rounded corner closer to the edge than the board allows.
"""
from __future__ import annotations
import math
from freeroute.geometry import IntBox, IntPoint
class BoardOutline:
"""A simple polygon in board units, with point and box clearance tests."""
def __init__(self, points: list[IntPoint]) -> None:
pts = list(points)
if len(pts) > 1 and pts[0] == pts[-1]:
pts.pop()
self.points = pts
self._edges = [(pts[i], pts[(i + 1) % len(pts)]) for i in range(len(pts))]
def contains(self, p: IntPoint) -> bool:
"""Even-odd point in polygon; points on the edge count as outside."""
inside = False
x, y = p.x, p.y
for a, b in self._edges:
if (a.y > y) != (b.y > y):
t = (y - a.y) / (b.y - a.y)
if x < a.x + t * (b.x - a.x):
inside = not inside
return inside
def row_inside_spans(self, y: float) -> list[tuple[float, float]]:
"""The x intervals inside the polygon along the horizontal line at ``y``."""
xs = sorted(
a.x + (y - a.y) * (b.x - a.x) / (b.y - a.y)
for a, b in self._edges
if (a.y > y) != (b.y > y)
)
# an odd count (a line grazing a vertex) drops the unpaired crossing
return list(zip(xs[0::2], xs[1::2], strict=False))
def edges(self):
return self._edges
def edge_distance(self, p: IntPoint) -> float:
return min(_point_segment(p, a, b) for a, b in self._edges)
def point_clear(self, p: IntPoint, reach: float) -> bool:
"""Copper reaching ``reach`` around ``p`` stays inside, clear of the edge."""
return self.contains(p) and self.edge_distance(p) >= reach
def box_clear(self, box: IntBox, clearance: float) -> bool:
"""``box`` lies inside the outline at least ``clearance`` from its edge."""
corners = [
IntPoint(box.ll.x, box.ll.y),
IntPoint(box.ur.x, box.ll.y),
IntPoint(box.ur.x, box.ur.y),
IntPoint(box.ll.x, box.ur.y),
]
if not all(self.contains(c) for c in corners):
return False
return all(_segment_box(a, b, box) >= clearance for a, b in self._edges)
def _point_segment(p: IntPoint, a: IntPoint, b: IntPoint) -> float:
dx, dy = b.x - a.x, b.y - a.y
length2 = dx * dx + dy * dy
if length2 == 0:
return math.hypot(p.x - a.x, p.y - a.y)
t = max(0.0, min(1.0, ((p.x - a.x) * dx + (p.y - a.y) * dy) / length2))
return math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy))
def _point_box(p: IntPoint, box: IntBox) -> float:
dx = max(box.ll.x - p.x, 0, p.x - box.ur.x)
dy = max(box.ll.y - p.y, 0, p.y - box.ur.y)
return math.hypot(dx, dy)
def _segment_box(a: IntPoint, b: IntPoint, box: IntBox) -> float:
"""Distance between segment ``ab`` and an axis-aligned box (0 if they touch)."""
if _segments_cross_box(a, b, box):
return 0.0
corners = [
IntPoint(box.ll.x, box.ll.y),
IntPoint(box.ur.x, box.ll.y),
IntPoint(box.ur.x, box.ur.y),
IntPoint(box.ll.x, box.ur.y),
]
return min(
_point_box(a, box),
_point_box(b, box),
*(_point_segment(c, a, b) for c in corners),
)
def _segments_cross_box(a: IntPoint, b: IntPoint, box: IntBox) -> bool:
"""Liang-Barsky clip: does segment ``ab`` enter the box?"""
t0, t1 = 0.0, 1.0
dx, dy = b.x - a.x, b.y - a.y
for p, q in (
(-dx, a.x - box.ll.x),
(dx, box.ur.x - a.x),
(-dy, a.y - box.ll.y),
(dy, box.ur.y - a.y),
):
if p == 0:
if q < 0:
return False
else:
r = q / p
if p < 0:
t0 = max(t0, r)
else:
t1 = min(t1, r)
if t0 > t1:
return False
return True