main
John Lauer Component record TPS16850VMAR: files batch 1 6db00ed 18h ago
#!/usr/bin/env python3
"""Build the TPS16850VMAR footprint from TI's recommended land pattern (SLVSHA1D p.55, LAND PATTERN EXAMPLE for
VMA0023A, 4230225/A 11/2023) and solder paste example (p.56).

KiCad frame: Y down, origin = package centre (TI's 0.000 PKG centre lines). TI draws the land pattern as a top
view with Y up, so every TI y is negated here.

Step 1 writes the plain pads (rectangles, the exposed pad, TI's 12 vias) as an adom-footprint create JSON and runs
adom-footprint 1.0.26. Step 2 adds what adom-footprint create cannot express:
  * pads 6 and 17: L-shaped custom pads (non solder mask defined, like the plain pads);
  * pads 7, 10 and 22: TI draws a copper rectangle under solder mask (blue dashed, "METAL UNDER SOLDER MASK") with
    a comb-shaped solder mask opening (green). Written as two pads with the same number: a copper-only rectangle
    (F.Cu) and a comb custom pad (F.Cu F.Mask F.Paste, mask margin 0);
  * exposed pad 24: copper and mask only; the paste is TI's six 0.98 x 1.04 mm windows (p.56, 76 % coverage),
    written as unnumbered F.Paste apertures;
  * silkscreen outline 0.2 mm outside the copper, pin-1 dot left of pad 1, fab outline with the pin-1 chamfer,
    3D model path; corner radius of the plain pads R0.05 as TI prints it.
Step 3 writes tps16850vmar-lbr-input.kicad_mod for adom-lbr 2.47.5, which drops custom-pad primitives and
paste-only pads: the same copper as rectangles (every custom shape here is rectilinear, so the union of the
rectangles is exact), copper-only rectangles with mask and paste margin -1.2 mm (no opening), no paste-only pads.
usage: build_fp.py outdir
"""
import json, os, re, subprocess, sys

out = sys.argv[1]
NAME = "tps16850vmar"
MODEL = "${KIPRJMOD}/tps16850vmar-etched.step"
R = 0.05

def rr(w, h): return round(R / min(w, h), 4)

pads = []
def pad(n, x, y, w, h):
    pads.append({"number": str(n), "x": x, "y": y, "dx": w, "dy": h, "shape": "roundrect"})

for n, ty in zip(range(1, 5), (0.5, 0.0, -0.5, -1.0)):      # left 1-4: (0.65) long, 21X (0.25) wide
    pad(n, -2.875, -ty if ty else 0.0, 0.65, 0.25)
pad(5, -2.85, 1.5, 0.70, 0.25)                              # pin 5 (0.7) long
pad(8, 0.225, 2.325, 0.25, 0.75)                            # 2X (0.75) at x 0.225 / 1.275, TI y -2.325
pad(9, 1.275, 2.325, 0.25, 0.75)
for n, ty in zip(range(11, 17), (-0.925, -0.425, 0.075, 0.575, 1.075, 1.575)):   # right, 10X (0.65)
    pad(n, 2.875, -ty, 0.65, 0.25)
for n, x in zip(range(18, 22), (1.85, 1.35, 0.85, 0.35)):   # top, 4X 0.6 at TI y 2.4
    pad(n, x, -2.4, 0.25, 0.60)
pad(23, -2.55, -1.55, 1.30, 0.25)                           # (1.3) at TI y 1.55
pads.append({"number": "24", "x": 0.0, "y": -0.075, "dx": 3.4, "dy": 2.35, "shape": "rect", "layers": '"F.Cu" "F.Mask"'})
VIAS = [(x, y) for y in (-0.875, -0.075, 0.725) for x in (-1.45, -0.59, 0.59, 1.45)]   # (0.2) TYP VIA
for x, y in VIAS:
    pads.append({"number": "24", "x": x, "y": y, "dx": 0.5, "dy": 0.5, "shape": "circle", "mount": "thru_hole",
                 "drill": 0.2, "layers": '"*.Cu"'})
spec = {"footprintName": NAME,
        "description": "TI VMA0023A LQFN-CLIP-23, 6.0 x 5.0 mm, 1.5 mm max height; land pattern and stencil per "
                       "TI SLVSHA1D p.55-56 (TPS1685x eFuse)",
        "tags": "TPS1685 TPS16850 VMA0023A LQFN-CLIP QFN eFuse",
        "pads": pads, "body": {"x1": -3.0, "y1": -2.5, "x2": 3.0, "y2": 2.5},
        "courtyard": {"x1": -3.6, "y1": -3.1, "x2": 3.6, "y2": 3.1}}
os.makedirs(out, exist_ok=True)
jp = os.path.join(out, "footprint-create.json")
json.dump(spec, open(jp, "w"), indent=1)
r = subprocess.run(["adom-footprint", "create", "--file", jp, "--out", os.path.join(out, "gen")], capture_output=True, text=True)
if r.returncode: sys.exit(r.stdout + r.stderr)
base = open(os.path.join(out, "gen", NAME + ".kicad_mod")).read()
base = re.sub(r'\s*\(fp_line [^\n]*\n\s*\(stroke [^\n]*\(layer "F.SilkS"\)\)', "", base)
base = re.sub(r'\s*\(fp_rect \(start -3 -2.5\) \(end 3 2.5\)\n\s*\(stroke [^\n]*\(layer "F.Fab"\)\)', "", base)
base = base.replace('(drill 0.2)\n    (layers "*.Cu"))', '(drill 0.2)\n    (layers "*.Cu") (property pad_prop_heatsink))')
base = base.replace("(roundrect_rratio 0.25))", "(roundrect_rratio 0.2))")       # r = 0.05 on 0.25 mm pads
base = re.sub(r'\(fp_text reference "REF\*\*" \(at 0 [-\d.]+\)', '(fp_text reference "REF**" (at 0 -4)', base)
base = re.sub(r'\(fp_text value "[^"]*" \(at 0 [-\d.]+\)', '(fp_text value "TPS16850VMAR" (at 0 4)', base)

def rect(x1, y1, x2, y2): return [(x1, y1), (x2, y1), (x2, y2), (x1, y2)]
ALL = '"F.Cu" "F.Paste" "F.Mask"'
# custom shapes (KiCad y)
P6 = [(-3.2, 1.875), (-2.5, 1.875), (-2.5, 2.7), (-2.8, 2.7), (-2.8, 2.125), (-3.2, 2.125)]
P17 = [(2.225, -2.7), (2.475, -2.7), (2.475, -2.15), (3.2, -2.15), (3.2, -1.9), (2.225, -1.9)]
C22 = [(-3.2, -1.925), (-1.0, -1.925), (-1.0, -2.7), (-1.25, -2.7), (-1.25, -2.25), (-1.5, -2.25), (-1.5, -2.7),
       (-1.75, -2.7), (-1.75, -2.25), (-2.0, -2.25), (-2.0, -2.7), (-2.25, -2.7), (-2.25, -2.25), (-2.5, -2.25),
       (-2.5, -2.7), (-2.75, -2.7), (-2.75, -2.175), (-3.2, -2.175)]
C7 = [(-1.7, 1.95), (-0.15, 1.95), (-0.15, 2.7), (-0.35, 2.7), (-0.35, 2.25), (-0.6, 2.25), (-0.6, 2.7), (-0.8, 2.7),
      (-0.8, 2.25), (-1.05, 2.25), (-1.05, 2.7), (-1.25, 2.7), (-1.25, 2.25), (-1.5, 2.25), (-1.5, 2.7), (-1.7, 2.7)]
C10 = [(1.65, 1.85), (3.2, 1.85), (3.2, 2.25), (2.4, 2.25), (2.4, 2.7), (2.15, 2.7), (2.15, 2.25), (1.9, 2.25),
       (1.9, 2.7), (1.65, 2.7)]
CU = {"22": (-3.25, -2.75, -0.95, -1.875), "7": (-1.75, 1.9, -0.1, 2.75), "10": (1.6, 1.8, 3.25, 2.75)}
ANCH = {"6": (-2.65, 2.0), "17": (2.35, -2.025), "22": (-2.1, -2.05), "7": (-0.925, 2.1), "10": (2.4, 2.05)}
# the same shapes as rectangles (for adom-lbr)
RECTS = {"6": [(-3.2, 1.875, -2.5, 2.125), (-2.8, 1.875, -2.5, 2.7)],
         "17": [(2.225, -2.7, 2.475, -1.9), (2.225, -2.15, 3.2, -1.9)],
         "22": [(-3.2, -2.175, -2.75, -1.925), (-2.75, -2.25, -1.0, -1.925)] +
               [(c - 0.125, -2.7, c + 0.125, -2.25) for c in (-2.625, -2.125, -1.625, -1.125)],
         "7": [(-1.7, 1.95, -0.15, 2.25)] + [(c - 0.1, 2.25, c + 0.1, 2.7) for c in (-1.6, -1.15, -0.7, -0.25)],
         "10": [(1.65, 1.85, 3.2, 2.25)] + [(c - 0.125, 2.25, c + 0.125, 2.7) for c in (1.775, 2.275)]}
SHAPE = {"6": P6, "17": P17, "22": C22, "7": C7, "10": C10}

def custom(num, pts, layers, extra=""):
    ax, ay = ANCH[num]
    rel = " ".join(f"(xy {round(x - ax, 4):g} {round(y - ay, 4):g})" for x, y in pts)
    return (f'  (pad "{num}" smd custom (at {ax:g} {ay:g}) (size 0.2 0.2)\n    (layers {layers}){extra}\n'
            f'    (options (clearance outline) (anchor rect))\n    (primitives (gr_poly (pts {rel}) (width 0) (fill yes))))\n')
def rpad(num, x1, y1, x2, y2, layers, extra=""):
    return (f'  (pad "{num}" smd rect (at {(x1 + x2) / 2:g} {(y1 + y2) / 2:g}) (size {round(x2 - x1, 4):g} {round(y2 - y1, 4):g})'
            f' (layers {layers}){extra})\n')

graphics = ('  (fp_poly (pts (xy -2.5 -2.5) (xy 3 -2.5) (xy 3 2.5) (xy -3 2.5) (xy -3 -2)) '
            '(stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))\n')
for a, b in (((-3.45, -2.95), (3.45, -2.95)), ((3.45, -2.95), (3.45, 2.95)), ((3.45, 2.95), (-3.45, 2.95)),
             ((-3.45, 2.95), (-3.45, -2.95))):
    graphics += f'  (fp_line (start {a[0]:g} {a[1]:g}) (end {b[0]:g} {b[1]:g})\n    (stroke (width 0.1) (type default)) (layer "F.SilkS"))\n'
graphics += '  (fp_circle (center -3.8 -0.5) (end -3.68 -0.5)\n    (stroke (width 0.1) (type default)) (fill solid) (layer "F.SilkS"))\n'
model = f'  (model "{MODEL}"\n    (offset (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)))\n'

main = ""
for n in ("6", "17"): main += custom(n, SHAPE[n], ALL)
for n in ("22", "7", "10"):
    x1, y1, x2, y2 = CU[n]
    main += custom(n, rect(x1, y1, x2, y2), '"F.Cu"')
    main += custom(n, SHAPE[n], ALL, " (solder_mask_margin 0)")
for y in (-0.695, 0.545):
    for x in (-1.18, 0.0, 1.18):
        main += f'  (pad "" smd roundrect (at {x:g} {y:g}) (size 0.98 1.04) (layers "F.Paste") (roundrect_rratio {rr(0.98, 1.04)}))\n'
lbr = ""
for n in ("6", "17"):
    for r_ in RECTS[n]: lbr += rpad(n, *r_, ALL)
for n in ("22", "7", "10"):
    lbr += rpad(n, *CU[n], '"F.Cu"', " (solder_mask_margin -1.2) (solder_paste_margin -1.2)")
    for r_ in RECTS[n]: lbr += rpad(n, *r_, ALL, " (solder_mask_margin 0)")

def finish(extra):
    i = base.rstrip().rfind(")")
    return base[:i] + graphics + extra + model + ")\n"
open(os.path.join(out, f"{NAME}.kicad_mod"), "w").write(finish(main))
open(os.path.join(out, f"{NAME}-lbr-input.kicad_mod"), "w").write(finish(lbr))
json.dump({"vias": VIAS, "rects": RECTS, "copper_only": CU, "ep": {"x": 0, "y": -0.075, "w": 3.4, "h": 2.35},
           "ep_paste": [[x, y, 0.98, 1.04] for y in (-0.695, 0.545) for x in (-1.18, 0.0, 1.18)]},
          open(os.path.join(out, "fp-extra.json"), "w"), indent=1)
print("OK", NAME, "and lbr-input written to", out)