master
John Lauer planner sync: gate self-test works on a copy of the good etch b850e82 18d ago
#!/usr/bin/env python3
"""Prove the legibility gate catches the bad marks AIs kept making, from one good etch:
  across     etched with --along across the short side       -> FAIL axis ("Text runs across ...")
  upside     the good mark turned 180 deg in place             -> FAIL upright ("flip 180")
  small      the good mark at 0.55 x                           -> FAIL size ("increase to ~...")
  raised     the good mark lifted 0.1 mm off the face          -> FAIL flat
  outline    an outline whose name runs across the short side  -> FAIL outline (with --bad-outline)
Usage: test_mark_gate.py --step part.step --footprint fp.kicad_mod --mpn MPN --out-dir DIR [--good good-etched.step]
       [--bad-outline bad.svg]   (needs the shared service; writes DIR/*.check.json|png and DIR/results.json)"""
import argparse, json, os, shutil, sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import step_text as ST
import mark_etch as ME
import mark_check as MC
import mark_geom as MG


def variant(good, out, fn):
    s = ST.Step(good); s.flatten()
    a = MG.analyse(s, "Adom.LaserEtch")
    fn(s, a); s.write(out)


def main():
    ap = argparse.ArgumentParser(); ap.add_argument("--step", required=True); ap.add_argument("--footprint", required=True)
    ap.add_argument("--mpn", required=True); ap.add_argument("--out-dir", required=True); ap.add_argument("--good"); ap.add_argument("--bad-outline")
    a = ap.parse_args()
    os.makedirs(a.out_dir, exist_ok=True)
    d = lambda n: os.path.join(a.out_dir, f"{a.mpn}-{n}.step")
    good = d("good")
    if a.good:                              # work on a copy: the gate writes reports beside the STEP it checks
        shutil.copyfile(a.good, good)
    else:
        ME.etch(a.step, a.mpn, good, a.footprint)
    plan = ME.MP.plan(a.step, a.footprint, a.mpn)
    cases = {}
    # across the short side: the override the old tools defaulted to
    long_axis = plan["longAxis"]["axis"]
    ME.etch(a.step, a.mpn, d("across"), a.footprint, along="x" if long_axis == "y" else "y")
    cases["across"] = d("across")
    variant(good, d("upside"), lambda s, m: s.transform_product("Adom.LaserEtch", m["centre"], 1.0, 2))
    variant(good, d("small"), lambda s, m: s.transform_product("Adom.LaserEtch", m["centre"], 0.55, 0))
    variant(good, d("raised"), lambda s, m: s.transform_product("Adom.LaserEtch", m["centre"], 1.0, 0, (0, 0, 0.1)))
    cases.update(upside=d("upside"), small=d("small"), raised=d("raised"))
    results = {}
    rep = MC.check(good, plan=plan, footprint=a.footprint, quiet=True)
    results["good"] = dict(status=rep["status"], checks={c["check"]: [c["status"], c["detail"]] for c in rep["checks"]}, image=rep.get("image"))
    for k, p in cases.items():
        rep = MC.check(p, plan=plan, footprint=a.footprint, quiet=True)
        results[k] = dict(status=rep["status"], fails=[f"{c['status']} {c['check']}: {c['detail']}" for c in rep["checks"] if c["status"] != "PASS"], image=rep.get("image"))
    if a.bad_outline:
        rep = MC.check(good, plan=plan, footprint=a.footprint, outline=a.bad_outline, quiet=True, render=False)
        results["outline"] = dict(status=rep["status"], fails=[f"{c['status']} {c['check']}: {c['detail']}" for c in rep["checks"] if c["status"] != "PASS"], file=a.bad_outline)
    json.dump(results, open(os.path.join(a.out_dir, "results.json"), "w"), indent=1)
    ok = results["good"]["status"] == "PASS" and all(results[k]["status"] == "FAIL" for k in results if k != "good")
    for k, r in results.items():
        print(f"{k:8s} {r['status']}")
        for f in r.get("fails", []):
            print("         Hint: " + f)
    print("gate test:", "OK (good passes, every bad case fails)" if ok else "PROBLEM")
    return 0 if ok else 1


if __name__ == "__main__":
    sys.exit(main())