main
John Lauer Component record for rc0402fr-0733kl: files batch 1 dea64e1 21h ago
"""Build one component page tree per part in parts.PARTS.

usage: build.py <slug> [--skip-remote]
Writes /tmp/claude-1001/passives5/pages/<slug>/ . Downloads are read from
/tmp/claude-1001/passives5/dl (never executed); scripts live in this folder.
"""
import sys, os, json, shutil, subprocess, hashlib, re
import numpy as np
from PIL import Image, ImageDraw, ImageFont

ROOT = "/tmp/claude-1001/esc3"
TOOLS = os.path.join(ROOT, "tools")
DL = os.path.join(ROOT, "dl")
PY = "/home/adom/.venvs/cq/bin/python"
sys.path.insert(0, TOOLS)
from parts import PARTS, TODAY

slug = sys.argv[1]
P = next(p for p in PARTS if p["slug"] == slug)
OUT = os.path.join(ROOT, "pages", slug)
os.makedirs(OUT, exist_ok=True)
MPN = P["mpn"]
sha = lambda f: hashlib.sha256(open(f, "rb").read()).hexdigest()
def gitblob(f):
    b = open(f, "rb").read()
    return hashlib.sha1(b"blob %d\0" % len(b) + b).hexdigest()
def run(cmd, **kw):
    r = subprocess.run(cmd, capture_output=True, text=True, **kw)
    if r.returncode != 0:
        print("FAILED", cmd, r.stdout[-2000:], r.stderr[-2000:]); sys.exit(1)
    return r
F = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FB = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"

for d in ("docs/verify", "docs/models", "docs/figures", "lod", "provenance/tools", "land-pattern", "altium"):
    os.makedirs(os.path.join(OUT, d), exist_ok=True)

# ---------- STEP (KiCad library bytes, unchanged) ----------
src_step = os.path.join(DL, "kicad", P["fp"] + ".step")
step_name = f"{MPN}.step"
step_path = os.path.join(OUT, step_name)
shutil.copyfile(src_step, step_path)
assert sha(step_path) == sha(src_step)

# ---------- footprint (KiCad library geometry, renamed + local model path) ----------
fp_name = f"{P['fp']}_{MPN}"
src_fp = open(os.path.join(DL, "kicad", P["fp"] + ".kicad_mod")).read()
fp = src_fp.replace(f'(footprint "{P["fp"]}"', f'(footprint "{fp_name}"', 1)
fp = re.sub(r'\(model "[^"]+"', f'(model "${{KIPRJMOD}}/{step_name}"', fp)
assert fp.count(fp_name) == 1 and "${KIPRJMOD}/" + step_name in fp
fp_file = f"{fp_name}.kicad_mod"
open(os.path.join(OUT, fp_file), "w").write(fp)
pads = [(float(a), float(b), float(c), float(d)) for a, b, c, d in
        re.findall(r'\(pad "\d" smd roundrect\s*\(at ([-\d.]+) ([-\d.]+)\)\s*\(size ([\d.]+) ([\d.]+)\)', fp)]
assert len(pads) == 2, pads

# ---------- symbol (original, conventional passive graphics) ----------
d = P["dist"]
def prop(name, val, y, hide=True):
    h = " hide" if hide else ""
    return f'    (property "{name}" "{val}" (at 0 {y} 0) (effects (font (size 1.27 1.27)){h}))\n'
ref = P["kind"]
desc = P["brief"]
props = (prop("Reference", ref, 0 if False else 0, False).replace("(at 0 0 0)", "(at 2.54 1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")
         + prop("Value", P["value_sym"], 0, False).replace("(at 0 0 0)", "(at 2.54 -1.27 0)").replace("(font (size 1.27 1.27))", "(font (size 1.27 1.27)) (justify left)")
         + prop("Footprint", fp_name, -7.62) + prop("Datasheet", P["ds"]["url"], -10.16) + prop("Description", desc, -12.7)
         + prop("MPN", MPN, -15.24) + prop("Manufacturer", P["mfr"], -17.78)
         + prop("Mouser", d["mouser"][0], -20.32) + prop("DigiKey", d["digikey"][0], -22.86) + prop("LCSC", d["lcsc"][0], -25.4) + prop("Adom Wiki", f"https://wiki.adom.inc/adom/{slug}", -27.94))
if ref == "C":
    body = ('      (polyline (pts (xy -2.032 -0.762) (xy 2.032 -0.762)) (stroke (width 0.508) (type default)) (fill (type none)))\n'
            '      (polyline (pts (xy -2.032 0.762) (xy 2.032 0.762)) (stroke (width 0.508) (type default)) (fill (type none)))\n')
    plen = 2.794
else:
    body = '      (rectangle (start -1.016 -2.54) (end 1.016 2.54) (stroke (width 0.254) (type default)) (fill (type none)))\n'
    plen = 2.54
sym = f'''(kicad_symbol_lib (version 20241209) (generator "adom_passive_symbol")
  (symbol "{MPN}" (pin_numbers (hide yes)) (pin_names (offset 0.254) (hide yes)) (exclude_from_sim no) (in_bom yes) (on_board yes)
{props}    (property "ki_keywords" "{P['category'].lower()} {P['package'].split()[0]} {MPN}" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
    (symbol "{MPN}_0_1"
{body}    )
    (symbol "{MPN}_1_1"
      (pin passive line (at 0 5.08 270) (length {plen}) (name "~" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))))
      (pin passive line (at 0 -5.08 90) (length {plen}) (name "~" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))))
    )
  )
)
'''
sym_file = f"{MPN}.kicad_sym"
open(os.path.join(OUT, sym_file), "w").write(sym)

# ---------- datasheets ----------
shutil.copyfile(os.path.join(DL, P["ds"]["local"]), os.path.join(OUT, P["ds"]["file"]))
for x in P["extra_docs"]:
    shutil.copyfile(os.path.join(DL, x["local"]), os.path.join(OUT, x["file"]))

# ---------- tools copied into provenance ----------
for t in ("lod.py", "ortho.py", "overlay_chip.py", "build.py", "parts.py", "landcheck.py", "records.py", "finish.py", "figures.py", "etch.py"):
    if os.path.exists(os.path.join(TOOLS, t)):
        shutil.copyfile(os.path.join(TOOLS, t), os.path.join(OUT, "provenance/tools", t))

# ---------- overlay proof against the datasheet table ----------
ov = dict(P["overlay"])
crop_png, box = ov.pop("crop")
spec = {"label": MPN, "mpn": MPN, **ov, "crops": [[os.path.join(ROOT, "work/crops", crop_png), box]],
        "marking": (f"datasheet code {P['marking_code']} on the marked display copy; this check measures the unmarked STEP" if P.get("marking_code") else "none specified (" + P["marking_src"] + ")")}
spec_path = os.path.join(OUT, f"provenance/tools/{slug}-overlay-spec.json")
spec_pub = dict(spec); spec_pub["crops"] = [[f"datasheet page render ({crop_png}, 100 dpi)", box]]
json.dump(spec_pub, open(spec_path, "w"), indent=1)
tmp_spec = os.path.join(ROOT, "work", f"{slug}-spec.json"); json.dump(spec, open(tmp_spec, "w"))
r = run([PY, "-I", os.path.join(TOOLS, "overlay_chip.py"), tmp_spec, step_path, os.path.join(OUT, f"docs/verify/{slug}-overlay.png")], cwd=TOOLS)
check = json.loads(r.stdout.strip().splitlines()[-1]); check["step"] = step_name
json.dump(check, open(os.path.join(OUT, f"docs/verify/{slug}-check.json"), "w"), indent=1)
print("overlay all_pass:", check["all_pass"])
if not check["all_pass"]:
    print(json.dumps(check, indent=1)); sys.exit(2)

# ---------- land pattern comparison (KiCad pads vs maker's recommendation) ----------
r = run([PY, "-I", os.path.join(TOOLS, "landcheck.py"), slug, os.path.join(OUT, "land-pattern")], cwd=TOOLS)
land = json.loads(r.stdout.strip().splitlines()[-1])
json.dump(land, open(os.path.join(OUT, "land-pattern/land-check.json"), "w"), indent=1)

# ---------- datasheet marking (RC0603): marked display STEP via the step2glb laser-etch service ----------
disp_step = step_path; marked_name = None; etch_info = None
if P.get("marking_code"):
    marked_name = f"{MPN}-marked.step"
    r = run(["python3", "-I", os.path.join(TOOLS, "etch.py"), step_path, P["marking_code"], os.path.join(OUT, marked_name), slug])
    etch_info = json.loads(r.stdout.strip().splitlines()[-1])
    disp_step = os.path.join(OUT, marked_name)

# ---------- GLB (step2glb) ----------
glb_path = os.path.join(OUT, "package-reference.glb")
g = subprocess.run(["step2glb", "convert", disp_step, "-o", glb_path], capture_output=True, text=True)
print("step2glb exit", g.returncode, g.stderr[-300:])
assert g.returncode == 0 and os.path.getsize(glb_path) > 1000

# ---------- thumbnails (service render of the STEP) ----------
def thumb(out, wh=None):
    extra = ["--width", str(wh[0]), "--height", str(wh[1])] if wh else []
    t = subprocess.run(["step2glb", "thumbnail", disp_step, "-o", out] + extra, capture_output=True, text=True)
    return t.returncode, (t.stdout + t.stderr)[-300:]
hero_rc = thumb(os.path.join(OUT, "docs/hero.png"), (1600, 1000))
th_rc = thumb(os.path.join(OUT, "docs/models/package-reference-step.png"))
print("thumbs", hero_rc, th_rc)

# ---------- LODs ----------
run([PY, "-I", os.path.join(TOOLS, "lod.py"), disp_step, MPN, OUT, os.path.basename(disp_step)] + ([step_path] if marked_name else []), cwd=TOOLS)
man = json.load(open(os.path.join(OUT, "lod/manifest.json")))
man["source"]["path"] = os.path.basename(disp_step)
if marked_name:
    man["purpose"] = ("clean board-use component models: no footprint pads, outlines, labels or silkscreen; the datasheet marking code "
                      + P["marking_code"] + " on the top coat in high and medium, omitted in low and super-low")
    man["source"]["unmarked"] = {"path": step_name, "sha256": sha(step_path)}
else:
    man["purpose"] = "clean board-use component models: no footprint pads, outlines, labels or silkscreen; no marking (" + P["marking_src"] + ")"
json.dump(man, open(os.path.join(OUT, "lod/manifest.json"), "w"), indent=1)

run(["python3", "-I", os.path.join(TOOLS, "figures.py"), slug])
json.dump({"step": step_name, "marked": marked_name, "etch": etch_info, "fp": fp_file, "sym": sym_file, "check": check, "land": land,
           "hero_rc": hero_rc, "thumb_rc": th_rc, "glb_stderr": g.stderr[-300:]},
          open(os.path.join(ROOT, "work", f"{slug}-build.json"), "w"), indent=1)
print("built", OUT)