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.
163 lines
6.2 KiB
Python
163 lines
6.2 KiB
Python
"""Smoothing: staircases become octilinear runs, corners are chamfered, and
|
|
nothing it keeps (ends, vias, joints, pads it crosses) moves."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.route.smooth import smooth
|
|
from rpmroute.route.verify import verify_result
|
|
from rpmroute.ses.model import RoutedVia, RoutedWire, RoutingResult
|
|
|
|
VIA = "Via[0-1]_600:300_um"
|
|
|
|
|
|
def _board(extra_pads: str = "", extra_places: str = "", nets: str = "") -> str:
|
|
"""A 30 x 30 mm two-layer board, units um, 0.2 mm track and clearance.
|
|
A1 at (2, -2) mm and A2 at (22, -22) mm are net SIG."""
|
|
return f"""(pcb "smooth.dsn"
|
|
(parser (string_quote ") (space_in_quoted_tokens on) (host_cad "t") (host_version "1"))
|
|
(resolution um 10)
|
|
(unit um)
|
|
(structure
|
|
(layer F.Cu (type signal) (property (index 0)))
|
|
(layer B.Cu (type signal) (property (index 1)))
|
|
(boundary (path pcb 0 0 0 30000 0 30000 -30000 0 -30000 0 0))
|
|
(via "{VIA}")
|
|
(rule (width 200) (clearance 200))
|
|
)
|
|
(library
|
|
(padstack P (shape (rect F.Cu -300 -300 300 300)) (attach off))
|
|
(padstack "{VIA}" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off))
|
|
(image PAD (pin P 1 0 0))
|
|
{extra_pads}
|
|
)
|
|
(placement
|
|
(component PAD (place A1 2000 -2000 front 0) (place A2 22000 -22000 front 0) {extra_places})
|
|
)
|
|
(network
|
|
(net SIG (pins A1-1 A2-1))
|
|
{nets}
|
|
(class default (rule (width 200) (clearance 200)))
|
|
)
|
|
(wiring)
|
|
)
|
|
"""
|
|
|
|
|
|
def _staircase(a, b, step):
|
|
"""Points from ``a`` to ``b`` (DSN um) in alternating x and y steps."""
|
|
(x, y), (bx, by) = a, b
|
|
pts = [(x, y)]
|
|
sx = step if bx > x else -step
|
|
sy = step if by > y else -step
|
|
while (x, y) != (bx, by):
|
|
if x != bx:
|
|
x = x + sx if abs(bx - x) > step else bx
|
|
pts.append((x, y))
|
|
if y != by:
|
|
y = y + sy if abs(by - y) > step else by
|
|
pts.append((x, y))
|
|
return pts
|
|
|
|
|
|
def _wire(pts, layer="F.Cu", fixed=False):
|
|
return RoutedWire(layer=layer, width=200, coords=[v for p in pts for v in p], fixed=fixed)
|
|
|
|
|
|
def _points(wire):
|
|
c = wire.coords
|
|
return [(round(c[i]), round(c[i + 1])) for i in range(0, len(c), 2)]
|
|
|
|
|
|
def _result(*wires, vias=(), net="SIG"):
|
|
result = RoutingResult()
|
|
for w in wires:
|
|
result.add_wire(net, w)
|
|
for x, y in vias:
|
|
result.net(net, create=True).vias.append(RoutedVia(padstack=VIA, x=x, y=y))
|
|
return result
|
|
|
|
|
|
def test_a_staircase_becomes_one_diagonal():
|
|
dsn = parse_dsn(_board())
|
|
stair = _staircase((2000, -2000), (22000, -22000), 1000)
|
|
result = _result(_wire(stair))
|
|
report = smooth(dsn, result)
|
|
pts = _points(result.nets[0].wires[0])
|
|
assert pts == [(2000, -2000), (22000, -22000)]
|
|
assert report.wires_changed == 1 and report.after.right_angles == 0
|
|
assert report.after.total_mm < report.before.total_mm
|
|
assert verify_result(dsn, result).clean
|
|
|
|
|
|
def test_a_pad_in_the_way_is_gone_round():
|
|
# another net's pad sits on the diagonal shortcut of an L-shaped route
|
|
dsn = parse_dsn(_board(extra_places="(place B1 12000 -12000 front 0) (place B2 28000 -28000 front 0)",
|
|
nets="(net OTHER (pins B1-1 B2-1))"))
|
|
result = _result(_wire([(2000, -2000), (22000, -2000), (22000, -22000)]))
|
|
report = smooth(dsn, result)
|
|
pts = _points(result.nets[0].wires[0])
|
|
assert pts[0] == (2000, -2000) and pts[-1] == (22000, -22000)
|
|
assert report.wires_changed == 1 # the corner is chamfered, short of the pad
|
|
assert verify_result(dsn, result).clean
|
|
|
|
|
|
def test_a_joint_and_a_via_stay():
|
|
dsn = parse_dsn(_board(extra_places="(place A3 22000 -2000 front 0)",
|
|
nets="").replace("(pins A1-1 A2-1)", "(pins A1-1 A2-1 A3-1)"))
|
|
stair = _staircase((2000, -2000), (22000, -22000), 1000)
|
|
joint = stair[11] # A3's leg ends here, on the staircase
|
|
leg = _wire([(22000, -2000), (joint[0], -2000), joint])
|
|
via = stair[25]
|
|
result = _result(_wire(stair), leg, vias=[via])
|
|
smooth(dsn, result)
|
|
pts = _points(result.nets[0].wires[0])
|
|
assert joint in pts and via in pts
|
|
assert pts[0] == (2000, -2000) and pts[-1] == (22000, -22000)
|
|
assert verify_result(dsn, result).clean
|
|
|
|
|
|
def test_the_board_edge_is_kept_clear_of():
|
|
# a staircase hugging the top edge: a chamfer may not push copper over it
|
|
dsn = parse_dsn(_board(extra_places="(place A3 22000 -500 front 0)").replace(
|
|
"(pins A1-1 A2-1)", "(pins A1-1 A3-1)"))
|
|
wire = _wire([(2000, -2000), (2000, -500), (22000, -500)])
|
|
result = _result(wire)
|
|
smooth(dsn, result, edge_clearance_mm=0.3)
|
|
for _x, y in _points(result.nets[0].wires[0]):
|
|
assert -y >= 300 + 100 # half width + edge clearance from y = 0
|
|
|
|
|
|
def test_fixed_wiring_is_left_alone():
|
|
dsn = parse_dsn(_board())
|
|
stair = _staircase((2000, -2000), (22000, -22000), 1000)
|
|
result = _result(_wire(stair, fixed=True))
|
|
report = smooth(dsn, result)
|
|
assert _points(result.nets[0].wires[0]) == stair
|
|
assert report.wires == 0
|
|
|
|
|
|
def test_a_detour_with_nothing_in_its_way_is_straightened():
|
|
dsn = parse_dsn(_board(extra_places="(place A3 22000 -2000 front 0)").replace(
|
|
"(pins A1-1 A2-1)", "(pins A3-1 A2-1)"))
|
|
# a U out to x = 12 mm and back, with nothing between its ends
|
|
result = _result(_wire([(22000, -2000), (12000, -2000), (12000, -22000), (22000, -22000)]))
|
|
report = smooth(dsn, result)
|
|
assert _points(result.nets[0].wires[0]) == [(22000, -2000), (22000, -22000)]
|
|
assert (report.before.right_angles, report.after.right_angles) == (2, 0)
|
|
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
|