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.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user