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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
"""A pours plan from the spec, not from the AI's memory of one board.
spec.pours is a list of entries; each becomes zones in the kicad_add_zone shape:
{"net": "GND", "outline": true, "layers": ["F.Cu","B.Cu"], "priority": 0, "connection": "thermal",
"thermalGap": 0.3, "thermalBridge": 0.35}
{"net": "+VBAT", "around": ["MC1","C2","C3","D1"], "margin": 1.0, "layers": ["F.Cu","B.Cu"], "priority": 2,
"connection": "solid", "exclude": ["U4"]} # the box around those parts' pads, grown by margin,
# clipped so it does not reach the excluded parts' boxes
{"net": "/DRV_SHA", "around": ["Q1.1","Q2.5","MC5"], "margin": 0.6, "layers": ["F.Cu"], "priority": 3, "connection": "solid"}
{"net": "+VBAT", "polygon": [[x,y],...], "layers": ["B.Cu"], "priority": 2} # explicit
spec.solidAt: ["C45.2"] pads that get a small solid patch (starved thermal reliefs)
spec.thermalVias: [{"pad": "Q1.5", "net": "+VBAT", "count": 2, "size": 0.8, "drill": 0.4}]
spec.kelvin: {"U6.3": "R32.2", ...} taps get pour keepouts along the routed tap (from the routing plan's kelvin entries)
"""
import json
import math
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from escboard import Board # noqa: E402
def pad_box(pads):
xs = [q[0] for p in pads for poly in p.polys for q in poly]
ys = [q[1] for p in pads for poly in p.polys for q in poly]
return [min(xs), min(ys), max(xs), max(ys)]
def rect(x0, y0, x1, y1):
return [[round(x0, 2), round(y0, 2)], [round(x1, 2), round(y0, 2)], [round(x1, 2), round(y1, 2)], [round(x0, 2), round(y1, 2)]]
def select(board, keys):
out = []
for k in keys:
if "." in k:
ref, name = k.split(".", 1)
out += [p for p in board.pads if p.ref == ref and p.name == name]
else:
out += [p for p in board.pads if p.ref == k]
return out
def build(board, spec, plan=None):
x0, y0, x1, y1 = board.bbox
edge = float(spec.get("edgeClearance", 0.5))
zones = []
for e in spec.get("pours", []):
z = {k: v for k, v in e.items() if k not in ("around", "margin", "exclude", "polygon", "outline", "inset")}
z.setdefault("clearance", spec.get("clearance", 0.2)); z.setdefault("minThickness", 0.25)
if e.get("outline"):
z["outline"] = True; z["inset"] = float(e.get("inset", edge))
elif e.get("polygon"):
z["polygon"] = e["polygon"]
elif e.get("around"):
pads = select(board, e["around"])
if not pads:
raise SystemExit("pour %s: none of %s found on the board" % (e.get("net"), e["around"]))
m = float(e.get("margin", 0.8))
bx = pad_box(pads)
bx = [max(x0 + edge, bx[0] - m), max(y0 + edge, bx[1] - m), min(x1 - edge, bx[2] + m), min(y1 - edge, bx[3] + m)]
for ex in e.get("exclude", []):
xb = pad_box(select(board, [ex]))
# clip the pour box away from the excluded part on the side where they overlap least
if not (xb[2] < bx[0] or xb[0] > bx[2] or xb[3] < bx[1] or xb[1] > bx[3]):
cuts = [("x1", xb[0] - 1.0, bx[2] - (xb[0] - 1.0)), ("x0", xb[2] + 1.0, (xb[2] + 1.0) - bx[0]), ("y1", xb[1] - 1.0, bx[3] - (xb[1] - 1.0)), ("y0", xb[3] + 1.0, (xb[3] + 1.0) - bx[1])]
side, val, _ = min(cuts, key=lambda c: c[2])
{"x0": lambda: bx.__setitem__(0, val), "y0": lambda: bx.__setitem__(1, val), "x1": lambda: bx.__setitem__(2, val), "y1": lambda: bx.__setitem__(3, val)}[side]()
z["polygon"] = rect(*bx)
else:
raise SystemExit("pour %s needs outline, polygon or around" % e.get("net"))
z.setdefault("name", "%s pour" % z.get("net"))
zones.append(z)
for key in spec.get("solidAt", []):
pads = select(board, [key])
if pads:
p = pads[0]
zones.append({"net": p.net, "layers": ["F.Cu" if "F.Cu" in p.layers else "B.Cu"], "polygon": rect(p.x - 0.9, p.y - 0.9, p.x + 0.9, p.y + 0.9), "priority": 1, "connection": "solid",
"clearance": spec.get("clearance", 0.2), "minThickness": 0.25, "name": "%s solid joint at %s" % (p.net, key)})
vias = []
blockers = []
if plan:
for e in plan.get("nets", []):
for path in e.get("paths", []):
prev = None
for w in path:
if isinstance(w, str):
prev = None; continue
if isinstance(w, dict) and "x" not in w:
continue
pt = (w["x"], w["y"]) if isinstance(w, dict) else (w[0], w[1])
if prev is not None:
blockers.append((prev, pt, e["net"], float(e.get("width", 0.25)) / 2.0 + 0.2 + 0.45))
prev = pt
def clear(x, y, net):
for (ax, ay), (bx, by), n, need in blockers:
if n == net:
continue
dx, dy = bx - ax, by - ay; L = dx * dx + dy * dy
t = 0 if L == 0 else max(0.0, min(1.0, ((x - ax) * dx + (y - ay) * dy) / L))
if math.hypot(x - (ax + t * dx), y - (ay + t * dy)) < need:
return False
return True
for tv in spec.get("thermalVias", []):
pads = select(board, [tv["pad"]])
if not pads:
continue
p = pads[0]; net = tv.get("net", p.net); n = int(tv.get("count", 2)); placed = 0
for dx, dy in ((-1.2, 0.0), (1.2, 0.0), (-1.2, -1.3), (1.2, -1.3), (-1.2, 1.3), (1.2, 1.3), (0.0, -1.3), (0.0, 1.3), (0.0, 0.0)):
if placed >= n:
break
x, y = round(p.x + dx, 2), round(p.y + dy, 2)
if abs(dx) < p.half_long - 0.4 and abs(dy) < p.half_short - 0.4 or (dx, dy) == (0.0, 0.0):
if clear(x, y, net):
vias.append({"net": net, "width": 0.5, "viaSize": tv.get("size", 0.8), "viaDrill": tv.get("drill", 0.4), "paths": [[[x, y], {"x": x, "y": y, "layer": "B.Cu"}]]}); placed += 1
for sv in spec.get("stitchVias", []):
x, y = sv["x"], sv["y"]
if clear(x, y, sv["net"]):
vias.append({"net": sv["net"], "width": 0.5, "viaSize": sv.get("size", 0.8), "viaDrill": sv.get("drill", 0.4), "paths": [[[x, y], {"x": x, "y": y, "layer": "B.Cu"}]]})
keepouts = 0
if plan and spec.get("kelvin"):
for e in plan.get("nets", []):
if not e.get("kelvin"):
continue
for path in e["paths"]:
pts = [(w["x"], w["y"]) if isinstance(w, dict) else (w[0], w[1]) for w in path if not isinstance(w, str)]
for (ax, ay), (bx, by) in zip(pts, pts[1:]):
dx, dy = bx - ax, by - ay; n = math.hypot(dx, dy)
if n < 0.05:
continue
ux, uy = dx / n, dy / n; w = float(e.get("width", 0.25)) / 2.0 + 0.2 + 0.1
poly = [[round(ax - uy * w, 3), round(ay + ux * w, 3)], [round(bx - uy * w, 3), round(by + ux * w, 3)], [round(bx + uy * w, 3), round(by - ux * w, 3)], [round(ax + uy * w, 3), round(ay - ux * w, 3)]]
zones.append({"type": "keepout", "layers": ["F.Cu", "B.Cu"], "polygon": poly, "keepout": {"copperPour": True}, "name": "Kelvin tap keepout %s" % e["net"]})
keepouts += 1
return zones, vias, keepouts
def main():
import argparse
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("board"); ap.add_argument("--spec", required=True); ap.add_argument("--plan"); ap.add_argument("--out", required=True); ap.add_argument("--vias")
a = ap.parse_args()
board = Board(a.board); spec = json.load(open(a.spec)); plan = json.load(open(a.plan)) if a.plan else None
zones, vias, keepouts = build(board, spec, plan)
json.dump({"zones": zones}, open(a.out, "w"), indent=1)
if a.vias:
json.dump({"engine": "adom-boardflow pour plan (thermal and stitching vias)", "nets": vias}, open(a.vias, "w"), indent=1)
print("%d zones (%d keepouts), %d vias -> %s" % (len(zones), keepouts, len(vias), a.out))
for z in zones:
print(" %-10s %-12s %s" % (z.get("net") or "keepout", "+".join(z["layers"]), "keepout" if z.get("keepout") else "prio %s %s %s" % (z.get("priority", 0), z.get("connection", "thermal"), "outline" if z.get("outline") else "%d pts" % len(z["polygon"]))))
if __name__ == "__main__":
main()