app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
John Lauer
v0.9.157: session sanitize - bridge projects are scrubbed from KiCad restore-on-launch (kicad.json system.open_projects…
ffb3b6f
2mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
"""make_part_project.py — kicad_make_part_project: generate a small, real KiCad
project AROUND an installed Adom-library part, so every project-level surface
(project / schematic / 2D board / 3D board) has something true to show for a
single component.
Why this exists (John, 2026-08-15, the Web Control demo): the demo buttons for
symbol / footprint / 3D chip / library map 1:1 onto show_* verbs, but schematic,
2D board, 3D board and project need a DESIGN that uses the part — and in the
electronics world you only get to a 3D board by having the library, schematic
and layout in place first. Callers (ab, web demos, AIs) should not hand-assemble
KiCad file formats; this verb generates them from the installed part:
kicad_install_footprint {..., model:{...}} (part into the Adom library)
kicad_install_symbol {...}
kicad_make_part_project {"partName":"IN-S42ATR"}
kicad_show_project / show_schematic / show_2d_board / show_3d_board {...}
SINGLE-COMPONENT BY DESIGN (John, 2026-08-16: "when demoing a single component,
you don't need to add extra components to the example 2d board layout as its
just confusing for the user"). The generated design is JUST the part: the sheet
holds the one symbol with short stub wires off its pins, the board holds the one
footprint centered in tight edge cuts with two copper stubs. The reference
designator comes from the part's own Reference property (D1 for a diode, C1 for
a capacitor, ...), not a hardcoded prefix.
Generated (formats identical to the proven kicad_demo generators):
<out>/<project>.kicad_pro — minimal project
<out>/<project>.kicad_sch — the part alone (lib_symbols embedded => self-contained)
<out>/<project>.kicad_pcb — the part's footprint, part-dominant edge cuts; the
footprint keeps its (model ...) so the 3D board
shows the real part model.
"""
import math
import os
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
IS_WINDOWS = sys.platform == "win32"
def _uuid(n: int) -> str:
return f"00000000-0000-4000-8000-{n:012d}"
def _find_symbol_block(lib_text: str, name: str) -> str | None:
import adom_library
for n, block in adom_library._extract_top_level_symbols(lib_text):
if n == name:
return block
return None
def _pins(sym_block: str):
"""[(x, y, angle_deg), ...] — pin (at ...) is the CONNECTION point."""
return [(float(a), float(b), float(c)) for a, b, c in
re.findall(r'\(pin\s+\S+\s+\S+\s*\n?\s*\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)',
sym_block)]
def _pads(fp_text: str):
"""[(x, y), ...] for (pad "N" ... (at x y ...))."""
return [(float(a), float(b)) for a, b in
re.findall(r'\(pad\s+"[^"]*"[^()]*\(at\s+([-\d.]+)\s+([-\d.]+)', fp_text)]
def handle_make_part_project(kicad_info: dict, args: dict) -> dict:
"""Generate <project>.kicad_pro/.kicad_sch/.kicad_pcb around an installed part.
Args:
partName (required) — symbol name in the Adom library (install it first).
footprintName — footprint in Adom.pretty (default: partName).
projectName — default "<partname-lower>-demo".
outputDir — default Documents/<projectName>. Overwritten if present.
"""
part = (args.get("partName") or args.get("symbolName") or "").strip()
if not part:
return {"success": False, "error": "partName required",
"_hint": "The symbol's name in the Adom library — kicad_install_symbol first."}
fp_name = (args.get("footprintName") or part).strip()
proj = (args.get("projectName") or "").strip() or \
re.sub(r"[^a-z0-9-]+", "-", part.lower()).strip("-") + "-demo"
user_dir = kicad_info.get("user_dir")
if not user_dir:
return {"success": False, "error": "KiCad user directory not found",
"_hint": "kicad_status reports install state; run once with KiCad installed."}
sym_lib = Path(user_dir) / "symbols" / "Adom.kicad_sym"
if not sym_lib.exists():
return {"success": False, "error": f"Adom library not found at {sym_lib}",
"_hint": "kicad_install_symbol installs the part into the Adom library first."}
block = _find_symbol_block(sym_lib.read_text(encoding="utf-8"), part)
if not block:
return {"success": False, "error": f"Symbol '{part}' not in the Adom library",
"_hint": "kicad_install_symbol with the part's .kicad_sym first (wiki part pages ship it)."}
fp_path = Path(user_dir) / "footprints" / "Adom.pretty" / f"{fp_name}.kicad_mod"
if not fp_path.exists():
return {"success": False, "error": f"Footprint '{fp_name}' not in Adom.pretty",
"_hint": "kicad_install_footprint (with model:{...} for the 3D step) first."}
fp_text = fp_path.read_text(encoding="utf-8")
out_dir = Path(args.get("outputDir") or (Path(os.environ.get("USERPROFILE", str(Path.home())))
/ "Documents" / proj))
out_dir.mkdir(parents=True, exist_ok=True)
# The part names its own reference prefix (D for a diode, C for a capacitor…)
ref_m = re.search(r'\(property\s+"Reference"\s+"([^"]*)"', block)
refdes = ((ref_m.group(1).strip() if ref_m else "") or "U") + "1"
# ---------------- schematic: JUST the part ----------------
sheet = _uuid(1)
ax, ay = 127.0, 80.0 # part anchor on the sheet (y-down)
lib_entry = " " + block.replace(f'(symbol "{part}"', f'(symbol "Adom:{part}"', 1)
wires = ""
wire_n = 300
for px, py, pa in _pins(block)[:2]:
cx, cy = ax + px, ay - py # symbol y-up -> sheet y-down
# outward = away from the body = opposite the pin's pointing direction
dx = -math.cos(math.radians(pa))
dy = math.sin(math.radians(pa)) # y flip folded in
ex, ey = round(cx + 6 * dx, 3), round(cy + 6 * dy, 3)
wires += (f' (wire (pts (xy {round(cx, 3)} {round(cy, 3)}) (xy {ex} {ey})) '
f'(stroke (width 0) (type default)) (uuid "{_uuid(wire_n)}"))\n')
wire_n += 1
schematic = f'''(kicad_sch (version 20231120) (generator "eeschema") (generator_version "8.0")
(uuid "{sheet}") (paper "A4")
(title_block (title "{part} demo - generated by the Adom KiCad Bridge") (company "Adom"))
(lib_symbols
{lib_entry}
)
{wires} (symbol (lib_id "Adom:{part}") (at {ax} {ay} 0) (unit 1)
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid "{_uuid(201)}")
(property "Reference" "{refdes}" (at {ax} {ay - 7.5} 0) (effects (font (size 1.27 1.27))))
(property "Value" "{part}" (at {ax} {ay + 7} 0) (effects (font (size 1.27 1.27))))
(property "Footprint" "Adom:{fp_name}" (at {ax} {ay} 0) (effects (font (size 1.27 1.27)) (hide yes)))
(instances (project "{proj}" (path "/{sheet}" (reference "{refdes}") (unit 1))))
)
(sheet_instances (path "/" (page "1")))
)
'''
# ---------------- board: JUST the part, centered ----------------
# Part-DOMINANT outline: tight margins so the part is clearly visible in the
# 3D board view (a 0402 on a roomy board reads as an empty board).
part_at = (108.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)
x1 = round(part_at[0] - half_w - margin, 2)
y1 = round(part_at[1] - max(half_h, 1.0) - margin, 2)
x2 = round(part_at[0] + half_w + 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**"', f'(fp_text reference "{refdes}"', 1)
fp_placed = fp_placed.replace('(property "Reference" "REF**"', f'(property "Reference" "{refdes}"', 1)
edge = ""
pts = [(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)]
for i in range(4):
(ex1, ey1), (ex2, ey2) = pts[i], pts[i + 1]
edge += (f' (gr_line (start {ex1} {ey1}) (end {ex2} {ey2}) '
f'(stroke (width 0.15) (type solid)) (layer "Edge.Cuts") (uuid "{_uuid(400 + i)}"))\n')
# two short copper stubs off the pads keep the board looking real without
# adding any other component
segments = ""
pads = _pads(fp_text)
if len(pads) >= 2 and abs(pads[0][1] - pads[1][1]) < 0.01:
left, right = sorted(pads[:2])
py_abs = part_at[1] + left[1]
segments = (f' (segment (start {round(part_at[0] + left[0], 3)} {round(py_abs, 3)}) '
f'(end {round(x1 + 0.8, 3)} {round(py_abs, 3)}) (width 0.3) (layer "F.Cu"))\n'
f' (segment (start {round(part_at[0] + right[0], 3)} {round(py_abs, 3)}) '
f'(end {round(x2 - 0.8, 3)} {round(py_abs, 3)}) (width 0.3) (layer "F.Cu"))\n')
fps = " " + fp_placed.rstrip().replace("\n", "\n ")
board = f'''(kicad_pcb (version 20240108) (generator "pcbnew") (generator_version "8.0")
(general (thickness 1.6)) (paper "A4")
(layers
(0 "F.Cu" signal) (31 "B.Cu" signal)
(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")
(38 "B.Mask" user) (39 "F.Mask" user) (40 "B.Paste" user) (41 "F.Paste" user)
(44 "Edge.Cuts" user) (45 "Margin" user) (49 "F.Fab" user) (50 "B.Fab" user)
(46 "B.CrtYd" user "B.Courtyard") (47 "F.CrtYd" user "F.Courtyard")
)
(setup (pad_to_mask_clearance 0))
{edge}{fps}
{segments})
'''
pro = ('{"board":{},"meta":{"filename":"%s.kicad_pro","version":1},"schematic":{},"sheets":[]}'
% proj)
files = {}
try:
for name, content in ((f"{proj}.kicad_pro", pro),
(f"{proj}.kicad_sch", schematic),
(f"{proj}.kicad_pcb", board)):
p = out_dir / name
p.write_text(content, encoding="utf-8")
files[name.rsplit(".", 1)[-1]] = str(p)
if name.endswith(".kicad_pro"):
try:
from handlers.win_focus import register_owned_project
register_owned_project(str(p))
except Exception:
pass
# stale single-instance locks from a previous open of the same project
for lck in out_dir.glob("*.lck"):
try:
lck.unlink()
except OSError:
pass
except Exception as e:
return {"success": False, "error": f"Failed to write project files: {e}"}
for name, content in ((f"{proj}.kicad_sch", schematic), (f"{proj}.kicad_pcb", board)):
if content.count("(") != content.count(")"):
return {"success": False, "error": f"internal: unbalanced s-expression in {name}"}
return {
"success": True,
"projectName": proj,
"projectFile": files["kicad_pro"],
"schematicFile": files["kicad_sch"],
"boardFile": files["kicad_pcb"],
"partName": part,
"footprintName": fp_name,
"reference": refdes,
"output": (f"Generated {proj}: the part alone as {refdes}, single-component by design "
f"(schematic + part-dominant board with edge cuts, copper stubs and the part's 3D model)."),
"_hint": ("Project surfaces now have something real to show: kicad_show_project {filePath: "
"projectFile}, kicad_show_schematic {filePath: schematicFile}, kicad_show_2d_board / "
"kicad_show_3d_board {filePath: boardFile}. The footprint keeps its (model ...) "
"reference, so the 3D board renders the actual part. Regenerating overwrites in place."),
"_next": ["kicad_show_project", "kicad_show_schematic", "kicad_show_2d_board", "kicad_show_3d_board"],
}