main
John Lauer Component record for rc0402fr-0733kl: files batch 1 dea64e1 21h ago
"""Compare the KiCad footprint pads with the manufacturer's recommended land pattern.

usage: landcheck.py <slug> <outdir>
Reads parts.PARTS[slug]["land"] and the page's footprint (KiCad library geometry).
Writes <outdir>/land-pattern.png (true-scale drawing + table) and
<outdir>/land-pattern-icon.png (256 px square thumbnail), prints a JSON result.

Drawing: grey = part body at datasheet nominal, teal = KiCad pads (copper),
amber dashed = the maker's land at the middle of its printed range (or its
printed nominal), amber band = the range the maker allows for the inner and
outer pad edges where the maker prints a range.
"""
import sys, os, json, re, glob
from PIL import Image, ImageDraw, ImageFont
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from parts import PARTS

slug, outdir = sys.argv[1], sys.argv[2]
P = next(p for p in PARTS if p["slug"] == slug)
L = P["land"]
page = os.path.dirname(outdir.rstrip("/"))
fpf = glob.glob(os.path.join(page, "*.kicad_mod"))[0]
fp = open(fpf).read()
pads = [tuple(map(float, m)) for m in re.findall(r'\(pad "\d" smd roundrect\s*\(at ([-\d.]+) ([-\d.]+)\)\s*\(size ([\d.]+) ([\d.]+)\)', fp)]
x, _, w, h = pads[1]
k = {"gap": round(2 * x - w, 3), "len": round(w, 3), "wid": round(h, 3), "overall": round(2 * x + w, 3)}

def mid(v): return (v[0] + v[1]) / 2 if isinstance(v, list) else v
rows = []
if L["kind"] in ("range", "tol"):
    rng = {"gap": L["gap_a"], "len": L["len_b"], "wid": L["wid_c"]}
    rng["overall"] = L.get("overall") or [round(L["gap_a"][0] + 2 * L["len_b"][0], 3), round(L["gap_a"][1] + 2 * L["len_b"][1], 3)]
    derived_overall = not L.get("overall")
    for key, name in (("gap", "gap between lands"), ("len", "land length"), ("wid", "land width"),
                      ("overall", "overall length (a + 2b, derived)" if derived_overall else "overall length")):
        lo, hi = rng[key]; v = k[key]
        ok = lo - 1e-6 <= v <= hi + 1e-6
        dev = 0 if ok else round(v - hi if v > hi else v - lo, 3)
        rows.append({"dim": name, "kicad_mm": v, "maker_min": lo, "maker_max": hi, "within": ok, "outside_by_mm": dev})
    ref = {kk: mid(rng[kk]) for kk in rng}
    if L["kind"] == "tol":
        ref = {"gap": L["nominal"]["gap_a"], "len": L["nominal"]["len_b"], "wid": L["nominal"]["wid_c"], "overall": L["nominal"]["overall"]}
else:
    ref = {"gap": L["gap_a"], "len": L["len_b"], "wid": L["wid_c"], "overall": L["overall"]}
    for key, name in (("gap", "gap between lands"), ("len", "land length"), ("wid", "land width"), ("overall", "overall length")):
        rows.append({"dim": name, "kicad_mm": k[key], "maker_nominal": ref[key], "difference_mm": round(k[key] - ref[key], 3)})

# ---------- drawing ----------
body = P["body_mm"]
span = max(k["overall"], ref["overall"] if isinstance(ref["overall"], float) else 0) + 0.4
PP = 560 / span
W_, H_ = 1000, int(max(k["wid"], ref["wid"], body["y_mm"]) * PP + 120)
img = Image.new("RGBA", (W_, H_), (255, 255, 255, 255))
d = ImageDraw.Draw(img, "RGBA")
cx, cy = W_ / 2, H_ / 2 + 10
def R(x0, y0, x1, y1): return [cx + x0 * PP, cy - y1 * PP, cx + x1 * PP, cy - y0 * PP]
# allowed edge bands (range kinds)
if L["kind"] in ("range", "tol"):
    g0, g1 = L["gap_a"]; b0, b1 = L["len_b"]; c1 = L["wid_c"][1]
    for s in (-1, 1):
        for a, b in ((g0 / 2, g1 / 2), (g0 / 2 + b0, g1 / 2 + b1)):
            xa, xb = sorted((s * a, s * b))
            d.rectangle(R(xa, -c1 / 2, xb, c1 / 2), fill=(255, 190, 70, 70))
# body
d.rectangle(R(-body["x_mm"] / 2, -body["y_mm"] / 2, body["x_mm"] / 2, body["y_mm"] / 2), outline=(110, 110, 110, 255), width=2)
# KiCad pads, rounded rect (rratio 0.25)
for px, py, pw, ph in pads:
    rr = 0.25 * min(pw, ph) * PP
    d.rounded_rectangle(R(px - pw / 2, py - ph / 2, px + pw / 2, py + ph / 2), radius=rr, fill=(0, 150, 150, 110), outline=(0, 120, 120, 255), width=2)
# maker land, dashed amber
def dashed(box, col, wdt=3):
    xa, ya, xb, yb = box
    for (p, q) in (((xa, ya), (xb, ya)), ((xb, ya), (xb, yb)), ((xb, yb), (xa, yb)), ((xa, yb), (xa, ya))):
        n = int(max(abs(q[0] - p[0]), abs(q[1] - p[1])) / 7) or 1
        for i in range(0, n, 2):
            t0, t1 = i / n, min(1, (i + 1) / n)
            d.line([(p[0] + (q[0] - p[0]) * t0, p[1] + (q[1] - p[1]) * t0), (p[0] + (q[0] - p[0]) * t1, p[1] + (q[1] - p[1]) * t1)], fill=col, width=wdt)
for s in (-1, 1):
    a, b = ref["gap"] / 2, ref["gap"] / 2 + ref["len"]
    xa, xb = sorted((s * a, s * b))
    dashed(R(xa, -ref["wid"] / 2, xb, ref["wid"] / 2), (200, 110, 0, 255))
icon_box = (int(cx - 0.5 * (k["overall"] + 0.25) * PP), int(cy - 0.5 * (k["overall"] + 0.25) * PP))
icon_side = int((k["overall"] + 0.25) * PP)
padded = Image.new("RGBA", (W_ + 2 * icon_side, H_ + 2 * icon_side), (255, 255, 255, 255)); padded.paste(img, (icon_side, icon_side))
icon = padded.crop((icon_box[0] + icon_side, icon_box[1] + icon_side, icon_box[0] + 2 * icon_side, icon_box[1] + 2 * icon_side)).convert("RGB").resize((256, 256), Image.LANCZOS)
mask = Image.new("L", (256, 256), 0); ImageDraw.Draw(mask).rounded_rectangle([0, 0, 255, 255], radius=28, fill=255)
ic = Image.new("RGBA", (256, 256), (0, 0, 0, 0)); ic.paste(icon, (0, 0), mask)
ic.save(os.path.join(outdir, "land-pattern-icon.png"))

f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 15)
fb = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 17)
lines = []
for r in rows:
    if "within" in r:
        lines.append((f"{r['dim']}:  KiCad {r['kicad_mm']:.2f}   maker {r['maker_min']:.2f} to {r['maker_max']:.2f}   "
                      + ("inside" if r["within"] else f"outside by {r['outside_by_mm']:+.2f}"), (0, 120, 40) if r["within"] else (190, 90, 0)))
    else:
        lines.append((f"{r['dim']}:  KiCad {r['kicad_mm']:.2f}   maker {r['maker_nominal']:.2f}   difference {r['difference_mm']:+.2f}", (40, 40, 40)))
sheet = Image.new("RGB", (W_, H_ + 100 + 24 * len(lines) + 70), (255, 255, 255))
sd = ImageDraw.Draw(sheet)
sd.text((14, 10), f"{P['mpn']}: KiCad {P['fp']} pads vs maker's land, true scale", fill=(0, 0, 0), font=fb)
sheet.paste(img.convert("RGB"), (0, 40))
y = H_ + 46
sd.text((14, y), "grey = body (nominal)   teal = KiCad pads   amber dashed = maker's land (middle of range or nominal)", fill=(60, 60, 60), font=f); y += 22
sd.text((14, y), "amber band = where the maker allows the inner and outer pad edges (range sheets only)", fill=(60, 60, 60), font=f); y += 26
for t, c in lines:
    sd.text((14, y), t, fill=c, font=f); y += 24
import textwrap
for ln in textwrap.wrap("Source: " + L["source"], 115):
    sd.text((14, y + 4), ln, fill=(90, 90, 90), font=f); y += 20
sheet.save(os.path.join(outdir, "land-pattern.png"))

res = {"footprint": os.path.basename(fpf), "kicad_library_footprint": P["fp"], "kicad": k, "maker_reference": ref,
       "maker_source": L["source"], "maker_letters": L["letters"], "rows": rows,
       "all_within": all(r.get("within", True) for r in rows) if L["kind"] != "nominal" else None}
print(json.dumps(res))