master
John Lauer v0.9.109: kicad_export_part (one-off single-part library exports: symbol-only .kicad_sym, footprint-only .pretty, bare… 9d49110 2mo ago
"""export_part.py — kicad_export_part: write ONE-OFF library files for a single
installed part: a .kicad_sym library containing ONLY that symbol, a .pretty
containing ONLY that footprint, and the bare 3D model file.

Why (John, 2026-08-15): "an lbr file that JUST has the specific symbol only, or
footprint only, or 3d chip only. this is not really useful in the day to day
world, but it proves you can do one-off of these" — the reverse direction of
install: the bridge can extract each asset of a part from the live Adom
library into a standalone, loadable, single-entry library file. (KiCad's
native equivalents of an EAGLE/Fusion .lbr: .kicad_sym for symbols, .pretty/
.kicad_mod for footprints, .step/.wrl for the 3D body.)
"""
import os
import re
import shutil
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))


def handle_export_part(kicad_info: dict, args: dict) -> dict:
    """One-off single-part library exports.

    Args:
      partName (required) — the part in the Adom library.
      what (optional)     — list from {"symbol","footprint","model"}; default all.
      footprintName       — footprint name if it differs from partName.
      outputDir           — default Documents/adom-part-exports/<partName>/.
    """
    part = (args.get("partName") or args.get("symbolName") or "").strip()
    if not part:
        return {"success": False, "error": "partName required"}
    fp_name = (args.get("footprintName") or part).strip()
    what = args.get("what") or ["symbol", "footprint", "model"]
    if isinstance(what, str):
        what = [what]
    what = [w.lower() for w in what]

    user_dir = kicad_info.get("user_dir")
    if not user_dir:
        return {"success": False, "error": "KiCad user directory not found"}

    out_dir = Path(args.get("outputDir") or (Path(os.environ.get("USERPROFILE", str(Path.home())))
                                             / "Documents" / "adom-part-exports" / part))
    out_dir.mkdir(parents=True, exist_ok=True)

    exports: dict = {}
    errors: list = []

    if "symbol" in what:
        sym_lib = Path(user_dir) / "symbols" / "Adom.kicad_sym"
        try:
            import adom_library
            blocks = adom_library._extract_top_level_symbols(sym_lib.read_text(encoding="utf-8"))
            hit = [b for n, b in blocks if n == part]
            if not hit:
                errors.append(f"symbol '{part}' not in {sym_lib}")
            else:
                one = ('(kicad_symbol_lib\n  (version 20231120)\n  (generator "adom-export-part")\n'
                       + hit[0].rstrip() + "\n)\n")
                p = out_dir / f"{part}-symbol-only.kicad_sym"
                p.write_text(one, encoding="utf-8")
                names = [n for n, _ in adom_library._extract_top_level_symbols(one)]
                exports["symbol"] = {"path": str(p), "symbolsInLibrary": names,
                                     "single": names == [part]}
        except Exception as e:
            errors.append(f"symbol export: {e}")

    if "footprint" in what:
        src = Path(user_dir) / "footprints" / "Adom.pretty" / f"{fp_name}.kicad_mod"
        if not src.exists():
            errors.append(f"footprint '{fp_name}' not in Adom.pretty")
        else:
            try:
                pretty = out_dir / f"{part}-footprint-only.pretty"
                pretty.mkdir(parents=True, exist_ok=True)
                dst = pretty / f"{fp_name}.kicad_mod"
                shutil.copyfile(src, dst)
                mods = [f.name for f in pretty.glob("*.kicad_mod")]
                exports["footprint"] = {"path": str(dst), "library": str(pretty),
                                        "footprintsInLibrary": mods,
                                        "single": mods == [f"{fp_name}.kicad_mod"]}
            except Exception as e:
                errors.append(f"footprint export: {e}")

    if "model" in what:
        models_dir = Path(user_dir) / "3dmodels" / "Adom"
        src = None
        for ext in (".step", ".stp", ".wrl"):
            c = models_dir / f"{fp_name}{ext}"
            if c.exists():
                src = c
                break
        if src is None and "footprint" not in errors:
            # fall back to the model referenced by the footprint
            fp = Path(user_dir) / "footprints" / "Adom.pretty" / f"{fp_name}.kicad_mod"
            if fp.exists():
                m = re.search(r'\(model\s+"([^"]+)"', fp.read_text(encoding="utf-8"))
                if m and Path(m.group(1)).exists():
                    src = Path(m.group(1))
        if src is None:
            errors.append(f"no 3D model for '{fp_name}' in 3dmodels/Adom")
        else:
            try:
                dst = out_dir / f"{part}-model-only{src.suffix}"
                shutil.copyfile(src, dst)
                exports["model"] = {"path": str(dst), "bytes": dst.stat().st_size}
            except Exception as e:
                errors.append(f"model export: {e}")

    ok = len(exports) == len(what) and not errors
    return {
        "success": ok,
        "partName": part,
        "outputDir": str(out_dir),
        "exports": exports,
        "errors": errors,
        "output": f"one-off exports for {part}: " + ", ".join(sorted(exports)) +
                  (f" | {len(errors)} error(s)" if errors else ""),
        "_hint": ("Each file is a standalone single-entry library: the .kicad_sym holds ONLY this "
                  "symbol (open with kicad_show_symbol after installing, or any KiCad), the .pretty "
                  "holds ONLY this footprint, the model file is the bare 3D body. The reverse of "
                  "install — proves per-asset round-trip. Pull them with ab pull_file."),
    }