app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
main
John Lauer
source at 0.1.25: the whole crate workspace, the flow, the skills and the docs
f31cc25
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
#!/usr/bin/env python3
"""Add copper pours (zones) to a copy of a KiCad board offline, from the same JSON kicad_add_zone takes.
python3 tools/add_pours.py BOARD.kicad_pcb --pours pours.json [--out FILE] [--drc] [--baseline BOARD.kicad_pcb]
pours.json is {"zones": [ {"net": "+VBAT", "layers": ["F.Cu", "B.Cu"], "polygon": [[x, y], ...] | "outline": true,
"priority": 2, "clearance": 0.2, "minThickness": 0.25, "connection": "thermal" | "solid",
"thermalGap": 0.5, "thermalBridge": 0.5, "name": "..."}, ... ]}. Coordinates are mm, +y down, board frame.
"outline": true uses the Edge.Cuts outline (arcs sampled) inset by the edge clearance; "inset": mm sets it.
The zones are written the way KiCad stores them (no filled polygons: KiCad fills them on load and the
shared DRC refills them), spliced before the closing paren; every other byte is untouched. --drc runs
service-kicad's DRC on the result and, with --baseline, reports NEW errors against the unrouted board.
This is the offline half of the kicad-copper-pours skill: gate the pours here, then land the same JSON
live with kicad_add_zone.
"""
import argparse
import json
import os
import sys
import uuid
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fixture import num # noqa: E402
from escboard import Board, polygon_area # noqa: E402
from planes import splice, run_drc # noqa: E402
def inset_polygon(poly, d):
"""Shrink a convex-ish outline by d toward its centroid (good enough for a rectangle with rounded corners)."""
if d <= 0:
return list(poly)
cx = sum(p[0] for p in poly) / len(poly)
cy = sum(p[1] for p in poly) / len(poly)
out = []
for (x, y) in poly:
vx, vy = x - cx, y - cy
n = (vx * vx + vy * vy) ** 0.5 or 1.0
out.append((x - vx / n * d, y - vy / n * d))
return out
def keepout_sexpr(board, z):
"""A rule area: no net, the layers, the keepout flags (copperPour by default), the outline."""
k = z.get("keepout") or {"copperPour": True}
layers = z.get("layers") or [z["layer"]]
layer_line = "\t\t(layer \"%s\")\n" % layers[0] if len(layers) == 1 else "\t\t(layers %s)\n" % " ".join('"%s"' % l for l in layers)
a = lambda b: "not_allowed" if b else "allowed"
pts = " ".join("(xy %s %s)" % (num(x), num(y)) for x, y in z["_polygon"])
return (
"\t(zone\n"
+ ("\t\t(net 0)\n\t\t(net_name \"\")\n" if board.net_format == "number" else "")
+ layer_line
+ "\t\t(uuid \"%s\")\n" % uuid.uuid4()
+ "\t\t(name \"%s\")\n" % z.get("name", "keepout").replace('"', '\\"')
+ "\t\t(hatch edge 0.5)\n"
+ "\t\t(keepout\n\t\t\t(tracks %s)\n\t\t\t(vias %s)\n\t\t\t(pads %s)\n\t\t\t(copperpour %s)\n\t\t\t(footprints %s)\n\t\t)\n" % (a(k.get("tracks")), a(k.get("vias")), a(k.get("pads")), a(k.get("copperPour")), a(k.get("footprints")))
+ "\t\t(fill\n\t\t\t(thermal_gap 0.5)\n\t\t\t(thermal_bridge_width 0.5)\n\t\t)\n"
+ "\t\t(polygon\n\t\t\t(pts\n\t\t\t\t%s\n\t\t\t)\n\t\t)\n" % pts
+ "\t)"
)
def zone_sexpr(board, z):
if z.get("type") == "keepout" or z.get("keepout"):
return keepout_sexpr(board, z)
net_name = z["net"]
if board.net_format == "number":
number = next((n for n, name in board.net_table.items() if name == net_name), None)
if number is None:
raise KeyError("net %r is not in the net table" % net_name)
net_lines = "\t\t(net %d)\n\t\t(net_name \"%s\")\n" % (number, net_name.replace('"', '\\"'))
else:
net_lines = board.net_ref_sexpr(net_name)
layers = z.get("layers") or [z["layer"]]
if len(layers) == 1:
layer_line = "\t\t(layer \"%s\")\n" % layers[0]
else:
layer_line = "\t\t(layers %s)\n" % " ".join('"%s"' % l for l in layers)
poly = z["_polygon"]
pts = " ".join("(xy %s %s)" % (num(x), num(y)) for x, y in poly)
solid = z.get("connection", "thermal") in ("solid", "full")
prio = int(z.get("priority", 0) or 0)
return (
"\t(zone\n"
+ net_lines
+ layer_line
+ "\t\t(uuid \"%s\")\n" % uuid.uuid4()
+ "\t\t(name \"%s\")\n" % z.get("name", "%s pour" % net_name).replace('"', '\\"')
+ "\t\t(hatch edge 0.5)\n"
+ ("\t\t(priority %d)\n" % prio if prio > 0 else "")
+ ("\t\t(connect_pads yes\n\t\t\t(clearance %s)\n\t\t)\n" if solid else "\t\t(connect_pads\n\t\t\t(clearance %s)\n\t\t)\n") % num(float(z.get("clearance", 0.2)))
+ "\t\t(min_thickness %s)\n" % num(float(z.get("minThickness", 0.25)))
+ "\t\t(filled_areas_thickness no)\n"
+ "\t\t(fill yes\n\t\t\t(thermal_gap %s)\n\t\t\t(thermal_bridge_width %s)\n\t\t)\n" % (num(float(z.get("thermalGap", 0.5))), num(float(z.get("thermalBridge", 0.5))))
+ "\t\t(polygon\n\t\t\t(pts\n\t\t\t\t%s\n\t\t\t)\n\t\t)\n" % pts
+ "\t)"
)
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("board")
ap.add_argument("--pours", required=True, help="JSON: {zones: [...]} (the kicad_add_zone shape)")
ap.add_argument("--out")
ap.add_argument("--drc", action="store_true")
ap.add_argument("--baseline", help="board whose DRC errors are inherited (default: the input board)")
ap.add_argument("--edge", type=float, default=0.5, help="edge clearance used for outline: true")
a = ap.parse_args(argv)
src = Path(a.board)
board = Board(str(src))
spec = json.load(open(a.pours, encoding="utf-8"))
zones = spec["zones"] if isinstance(spec, dict) else spec
outline = list(board.outline)
if polygon_area(outline) < 0:
outline.reverse()
blocks = []
summary = []
for z in zones:
keep = z.get("type") == "keepout" or bool(z.get("keepout"))
if not keep and z["net"] not in board.nets:
raise SystemExit("net %r has no pads on this board" % z["net"])
for l in (z.get("layers") or [z["layer"]]):
if l not in board.copper_layers:
raise SystemExit("layer %r is not a copper layer (%s)" % (l, board.copper_layers))
if z.get("outline"):
poly = inset_polygon(outline, float(z.get("inset", a.edge)))
else:
poly = [(float(p[0]), float(p[1])) for p in z["polygon"]]
if len(poly) >= 2 and poly[0] == poly[-1]:
poly.pop()
if len(poly) < 3:
raise SystemExit("zone %s needs three or more points" % z["net"])
z["_polygon"] = poly
blocks.append(zone_sexpr(board, z))
summary.append("%s on %s (%.0f mm2, %s)" % (z.get("net") or "keepout", "+".join(z.get("layers") or [z["layer"]]), abs(polygon_area(poly)), "keepout" if keep else "priority %s, %s" % (z.get("priority", 0), z.get("connection", "thermal"))))
out = Path(a.out) if a.out else src.with_name(src.name[: -len(".kicad_pcb")] + "-pours.kicad_pcb")
text = splice(board.text, blocks)
out.write_text(text, encoding="utf-8", newline="")
Board(str(out))
print("%s: %d zones: %s" % (out, len(blocks), "; ".join(summary)))
if a.drc:
errors, warnings, unconnected, data = run_drc(out)
by_type = {}
for v in data.get("violations", []):
if v.get("severity") == "error":
by_type[v.get("type")] = by_type.get(v.get("type"), 0) + 1
line = "drc: %d errors, %d warnings, %d unconnected; error types %s (KiCad %s)" % (errors, warnings, unconnected, json.dumps(by_type, sort_keys=True), data.get("kicad_version"))
if a.baseline:
b_err, _, _, b_data = run_drc(Path(a.baseline))
def sig(v):
return (v.get("type"), tuple(sorted((i.get("description") or "") for i in v.get("items", []))))
base = {sig(v) for v in b_data.get("violations", []) if v.get("severity") == "error"}
new = [v for v in data.get("violations", []) if v.get("severity") == "error" and sig(v) not in base]
line += "; %d NEW against the baseline (%d inherited)" % (len(new), errors - len(new))
for v in new[:20]:
print(" new:", v.get("type"), [(i.get("description") or "")[:60] for i in v.get("items", [])])
print(line)
return 0 if (not new and unconnected == 0) else 2
print(line)
return 0 if (errors == 0 and unconnected == 0) else 2
return 0
if __name__ == "__main__":
sys.exit(main())