main
John Lauer Add KiCad symbol and footprint, viewers, provenance, README, models.json and verification log 354b2dd 11h ago
#!/usr/bin/env python3
"""Pin-1 proof: footprint pads vs model leads, and the model's bottom view next to the datasheet's Figure 1.

1. Reads the KiCad footprint pads and the model's lead solids (STEP), maps KiCad (x, y-down) to the model frame
   (x, -y) and checks every lead centre sits on its pad centre.
2. Draws the model seen from BELOW, turned so the tab points up as in Melexis datasheet Rev 021 p.6 Figure 1,
   labels each lead with the footprint pad number and the symbol pin name, and places it beside the datasheet
   figure (page crop) for comparison.
usage: pin1_proof.py model.step footprint.kicad_mod page6_png out.png out.json
"""
import json, math, re, sys
import cadquery as cq
from PIL import Image, ImageDraw, ImageFont

step, fp, p6, out_png, out_json = sys.argv[1:6]
NAMES = {'1': 'SCL', '2': 'SDA/PWM', '3': 'VDD', '4': 'VSS'}
CX, CY = 2.54, 0.0

pads = {m.group(1): (float(m.group(2)), float(m.group(3)))
        for m in re.finditer(r'\(pad "(\d+)" thru_hole \w+\s*\(at ([-\d.]+) ([-\d.]+)\)', open(fp).read())}
shape = cq.importers.importStep(step)
solids = shape.solids().vals()
leads = []
for s in solids:
    b = s.BoundingBox()
    if b.zmin < -10 and (b.xmax - b.xmin) < 0.6:          # a lead: thin and reaching below the board
        leads.append(((b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2))
if len(leads) != 4:   # leads fused into one solid: split by connected faces is overkill; use the bbox of each lead face
    leads = []
    for f in shape.faces().vals():
        b = f.BoundingBox()
        if abs(b.zmin - b.zmax) < 1e-6 and b.zmin < -12 and (b.xmax - b.xmin) < 0.6:
            leads.append(((b.xmin + b.xmax) / 2, (b.ymin + b.ymax) / 2))
res = {'pads_kicad': pads, 'leads_model': leads, 'pairs': {}, 'max_offset_mm': 0.0}
for n, (px, py) in pads.items():
    mx, my = px, -py
    d = min(math.hypot(mx - lx, my - ly) for lx, ly in leads)
    res['pairs'][n] = {'pad_model_xy': [mx, my], 'nearest_lead_offset_mm': round(d, 4)}
    res['max_offset_mm'] = max(res['max_offset_mm'], round(d, 4))
res['leads_on_pads'] = res['max_offset_mm'] < 0.01 and len(leads) == 4

# bottom view, tab up: mirror x (seen from below), then turn +45 deg (the tab at 135 deg in the top view goes to 90)
def bv(x, y):
    dx, dy = -(x - CX), (y - CY)
    a = math.radians(45)
    return dx * math.cos(a) - dy * math.sin(a), dx * math.sin(a) + dy * math.cos(a)

S, K = 900, 70.0
im = Image.new('RGB', (S, S), (255, 255, 255))
d = ImageDraw.Draw(im)
f = ImageFont.truetype('DejaVuSans.ttf', 30)
fs = ImageFont.truetype('DejaVuSans.ttf', 24)
def P(u, v): return (S / 2 + u * K, S / 2 + 40 - v * K)
# header + tab (from the model's dims)
dims = dict(header_r=4.575, tab_w=0.8, tab_tip=5.46, tab_angle=135)
d.ellipse([P(-dims['header_r'], dims['header_r']), P(dims['header_r'], -dims['header_r'])], outline=(0, 0, 0), width=3)
ta = math.radians(dims['tab_angle'])
tx, ty = CX + math.cos(ta) * dims['tab_tip'], CY + math.sin(ta) * dims['tab_tip']
u, v = bv(tx, ty)
d.rectangle([P(u - 0.4, v), P(u + 0.4, v - 0.9)], outline=(0, 0, 0), width=3)
for n, (px, py) in pads.items():
    u, v = bv(px, -py)
    d.ellipse([P(u - 0.6, v + 0.6), P(u + 0.6, v - 0.6)], outline=(40, 40, 40), width=3, fill=(200, 200, 200))
    d.text(P(u - 0.9, v + 1.5), f'{n} - {NAMES[n]}', fill=(200, 0, 0), font=f)
d.text((20, 15), 'Model bottom view (tab up), pad numbers from the footprint', fill=(0, 0, 0), font=fs)
d.text((20, S - 40), 'pin 1 = footprint pad 1 (oval pad); leads on pads: ' + ('PASS' if res['leads_on_pads'] else 'FAIL'),
       fill=(0, 120, 0) if res['leads_on_pads'] else (200, 0, 0), font=fs)
ds = Image.open(p6).convert('RGB')
W, H = ds.size
ds = ds.crop((int(W * 0.30), int(H * 0.27), int(W * 0.70), int(H * 0.56)))
ds = ds.resize((S, int(ds.height * S / ds.width)))
canvas = Image.new('RGB', (2 * S + 40, max(S, ds.height) + 60), (255, 255, 255))
canvas.paste(ds, (0, 50)); canvas.paste(im, (S + 40, 0))
ImageDraw.Draw(canvas).text((20, 10), 'Melexis MLX90614 datasheet Rev 021 p.6 Figure 1 (bottom view)', fill=(0, 0, 0), font=fs)
canvas.save(out_png)
json.dump(res, open(out_json, 'w'), indent=1)
print('OK', res['leads_on_pads'], res['max_offset_mm'])