main
Caleb Publish 0.1.1 9577395 16h ago
#!/usr/bin/env python3
"""KiCad 10 footprints for the Molex right-angle headers, from each sales drawing's recommended PCB layout.
Pad 1 at the origin, positions along +Y; the body outline (F.Fab, F.SilkS clipped clear of the pads, F.CrtYd)
comes from the same dimensions the 3D builder uses (molex_ra.py). Model: ${KIPRJMOD}/<slug>.step, identity.

usage: molex_fp.py <series> <circuits> <slug> <out.kicad_mod>
"""
import sys, uuid

SILK_W, FAB_W, CRT_W = 0.12, 0.10, 0.05
SILK_OFF, CRT_OFF, PAD_CLR = 0.11, 0.50, 0.20


def U():
    return str(uuid.uuid4())


def r2(v):
    return f'{round(v + 0.0, 3):g}'


def line(a, b, layer, w):
    return (f'\t(fp_line\n\t\t(start {r2(a[0])} {r2(a[1])})\n\t\t(end {r2(b[0])} {r2(b[1])})\n'
            f'\t\t(stroke (width {w}) (type solid))\n\t\t(layer "{layer}")\n\t\t(uuid "{U()}")\n\t)\n')


def poly(pts, layer, w, fill=False):
    p = ' '.join(f'(xy {r2(x)} {r2(y)})' for x, y in pts)
    return (f'\t(fp_poly\n\t\t(pts {p})\n\t\t(stroke (width {w}) (type solid))\n\t\t(fill {"yes" if fill else "no"})\n'
            f'\t\t(layer "{layer}")\n\t\t(uuid "{U()}")\n\t)\n')


def rect_lines(x0, y0, x1, y1, layer, w):
    c = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
    return ''.join(line(c[i], c[(i + 1) % 4], layer, w) for i in range(4))


def text_prop(name, val, at, layer, hide=True, size=1.0):
    return (f'\t(property "{name}" "{val}"\n\t\t(at {r2(at[0])} {r2(at[1])} 0)\n\t\t(layer "{layer}")\n'
            + ('\t\t(hide yes)\n' if hide else '') +
            f'\t\t(uuid "{U()}")\n\t\t(effects\n\t\t\t(font (size {size} {size}) (thickness {0.15 if size >= 1 else 0.1}))\n\t\t)\n\t)\n')


def pad(num, x, y, size, drill, first=False, npth=False):
    if npth:
        return (f'\t(pad "" np_thru_hole circle\n\t\t(at {r2(x)} {r2(y)})\n\t\t(size {r2(size)} {r2(size)})\n'
                f'\t\t(drill {r2(drill)})\n\t\t(layers "*.Cu" "*.Mask")\n\t\t(uuid "{U()}")\n\t)\n')
    shape = 'roundrect' if first else 'circle'
    rr = '\t\t(roundrect_rratio 0.143678)\n' if first else ''
    return (f'\t(pad "{num}" thru_hole {shape}\n\t\t(at {r2(x)} {r2(y)})\n\t\t(size {r2(size)} {r2(size)})\n'
            f'\t\t(drill {r2(drill)})\n\t\t(layers "*.Cu" "*.Mask")\n\t\t(remove_unused_layers no)\n{rr}'
            f'\t\t(uuid "{U()}")\n\t)\n')


def clip_hlines(y, x0, x1, keep_out):
    """Horizontal silk segment from x0 to x1 at y, minus intervals that come within PAD_CLR of any pad circle."""
    segs = [(min(x0, x1), max(x0, x1))]
    for (px, py, r) in keep_out:
        if abs(py - y) >= r + PAD_CLR: continue
        dx = ((r + PAD_CLR) ** 2 - (py - y) ** 2) ** 0.5
        a, b = px - dx, px + dx
        out = []
        for s, e in segs:
            if e <= a or s >= b: out.append((s, e))
            else:
                if s < a: out.append((s, a))
                if e > b: out.append((b, e))
        segs = out
    return [(s, e) for s, e in segs if e - s > 0.5]


def clip_vlines(x, y0, y1, keep_out):
    return [(s, e) for s, e in clip_hlines(x, y0, y1, [(py, px, r) for px, py, r in keep_out])]


def outline_silk(x0, y0, x1, y1, keep_out):
    """Rectangle offset by SILK_OFF, each side clipped clear of pads."""
    x0, y0, x1, y1 = x0 - SILK_OFF, y0 - SILK_OFF, x1 + SILK_OFF, y1 + SILK_OFF
    out = ''
    for y in (y0, y1):
        for s, e in clip_hlines(y, x0, x1, keep_out): out += line((s, y), (e, y), 'F.SilkS', SILK_W)
    for x in (x0, x1):
        for s, e in clip_vlines(x, y0, y1, keep_out): out += line((x, s), (x, e), 'F.SilkS', SILK_W)
    return out


def poly_silk(pts, keep_out):
    """Axis-aligned closed outline (already offset outward), each edge clipped clear of pads."""
    out = ''
    for i in range(len(pts)):
        (ax, ay), (bx, by) = pts[i], pts[(i + 1) % len(pts)]
        if abs(ay - by) < 1e-9:
            for s, e in clip_hlines(ay, ax, bx, keep_out): out += line((s, ay), (e, ay), 'F.SilkS', SILK_W)
        else:
            for s, e in clip_vlines(ax, ay, by, keep_out): out += line((ax, s), (ax, e), 'F.SilkS', SILK_W)
    return out


def spec(series, n):
    """Return dict: pads [(num, x, y)], pad size, drill, npth [(x,y,d)], fab rects [(x0,y0,x1,y1)],
    silk rects, courtyard extra rects, pin-1 marker side, descr."""
    if series == '70553':
        p = 2.54; A = (n - 1) * p
        B = {2: 5.33, 3: 8.13, 4: 10.67, 9: 23.37}[n]; W = B + 2.04
        ut = 13.21 - 0.32
        yc = A / 2
        pads = [(str(k + 1), 0.0, k * p) for k in range(n)]
        fab = [(ut - 11.30, yc - W / 2, ut, yc + W / 2), (ut - 12.45, yc - B / 2, ut - 11.30, yc + B / 2),
               (ut - 13.59, yc - B / 2, ut - 12.45, yc + B / 2)]
        silk = [fab[0]]
        return dict(pads=pads, size=1.74, drill=1.09, npth=[], fab=fab, silk=silk, mark=-1,
                    descr=f'Molex SL right-angle header with latch, 70553-{ {2: "0001", 4: "0003", 9: "0008"}[n] }, '
                          f'1x{n:02d}, 2.54 mm pitch, no pegs; pads, drill and body per Molex SDA-70553-XXXX rev M '
                          f'(recommended PC board layout, 1.09 mm holes for a 1.57 mm board)',
                    tags='Molex SL 70553 right angle header latch 2.54mm THT')
    if series == '74164':
        p = 2.54; ncol = n // 2; Bs = (ncol - 1) * p; K = Bs + 2 * 3.555; P = 15.54; Wo = P + 4.26
        uA = 1.44 + 11.46 + 2.54; yc = Bs / 2
        pads = []
        for k in range(ncol):
            pads += [(str(2 * k + 1), 0.0, k * p), (str(2 * k + 2), p, k * p)]
        xpeg = uA - 1.44
        npth = [(xpeg, yc - K / 2, 2.36), (xpeg, yc + K / 2, 2.36)]
        fab = [(uA - 11.43, yc - Wo / 2, uA, yc + Wo / 2), (uA - 16.23, yc - P / 2, uA - 11.43, yc + P / 2)]
        o = SILK_OFF; xm, xr, xf = uA - 11.43, uA - 16.23, uA
        silk_poly = [(xr - o, yc - P / 2 - o), (xm - o, yc - P / 2 - o), (xm - o, yc - Wo / 2 - o), (xf + o, yc - Wo / 2 - o),
                     (xf + o, yc + Wo / 2 + o), (xm - o, yc + Wo / 2 + o), (xm - o, yc + P / 2 + o), (xr - o, yc + P / 2 + o)]
        silk = []
        keepout = (-0.8, -4.8, -0.8 + 16.3, Bs + 4.8)
        return dict(pads=pads, size=1.70, drill=0.99, npth=npth, fab=fab, silk=silk, mark=-1, keepout=keepout,
                    silk_poly=silk_poly,
                    descr='Molex C-Grid right-angle shrouded header with PC-board stab pegs, 74164-0110, 2x05, '
                          '2.54 mm pitch; pads, peg holes and body per Molex SD-74164-001 rev B1 (recommended P.C. '
                          'board hole layout; keep-out of note 10 drawn as the courtyard). Pins: odd = row A, '
                          'even = row B, two per position',
                    tags='Molex C-Grid 74164 right angle shrouded header pegs 2.54mm THT')
    if series == '87833':
        p = 2.00; ncol = n // 2
        Atab, Btab, Ctab = {8: (10.65, 6.00, 9.00), 10: (12.65, 8.00, 11.00)}[n]
        u1 = 4.70 + 1.70 + 2.00; yc = Btab / 2
        pads = []
        for k in range(ncol):
            pads += [(str(2 * k + 1), 0.0, k * p), (str(2 * k + 2), p, k * p)]
        fab = [(u1 - 4.70, yc - Atab / 2, u1, yc + Atab / 2), (u1 - 6.40, yc - Ctab / 2, u1 - 4.70, yc + Ctab / 2)]
        silk = [fab[0]]
        return dict(pads=pads, size=1.40, drill=0.90, npth=[], fab=fab, silk=silk, mark=-1,
                    descr=f'Molex Milli-Grid right-angle shrouded header, 87833-{n:02d}32, 2x{ncol:02d}, 2.00 mm pitch, '
                          'centre friction lock, dual side slots; pads, drill and body per Molex SD-87833-0001 rev '
                          'A15 (recommended PCB layout, 0.90 mm holes). CKT 1 and 2 share the first position',
                    tags='Molex Milli-Grid 87833 right angle shrouded header 2.00mm THT')
    if series == '90130':
        p = 2.54; ncol = n // 2; A = (ncol - 1) * p; Bw = 12.50
        u1 = 7.68 + 2.858 + 2.54; yc = A / 2
        pads = []
        for k in range(ncol):
            pads += [(str(2 * k + 1), 0.0, k * p), (str(2 * k + 2), -p, k * p)]
        fab = [(-u1, yc - Bw / 2, -(u1 - 10.90), yc + Bw / 2)]
        silk = fab
        return dict(pads=pads, size=1.70, drill=1.00, npth=[], fab=fab, silk=silk, mark=+1,
                    descr='Molex C-Grid III dual-row shrouded header, right angle (style 3), 90130-3108, 2x04, '
                          '2.54 mm pitch; pads, drill and body per Molex SDA-90130 rev CA6 sheet 3 (recommended P.C. '
                          'board hole pattern, 1.00 mm holes for a 1.60 mm board). CKT 1 and 2 share the first position',
                    tags='Molex C-Grid III 90130 right angle shrouded header 2.54mm THT')
    raise SystemExit('unknown series')


def build(series, n, slug):
    s = spec(series, n)
    pr = s['size'] / 2
    keep = [(x, y, pr) for _, x, y in s['pads']] + [(x, y, d / 2) for x, y, d in s['npth']]
    xs = [x for _, x, _ in s['pads']]; ys = [y for _, _, y in s['pads']]
    bx0 = min([r[0] for r in s['fab']] + [x - pr for x in xs] + [x - d / 2 for x, _, d in s['npth']])
    bx1 = max([r[2] for r in s['fab']] + [x + pr for x in xs] + [x + d / 2 for x, _, d in s['npth']])
    by0 = min([r[1] for r in s['fab']] + [y - pr for y in ys] + [y - d / 2 for _, y, d in s['npth']])
    by1 = max([r[3] for r in s['fab']] + [y + pr for y in ys] + [y + d / 2 for _, y, d in s['npth']])
    if 'keepout' in s:
        k = s['keepout']; bx0, by0, bx1, by1 = min(bx0, k[0]), min(by0, k[1]), max(bx1, k[2]), max(by1, k[3])
    c0x, c0y, c1x, c1y = [round(v / 0.01) * 0.01 for v in (bx0 - CRT_OFF, by0 - CRT_OFF, bx1 + CRT_OFF, by1 + CRT_OFF)]
    o = f'(footprint "{slug}"\n\t(version 20241229)\n\t(generator "adom_molex_fp")\n\t(layer "F.Cu")\n'
    o += f'\t(descr "{s["descr"]}")\n\t(tags "{s["tags"]}")\n'
    o += text_prop('Reference', 'REF**', (bx0 + (bx1 - bx0) / 2, c0y - 0.9), 'F.SilkS', hide=True)
    o += text_prop('Value', slug, (bx0 + (bx1 - bx0) / 2, c1y + 0.9), 'F.Fab', hide=True)
    o += text_prop('Datasheet', '', (0, 0), 'F.Fab', hide=True, size=1.27)
    o += text_prop('Description', s['descr'].split(';')[0], (0, 0), 'F.Fab', hide=True, size=1.27)
    o += '\t(attr through_hole)\n\t(duplicate_pad_numbers_are_jumpers no)\n'
    # fab: body rectangles + exposed pin runs + pin-1 marker
    for r in s['fab']:
        o += rect_lines(*r, 'F.Fab', FAB_W)
    m = s['mark']
    o += poly([(m * 0.9, -0.5), (m * 0.9, 0.5), (m * 0.25, 0.0)], 'F.Fab', FAB_W)
    # silk: body outlines clipped clear of pads, pin-1 triangle outside the pad field
    for r in s['silk']:
        o += outline_silk(*r, keep)
    if s.get('silk_poly'):
        o += poly_silk(s['silk_poly'], keep)
    pr1 = pr + PAD_CLR + 0.15
    o += poly([(m * (pr1 + 0.65), -0.4), (m * (pr1 + 0.65), 0.4), (m * pr1, 0.0)], 'F.SilkS', SILK_W, fill=True)
    # courtyard
    o += rect_lines(c0x, c0y, c1x, c1y, 'F.CrtYd', CRT_W)
    if 'keepout' in s:
        k = s['keepout']
        o += rect_lines(*k, 'F.Fab', FAB_W)
        o += (f'\t(fp_text user "KEEP OUT (Molex note 10)"\n\t\t(at {r2((k[0] + k[2]) / 2)} {r2(k[3] - 1.2)} 0)\n'
              f'\t\t(layer "F.Fab")\n\t\t(uuid "{U()}")\n\t\t(effects\n\t\t\t(font (size 0.8 0.8) (thickness 0.1))\n\t\t)\n\t)\n')
    o += (f'\t(fp_text user "${{REFERENCE}}"\n\t\t(at {r2(sum(r[0] + r[2] for r in s["fab"][:1]) / 2)} '
          f'{r2(sum(r[1] + r[3] for r in s["fab"][:1]) / 2)} 90)\n\t\t(layer "F.Fab")\n\t\t(uuid "{U()}")\n'
          f'\t\t(effects\n\t\t\t(font (size 1 1) (thickness 0.15))\n\t\t)\n\t)\n')
    for i, (num, x, y) in enumerate(s['pads']):
        o += pad(num, x, y, s['size'], s['drill'], first=(num == '1'))
    for x, y, d in s['npth']:
        o += pad('', x, y, d, d, npth=True)
    o += '\t(embedded_fonts no)\n'
    o += (f'\t(model "${{KIPRJMOD}}/{slug}.step"\n\t\t(offset (xyz 0 0 0))\n\t\t(scale (xyz 1 1 1))\n'
          f'\t\t(rotate (xyz 0 0 0))\n\t)\n)\n')
    return o


if __name__ == '__main__':
    series, n, slug, out = sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4]
    open(out, 'w').write(build(series, n, slug))
    print('wrote', out)