main
noah Heatsink vias on a 0.7 mm grid; variant stock flags (1/5) 8ab08ff 7d ago
"""Finish the two footprints from adom-footprint's output (KiCad Y down, top view).

    python3 tools/footprints.py <build/fp>

LAN8770M: Microchip land pattern C04-3268 Rev A (DS00002550C Figure 3-3). adom-footprint writes the 32 oval pads,
the 3.20 mm exposed pad and the 3 x 3 grid of 0.33 mm thermal vias (EV 1.20 mm); this adds a split paste stencil
on the exposed pad and the 3D model.

TMC2262: ADI land pattern 90-100245 Rev B (package 21-100708 Rev A). adom-footprint writes the 10 side bars, the
16 small pads and the 1.45 x 4.05 mm exposed pad with its heatsink vias; this adds the four L-shaped corner pads
(pins 6, 15, 21, 30) as KiCad custom pads with the drawing's R0.150 inner corner, a split paste stencil, the
silkscreen (the corner pads reach 0.3 mm past the body, so the outline sits outside the copper) and the model.
"""
import json, math, os, re, subprocess, sys

OUT = sys.argv[1] if len(sys.argv) > 1 else 'build/fp'
H = os.path.dirname(os.path.abspath(__file__))


def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode:
        raise SystemExit('%s failed: %s %s' % (cmd, r.stdout[-500:], r.stderr[-500:]))
    return r.stdout + r.stderr


def f(v):
    s = ('%.4f' % v).rstrip('0').rstrip('.')
    return '0' if s in ('-0', '') else s


def paste_windows(cx, cy, nx, ny, w, h, pitch_x, pitch_y):
    out = []
    for i in range(nx):
        for j in range(ny):
            x = cx + (i - (nx - 1) / 2) * pitch_x
            y = cy + (j - (ny - 1) / 2) * pitch_y
            out.append('  (pad "" smd rect (at %s %s) (size %s %s)\n    (layers "F.Paste"))' % (f(x), f(y), f(w), f(h)))
    return out


def model(slug):
    return ('  (model "${KIPRJMOD}/%s-etched.step"\n    (offset (xyz 0 0 0))\n    (scale (xyz 1 1 1))\n'
            '    (rotate (xyz 0 0 0))\n  )' % slug)


def finish(text, ep, extra, slug, drop_silk=False):
    # the exposed pad keeps copper + mask; its paste goes into separate windows
    text = re.sub(r'(\(pad "%s" smd rect \(at [^)]*\) \(size [^)]*\)\n    \(layers "F.Cu") "F.Paste" ("F.Mask"\))' % ep,
                  r'\1 \2', text, count=1)
    if drop_silk:
        text = re.sub(r'  \(fp_line \(start [^)]*\) \(end [^)]*\)\n    \(stroke [^\n]*\(layer "F.SilkS"\)\)\n', '', text)
    text = text.rstrip()
    assert text.endswith(')')
    return text[:-1].rstrip() + '\n' + '\n'.join(extra) + '\n' + model(slug) + '\n)\n'


# ------------------------------------------------------------------------------------------------ LAN8770M
print(run(['adom-footprint', 'create', '--file', os.path.join(OUT, 'lan8770m-create.json'), '--out', os.path.join(OUT, 'lan8770m')]).strip().splitlines()[-3:])
lan = os.path.join(OUT, 'lan8770m', 'lan8770m.kicad_mod')
t = open(lan).read()
extra = paste_windows(0, 0, 2, 2, 1.25, 1.25, 1.6, 1.6)               # 4 x 1.5625 mm2 = 61 % of the 3.2 mm pad
# pin 1: a filled dot at the pad-1 corner (the hero generator and every EDA tool read a filled mark as pin 1),
# in place of adom-footprint's bare line
t = t.replace('  (fp_line (start -2.8 -2.8) (end -2.2 -2.8)\n    (stroke (width 0.12) (type default)) (layer "F.SilkS"))\n', '')
extra.append('  (fp_circle (center -3 -2.3) (end -2.9 -2.3)\n    (stroke (width 0.05) (type default)) (fill solid) (layer "F.SilkS"))')
t = finish(t, 'EP', extra, 'lan8770m')
t = t.replace('  (fp_rect (start -2.5 -2.5) (end 2.5 2.5)\n    (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))',
              '  (fp_poly (pts (xy -2 -2.5) (xy 2.5 -2.5) (xy 2.5 2.5) (xy -2.5 2.5) (xy -2.5 -2))\n    (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))')
open(lan, 'w').write(t)
print('OK', lan, len(re.findall(r'\(pad "', t)), 'pads')

# ------------------------------------------------------------------------------------------------ TMC2262
e = 0.5
pads = []
bars = [-1.73, -0.855, 0.0, 0.855, 1.73]                              # KiCad y of pins 1..5 (top to bottom)
for i, y in enumerate(bars):
    pads.append(dict(number=str(1 + i), x=-2.1625, y=y, dx=2.275, dy=0.59, shape='rect'))        # x -3.3 .. -1.025
    pads.append(dict(number=str(20 - i), x=2.1625, y=y, dx=2.275, dy=0.59, shape='rect'))         # pin 20 top right
small = [-1.9, -1.4, -0.9, -0.4, 0.4, 0.9, 1.4, 1.9]
for i, x in enumerate(small):
    pads.append(dict(number=str(7 + i), x=x, y=2.9375, dx=0.30, dy=0.725, shape='rect'))         # bottom row, 7..14
    pads.append(dict(number=str(29 - i), x=x, y=-2.9375, dx=0.30, dy=0.725, shape='rect'))       # top row, 29..22
pads.append(dict(number='EP', x=0, y=0, dx=1.45, dy=4.05, shape='rect'))
cj = dict(footprintName='tmc2262', description='Analog Devices TMC2262 30L FC2QFN 6x6x0.75 mm (F306A6F+1F), 0.5 mm pitch; '
          'land pattern 90-100245 Rev B, package outline 21-100708 Rev A',
          tags='TMC2262 TMC2262AFV+ FC2QFN-30 6x6 stepper driver', pads=pads,
          body=dict(x1=-3.0, y1=-3.0, x2=3.0, y2=3.0), courtyard=dict(x1=-3.75, y1=-3.75, x2=3.75, y2=3.75),
          heatsinkVias=dict(pad='EP', drill_mm=0.3, pitch_mm=0.7, ring_mm=0.15, tented=True))
os.makedirs(os.path.join(OUT, 'tmc2262'), exist_ok=True)
json.dump(cj, open(os.path.join(OUT, 'tmc2262-create.json'), 'w'), indent=1)
print(run(['adom-footprint', 'create', '--file', os.path.join(OUT, 'tmc2262-create.json'), '--out', os.path.join(OUT, 'tmc2262')]).strip().splitlines()[-3:])
tp = os.path.join(OUT, 'tmc2262', 'tmc2262.kicad_mod')
t = open(tp).read()


def corner(num, sx, sy):
    """The L pad from 90-100245: a 0.450 wide leg from the outer edge (6.6 mm) 0.975 inward at x = sx*2.525, and a
    0.450 high leg from y = sy*2.325 .. sy*2.775 out to the side edge; R0.150 on the corner that faces the die."""
    ax, ay = sx * 2.525, sy * 2.8                                       # anchor: inside the vertical leg
    r = 0.15
    # polygon in absolute coordinates for the top-right corner (sx = sy = +1 means x right, y DOWN in KiCad);
    # built for sx=+1, sy=-1 (top right in KiCad) and mirrored
    pts = [(2.30, -3.30), (2.75, -3.30), (2.75, -2.775), (3.30, -2.775), (3.30, -2.325), (2.30 + r, -2.325)]
    cx, cy = 2.30 + r, -2.325 - r
    for k in range(1, 8):                                               # fillet from (2.45,-2.325) to (2.30,-2.475)
        a = math.pi / 2 + k * (math.pi / 2) / 8
        pts.append((cx + r * math.cos(a), cy + r * math.sin(a)))
    pts.append((2.30, -2.325 - r))
    mx, my = (1 if sx > 0 else -1), (1 if sy < 0 else -1)
    pts = [(px * mx, py * my) for px, py in pts]
    if mx * my < 0:
        pts.reverse()
    rel = ' '.join('(xy %s %s)' % (f(px - ax), f(py - ay)) for px, py in pts)
    return ('  (pad "%s" smd custom (at %s %s) (size 0.4 0.4)\n    (layers "F.Cu" "F.Paste" "F.Mask")\n'
            '    (options (clearance outline) (anchor rect))\n'
            '    (primitives\n      (gr_poly (pts %s) (width 0) (fill yes))\n    )\n  )') % (num, f(ax), f(ay), rel)


extra = [corner('30', -1, -1), corner('21', +1, -1), corner('6', -1, +1), corner('15', +1, +1)]
extra += paste_windows(0, 0, 1, 4, 1.15, 0.85, 0, 1.0)                # 4 x 0.98 mm2 = 66 % of the 5.87 mm2 pad
# silkscreen: corner brackets 0.16 mm clear of the 6.6 mm copper extent, pin-1 dot beside pad 1
s = 3.52
for qx in (-1, 1):
    for qy in (-1, 1):
        x0, y0 = qx * s, qy * s
        extra.append('  (fp_line (start %s %s) (end %s %s)\n    (stroke (width 0.12) (type default)) (layer "F.SilkS"))' % (f(x0), f(y0), f(x0 - qx * 0.6), f(y0)))
        extra.append('  (fp_line (start %s %s) (end %s %s)\n    (stroke (width 0.12) (type default)) (layer "F.SilkS"))' % (f(x0), f(y0), f(x0), f(y0 - qy * 0.6)))
extra.append('  (fp_circle (center -3.72 -1.73) (end -3.6 -1.73)\n    (stroke (width 0.12) (type default)) (fill solid) (layer "F.SilkS"))')
t = finish(t, 'EP', extra, 'tmc2262', drop_silk=True)
# pin-1 chamfer on the fab outline
t = t.replace('  (fp_rect (start -3 -3) (end 3 3)\n    (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))',
              '  (fp_poly (pts (xy -2.5 -3) (xy 3 -3) (xy 3 3) (xy -3 3) (xy -3 -2.5))\n    (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))')
open(tp, 'w').write(t)
print('OK', tp, len(re.findall(r'\(pad "', t)), 'pads')