← Commit history

v0.9.8 place_footprint

John Lauer ·e4c3ab1426 ·3mo ago ·parent d663306
1 file changed +109
handlers/place_footprint.pyadded+109
@@ -0,0 +1,109 @@+"""Handler for place_footprint command.++DETERMINISTIC footprint placement onto a PCB — no AI, no fragile protobuf.+Approach: take the footprint's s-expression (a .kicad_mod is already a valid+`(footprint ...)` block), give it a position + uuid, splice it into a known-good+blank KiCad board template, and open the result in pcbnew. Proven 2026-06-25+(kipy get_footprints() confirmed the placed footprint + pad count).++This is a PREVIEW placement (a fresh board with just this footprint) — it never+touches the user's existing board, so it can't clobber their work. The footprint+is also already in the Adom library (via install_footprint), so the user can drag+it into their own board from there.++Args:+    fileName (required):    e.g. "BMI423.kicad_mod"+    fileContent (required): base64-encoded .kicad_mod content+    footprintName (opt):    parsed from the content if omitted+    x, y (opt):             placement in mm (default board-center-ish 148,105)+    open (opt):             open the board in pcbnew (default True)++Returns: {success, boardPath, footprintName, placedAt, opened, output}+"""++import base64+import re+import subprocess+import sys+import uuid+from pathlib import Path++sys.path.insert(0, str(Path(__file__).parent.parent))+from proc import run as _safe_run  # noqa: E402++_TEMPLATE = Path(__file__).resolve().parent.parent / "templates" / "blank-board.kicad_pcb"+++def handle_place_footprint(kicad_info: dict, args: dict) -> dict:+    if not kicad_info.get("installed"):+        return {"success": False, "error": "KiCad not installed",+                "_hint": "Install KiCad from https://www.kicad.org/download/ then retry."}++    file_name = args.get("fileName", "")+    file_content = args.get("fileContent", "")+    if not file_content:+        return {"success": False, "error": "No fileContent (base64 .kicad_mod) specified"}++    try:+        fp_text = base64.b64decode(file_content).decode("utf-8", "replace").strip()+    except Exception as e:+        return {"success": False, "error": f"could not decode fileContent: {e}"}+    if not fp_text.startswith("(footprint"):+        return {"success": False, "error": "fileContent is not a .kicad_mod (must start with '(footprint')"}++    fp_name = args.get("footprintName") or ""+    if not fp_name:+        m = re.search(r'\(footprint\s+"([^"]+)"', fp_text)+        fp_name = m.group(1) if m else Path(file_name).stem or "footprint"++    x = args.get("x", 148.0)+    y = args.get("y", 105.0)++    if not _TEMPLATE.exists():+        return {"success": False, "error": f"board template missing at {_TEMPLATE}"}+    skel = _TEMPLATE.read_text(encoding="utf-8")++    # Make the footprint a board item: inject a uuid + position right after its+    # first (layer "...") token, then splice before the board's final paren.+    uid = str(uuid.uuid4())+    fp_board = re.sub(+        r'(\(layer\s+"[^"]+"\))',+        r'\1\n\t\t(uuid "%s")\n\t\t(at %s %s)' % (uid, x, y),+        fp_text, count=1,+    )+    idx = skel.rstrip().rfind(")")+    combined = skel[:idx].rstrip() + "\n" + fp_board + "\n)\n"++    user_dir = kicad_info.get("user_dir") or str(Path.home())+    out_path = Path(user_dir) / f"adom-place-{fp_name}.kicad_pcb"+    try:+        out_path.write_text(combined, encoding="utf-8")+    except OSError as e:+        return {"success": False, "error": f"could not write board: {e}"}++    opened = False+    open_it = args.get("open", True)+    if open_it:+        pcbnew_exe = kicad_info.get("pcbnew_exe")+        if pcbnew_exe and Path(pcbnew_exe).exists():+            try:+                # detached launch so the bridge doesn't block on the GUI+                _safe_run([pcbnew_exe, str(out_path)], timeout=8)+                opened = True+            except subprocess.TimeoutExpired:+                opened = True  # launched; it just doesn't return (GUI stays up)+            except OSError as e:+                return {"success": True, "boardPath": str(out_path), "footprintName": fp_name,+                        "placedAt": [x, y], "opened": False,+                        "output": f"Board written but pcbnew launch failed: {e}",+                        "_hint": f"Open it manually: {out_path}"}++    return {+        "success": True,+        "boardPath": str(out_path),+        "footprintName": fp_name,+        "placedAt": [x, y],+        "opened": opened,+        "output": f"Placed footprint '{fp_name}' at ({x}, {y}) mm on a preview board"+                  + (" and opened it in pcbnew" if opened else ""),+    }