← Commit history

v0.9.109: kicad_export_part (one-off single-part library exports: symbol-only .kicad_sym, footprint-only .pretty, bare model); make_part_project boards are now part-dominant (tight edge cuts) so the chip is visible in the 3D board view

John Lauer ·9d49110ba6 ·2mo ago ·parent 5313e89
8 files changed +154−19
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.108+0.9.109
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.108",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.108/adom-bridge-kicad-v0.9.108.zip",-  "sha256": "648be128ecc2fd57b36dec6385b01a76efe96207ae18b59d1aee6c7c0d735545",-  "size": 403548,+  "version": "0.9.109",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.109/adom-bridge-kicad-v0.9.109.zip",+  "sha256": "1425028bec0a028a734e452dcd549cebac86b48ab024a62524e15704f75c32dc",+  "size": 406095,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-15T11:26:35Z",+  "released_at": "2026-08-15T12:30:08Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.109.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.108",+  "version": "0.9.109",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/export_part.pyadded+125
@@ -0,0 +1,125 @@+"""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."),+    }
handlers/make_part_project.py+13−11
@@ -191,22 +191,24 @@ def handle_make_part_project(kicad_info: dict, args: dict) -> dict: '''      # ---------------- board ----------------+    # Part-DOMINANT outline: tight margins so the part is clearly visible in the+    # 3D board view (John, 2026-08-15: on a roomy board a 0402 is an invisible+    # speck and the 3D board "isn't showing the chip").     part_at = (108.0, 68.0)-    r_at = (116.0, 68.0)+    coords = [(float(a), float(b)) for a, b in+              re.findall(r'\((?:start|end|at|center)\s+([-\d.]+)\s+([-\d.]+)', fp_text)]+    half_w = max([abs(x) for x, _ in coords] + [1.0])+    half_h = max([abs(y) for _, y in coords] + [1.0])+    margin = max(2.0, half_h * 0.8)+    r_at = (round(part_at[0] + half_w + 4.6, 2), part_at[1])  # 0805 R just beside the part+    x1 = round(part_at[0] - half_w - margin, 2)+    y1 = round(part_at[1] - max(half_h, 1.0) - margin, 2)+    x2 = round(r_at[0] + 1.7 + margin, 2)+    y2 = round(part_at[1] + max(half_h, 1.0) + margin, 2)     fp_placed = fp_text.replace(f'(footprint "{fp_name}"', f'(footprint "Adom:{fp_name}"', 1)     fp_placed = fp_placed.replace('(layer "F.Cu")', f'(layer "F.Cu")\n  (at {part_at[0]} {part_at[1]})', 1)     fp_placed = fp_placed.replace('(fp_text reference "REF**"', '(fp_text reference "D1"', 1)     fp_placed = fp_placed.replace('(property "Reference" "REF**"', '(property "Reference" "D1"', 1)--    # board outline sized from the part's coordinates (pads/graphics), with margin-    coords = [(float(a), float(b)) for a, b in-              re.findall(r'\((?:start|end|at|center)\s+([-\d.]+)\s+([-\d.]+)', fp_text)]-    half_w = max([abs(x) for x, _ in coords] + [2.0])-    half_h = max([abs(y) for _, y in coords] + [2.0])-    x1 = round(part_at[0] - half_w - 6, 2)-    y1 = round(part_at[1] - half_h - 6, 2)-    x2 = round(r_at[0] + 8, 2)-    y2 = round(part_at[1] + half_h + 6, 2)     edge = ""     pts = [(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)]     for i in range(4):
handlers/uninstall.py+3−1
@@ -194,11 +194,13 @@ def handle_uninstall(kicad_info: dict, args: dict) -> dict:     for f in tmp.glob("adom-kicad-bridge-*.json"):         rm_file(f, "plugin discovery file") -    # 6) screenshot cache, 7) demo project+    # 6) screenshot cache, 7) demo project, 8) part exports/staging (0.9.109)     rm_tree(Path("C:/tmp/adom-desktop-screenshots"), "screenshot cache")     up = os.environ.get("USERPROFILE")     if up:         rm_tree(Path(up) / "Documents" / "adom-kicad-demo", "demo project")+        rm_tree(Path(up) / "Documents" / "adom-part-exports", "one-off part exports")+        rm_tree(Path(up) / "Documents" / "adom-part-staging", "part install staging")      ok = len(failed) == 0     return {
server.py+6
@@ -67,6 +67,7 @@ from handlers.export_molecule import handle_export_molecule # v0.9.63+: first-class showcase tour (Bridge demos this bridge during Hydrogen install). from handlers.demo import handle_demo from handlers.make_part_project import handle_make_part_project+from handlers.export_part import handle_export_part # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only; # upgrade silent-installs the official signed NSIS build.@@ -366,6 +367,10 @@ _VERB_CATALOG = {     "export_step": {"summary": "Export a STEP 3D model from a board.", "long": False,         "hint": "Absolute board path + output; exports a STEP assembly via kicad-cli.",         "related": ["kicad_open_3d_viewer", "kicad_export_gerber"], "pitfalls": ["only components with 3D models attached appear in the STEP"]},+    "export_part": {"summary": "One-off single-part library exports: a .kicad_sym with ONLY that symbol, a .pretty with ONLY that footprint, the bare 3D model file — the reverse of install.", "long": False,+        "hint": "kicad_export_part {\"partName\":\"IN-S42ATA\"} (optional what:[\"symbol\",\"footprint\",\"model\"]). Writes to Documents/adom-part-exports/<part>/; pull with ab pull_file. Proves per-asset round-trip.",+        "related": ["kicad_install_symbol", "kicad_install_footprint", "kicad_make_part_project"],+        "pitfalls": ["the part must be installed in the Adom library first"]},     "make_part_project": {"summary": "Generate a small real project (pro/sch/pcb) AROUND an installed Adom-library part — gives the project/schematic/2D-board/3D-board surfaces something true to show for one component.", "long": False,         "hint": "kicad_make_part_project {\"partName\":\"IN-S42ATR\"} after install_symbol/install_footprint. Returns projectFile/schematicFile/boardFile for the show_* verbs. Board keeps the footprint's (model ...) so show_3d_board renders the real part.",         "related": ["kicad_show_project", "kicad_show_schematic", "kicad_show_2d_board", "kicad_show_3d_board"],@@ -543,6 +548,7 @@ COMMAND_HANDLERS = {     "export_molecule": handle_export_molecule,     "demo": handle_demo,     "make_part_project": handle_make_part_project,+    "export_part": handle_export_part,     "export_bom_csv": handle_export_bom_csv,     # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org     # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates