--smooth: no 45-degree leg shorter than two track widths

A short diagonal at a wire's end is a kink beside its pad, and the pour
refilled round it left a copper sliver (KiCad copper_sliver) on the
TerraHawk lights node. Small offsets now stay square jogs; chamfers get
the same floor.
This commit is contained in:
2026-10-17 11:15:47 -06:00
parent c91ca1994c
commit 34200f27ff
2 changed files with 28 additions and 1 deletions
+14 -1
View File
@@ -39,6 +39,9 @@ from .pipeline import _dsn_units_per_um, _mm_to_board, _rule_clearance_dsn, _via
#: how many points ahead a staircase collapse looks from each point #: how many points ahead a staircase collapse looks from each point
LOOKAHEAD = 64 LOOKAHEAD = 64
#: a 45-degree leg at least this many track widths long: a shorter one is a
#: kink beside its end's pad, where a refilled pour leaves a copper sliver
MIN_DIAGONAL_WIDTHS = 2
@dataclass @dataclass
@@ -121,6 +124,13 @@ def _candidates(a: IntPoint, b: IntPoint) -> list[list[IntPoint]]:
] ]
def _too_short(a: IntPoint, b: IntPoint, half: int) -> bool:
"""A slanted leg shorter than MIN_DIAGONAL_WIDTHS track widths."""
if a.x == b.x or a.y == b.y:
return False
return (b.x - a.x) ** 2 + (b.y - a.y) ** 2 < (MIN_DIAGONAL_WIDTHS * 2 * half) ** 2
def _dedupe(pts: list[IntPoint]) -> list[IntPoint]: def _dedupe(pts: list[IntPoint]) -> list[IntPoint]:
out: list[IntPoint] = [] out: list[IntPoint] = []
for p in pts: for p in pts:
@@ -211,6 +221,8 @@ class _Smoother:
def path_clear(self, path: list[IntPoint], half: int, net: int, layer: int) -> bool: def path_clear(self, path: list[IntPoint], half: int, net: int, layer: int) -> bool:
"""New legs and new corners of ``path`` (its ends are kept points).""" """New legs and new corners of ``path`` (its ends are kept points)."""
if any(_too_short(p, q, half) for p, q in zip(path, path[1:], strict=False)):
return False
return all( return all(
self.leg_clear(p, q, half, net, layer) for p, q in zip(path, path[1:], strict=False) self.leg_clear(p, q, half, net, layer) for p, q in zip(path, path[1:], strict=False)
) and all(self.corner_clear(c, half, net, layer) for c in path[1:-1]) ) and all(self.corner_clear(c, half, net, layer) for c in path[1:-1])
@@ -262,7 +274,8 @@ class _Smoother:
cap_out = len_out if (b.x, b.y) in keep or i == len(pts) - 2 else len_out // 2 cap_out = len_out if (b.x, b.y) in keep or i == len(pts) - 2 else len_out // 2
d = min(cap_in, cap_out) d = min(cap_in, cap_out)
done = False done = False
while d >= 2 * half: # the chamfer's leg is d * sqrt(2) long
while d * d * 2 >= (MIN_DIAGONAL_WIDTHS * 2 * half) ** 2:
p = IntPoint(c.x - ux * d, c.y - uy * d) p = IntPoint(c.x - ux * d, c.y - uy * d)
q = IntPoint(c.x + vx * d, c.y + vy * d) q = IntPoint(c.x + vx * d, c.y + vy * d)
corners = [x for x in (p, q) if (x.x, x.y) not in ((a.x, a.y), (b.x, b.y))] corners = [x for x in (p, q) if (x.x, x.y) not in ((a.x, a.y), (b.x, b.y))]
+14
View File
@@ -146,3 +146,17 @@ def test_a_detour_with_nothing_in_its_way_is_straightened():
assert _points(result.nets[0].wires[0]) == [(22000, -2000), (22000, -22000)] assert _points(result.nets[0].wires[0]) == [(22000, -2000), (22000, -22000)]
assert (report.before.right_angles, report.after.right_angles) == (2, 0) assert (report.before.right_angles, report.after.right_angles) == (2, 0)
assert verify_result(dsn, result).clean assert verify_result(dsn, result).clean
def test_a_jog_too_small_for_a_long_diagonal_stays_square():
# a 0.1 mm jog halfway: its 45-degree leg would be shorter than two track
# widths, a kink beside the copper that leaves pour slivers
dsn = parse_dsn(_board(extra_places="(place A3 2100 -22000 front 0)").replace(
"(pins A1-1 A2-1)", "(pins A1-1 A3-1)"))
wire = [(2000, -2000), (2000, -12000), (2100, -12000), (2100, -22000)]
result = _result(_wire(wire))
smooth(dsn, result)
pts = _points(result.nets[0].wires[0])
for a, b in zip(pts, pts[1:], strict=False):
if a[0] != b[0] and a[1] != b[1]:
assert (b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2 >= (2 * 200) ** 2