- 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.
123 lines
4.1 KiB
Python
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
|