"""Pins of a fine-pitch part turned off the grid axes leave along their pads. A QFN at 45 degrees has no straight exit: every horizontal or vertical step from a pin passes its neighbours closer than the clearance (0.35 mm away on each axis at 0.5 mm pitch). The grid leaves along the pad's own axis first, past the pin row, and the exact engine checks that slanted leg on its exact copper. Found on th-eda's lift ESC, where a QFN-24 at 45 degrees left 15 connections unroutable on an empty board. """ from __future__ import annotations import math import pytest from freeroute.board import build_board from freeroute.dsn import parse_dsn from freeroute.geometry import IntPoint, overlaps_2d, segment_slab from freeroute.route.grid_router import GridRouter from freeroute.route.pipeline import ( _rule_clearance_dsn, _rule_width_dsn, _via_half_board, build_exact_routing_result, ) def _qfn_side_dsn(rotation: float) -> str: """Two rows of five 0.25 x 0.8 mm pads at 0.5 mm pitch (one side of a QFN and its opposite side), turned by ``rotation``; each pin of the top row is its own net, routed to a pad 4.5 mm out, the pads fanned 1.5 mm apart.""" cx, cy = 100000, -100000 t = math.radians(rotation) out = (-math.sin(t), math.cos(t)) # the pads' outward axis after turning along = (math.cos(t), math.sin(t)) # the pin row pins, targets, nets = [], [], [] for i in range(5): x = -1000 + 500 * i pins += [f"(pin P {i + 1} {x} 1000)", f"(pin P {i + 6} {x} -1000)"] px = cx + x * math.cos(t) - 1000 * math.sin(t) py = cy + x * math.sin(t) + 1000 * math.cos(t) spread = 1500 * (i - 2) - x # fan the far pads out to 1.5 mm apart tx = px + 4500 * out[0] + spread * along[0] ty = py + 4500 * out[1] + spread * along[1] targets.append(f"(place T{i + 1} {round(tx)} {round(ty)} front 0)") nets.append(f"(net N{i} (pins U1-{i + 1} T{i + 1}-1))") return f"""(pcb qfn (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (layer B.Cu (type signal)) (boundary (path pcb 0 115000 -115000 85000 -115000 85000 -85000 115000 -85000 115000 -115000)) (via "Via[0-1]_450:200_um") (rule (width 150) (clearance 150))) (placement (component Q (place U1 {cx} {cy} front {rotation})) (component T {" ".join(targets)})) (library (image Q {" ".join(pins)}) (image T (pin t 1 0 0)) (padstack P (shape (rect F.Cu -125 -400 125 400)) (attach off)) (padstack t (shape (rect F.Cu -300 -300 300 300)) (attach off)) (padstack "Via[0-1]_450:200_um" (shape (circle F.Cu 450)) (shape (circle B.Cu 450)) (attach off))) (network {" ".join(nets)}))""" @pytest.mark.parametrize("rotation", [45, 30, -45]) def test_a_turned_fine_pitch_part_gets_its_pins_out(rotation): result = build_exact_routing_result(parse_dsn(_qfn_side_dsn(rotation)), resolution=2, shove=True) assert result.unrouted == [] def test_the_exit_runs_along_the_pad_axis_and_clears_the_neighbours(): dsn = parse_dsn(_qfn_side_dsn(45)) s = dsn.resolution.value board = build_board(dsn) router = GridRouter( board, trace_width=round(_rule_width_dsn(dsn) * s), clearance=round(_rule_clearance_dsn(dsn) * s), via_half=_via_half_board(dsn, s), orthogonal=True, ) pins = {p.name: p for p in board.get_pins() if p.component_ref == "U1"} pin = pins["3"] exit_point = router._axis_exit(pin) assert exit_point is not None dx, dy = exit_point.x - pin.location.x, exit_point.y - pin.location.y assert abs(abs(dx) - abs(dy)) <= 2 # along the 45-degree axis leg = segment_slab(pin.location, exit_point, router.half_width + router.clearance) for name in ("2", "4"): assert not overlaps_2d(leg, pins[name].shape) def test_a_pad_on_the_grid_axes_keeps_the_usual_exits(): dsn = parse_dsn(_qfn_side_dsn(0)) s = dsn.resolution.value board = build_board(dsn) router = GridRouter(board, trace_width=round(_rule_width_dsn(dsn) * s), clearance=round(_rule_clearance_dsn(dsn) * s), via_half=_via_half_board(dsn, s)) pin = next(p for p in board.get_pins() if p.component_ref == "U1" and p.name == "3") assert router._axis_exit(pin) is None def test_a_slanted_slab_is_tight_where_a_bounding_box_is_not(): slab = segment_slab(IntPoint(0, 0), IntPoint(10000, 10000), 750) # a point beside the diagonal but inside its bounding box from freeroute.geometry import IntBox assert not overlaps_2d(slab, IntBox(8000, 0, 8100, 100)) assert overlaps_2d(slab, IntBox(4900, 4900, 5100, 5100))