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
ESC routing to 100%: the place-route loop (smaller signal vias, 13 parts moved, 114 commits, 0 unconnected), kicad-plac…
df61ffd
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
#!/usr/bin/env python3
"""Find legal spots for a few parts near where they should be (the placement-adjustment helper).
python3 tools/pack_moves.py BOARD.kicad_pcb --wish wishes.json --out moves.json [--gap 0.4]
wishes.json: an ordered object {REF: {"x":.., "y":.., "rot":[0,90], "radius": 4.0, "keepout": [[x0,y0,x1,y1,margin], ...]}}.
For each part in order: candidates on a 0.1 mm grid around the wish, nearest first, each rotation in the
given order; the first whose courtyard clears every other part's courtyard (pad box + 0.25 mm when a part has none) by `gap` (or the part's own "gap"), stays `edge` inside the outline
bbox and avoids the keepout rectangles (grown by their margin) wins. Prints what it found; the output is
the shape kicad_move_footprint and tools/move_footprints.py take.
"""
import argparse, json, math, os, re, sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "demo", "routing", "esc"))
from escboard import Board, rot_kicad # noqa: E402
def courtyard_local(text, ref):
"""Local (unrotated, anchor-relative) F.CrtYd points of a footprint from the file text, or None."""
i = text.find('(property "Reference" "%s"' % ref)
if i < 0:
return None
s = text.rfind("\n\t(footprint ", 0, i)
depth = 0; j = s + 1
while True:
c = text[j]
if c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
break
j += 1
block = text[s:j + 1]
pts = []
for m in re.finditer(r"\((fp_line|fp_rect|fp_poly|fp_circle|fp_arc)\b(.*?)\n\t\t\)", block, re.S):
body = m.group(2)
if '(layer "F.CrtYd")' not in body and '(layer "B.CrtYd")' not in body:
continue
for xm in re.finditer(r"\((?:start|end|xy|center|mid) (-?[\d.]+) (-?[\d.]+)\)", body):
pts.append((float(xm.group(1)), float(xm.group(2))))
if m.group(1) == "fp_circle":
c = re.search(r"\(center (-?[\d.]+) (-?[\d.]+)\)", body); e = re.search(r"\(end (-?[\d.]+) (-?[\d.]+)\)", body)
if c and e:
r = math.hypot(float(e.group(1)) - float(c.group(1)), float(e.group(2)) - float(c.group(2)))
cx, cy = float(c.group(1)), float(c.group(2))
pts += [(cx - r, cy - r), (cx + r, cy + r)]
return pts or None
def pad_boxes(board):
boxes = {}
for p in board.pads:
xs = [q[0] for poly in p.polys for q in poly]; ys = [q[1] for poly in p.polys for q in poly]
bx = boxes.setdefault(p.ref, [1e9, 1e9, -1e9, -1e9])
bx[0] = min(bx[0], min(xs)); bx[1] = min(bx[1], min(ys)); bx[2] = max(bx[2], max(xs)); bx[3] = max(bx[3], max(ys))
return boxes
def rel_box(board, ref):
"""The part's pad box relative to its anchor at rotation 0 (undo the file rotation)."""
fp = board.footprints[ref]
pts = []
for p in board.pads:
if p.ref != ref:
continue
for poly in p.polys:
for (x, y) in poly:
pts.append(rot_kicad(x - fp["x"], y - fp["y"], -(fp["rot"] or 0)))
return pts
def placed_box(pts, x, y, rot):
rp = [rot_kicad(dx, dy, rot) for dx, dy in pts]
xs = [x + a for a, _ in rp]; ys = [y + c for _, c in rp]
return [min(xs), min(ys), max(xs), max(ys)]
def clear(box, others, gap):
for o in others:
if box[0] - gap < o[2] and o[0] - gap < box[2] and box[1] - gap < o[3] and o[1] - gap < box[3]:
return False
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("board"); ap.add_argument("--wish", required=True); ap.add_argument("--out", required=True)
ap.add_argument("--gap", type=float, default=0.1); ap.add_argument("--edge", type=float, default=1.0)
a = ap.parse_args()
b = Board(a.board)
text = open(a.board, encoding="utf-8").read()
boxes = pad_boxes(b)
crt = {}
for ref, fp in b.footprints.items():
pts = courtyard_local(text, ref)
if pts:
crt[ref] = pts
boxes[ref] = placed_box(pts, fp["x"], fp["y"], fp["rot"] or 0)
else:
bx = boxes.get(ref)
if bx:
boxes[ref] = [bx[0] - 0.25, bx[1] - 0.25, bx[2] + 0.25, bx[3] + 0.25]
wishes = json.load(open(a.wish))
x0, y0, x1, y1 = b.bbox
out = {}
for ref, w in wishes.items():
pts = crt.get(ref) or [(dx - 0.25, dy - 0.25) for dx, dy in rel_box(b, ref)] + [(dx + 0.25, dy + 0.25) for dx, dy in rel_box(b, ref)]
others = [bx for r, bx in boxes.items() if r != ref]
keep = w.get("keepout", [])
best = None
R = w.get("radius", 4.0)
cands = []
n = int(R / 0.1)
for i in range(-n, n + 1):
for j in range(-n, n + 1):
d = math.hypot(i, j) * 0.1
if d <= R:
cands.append((d, round(w["x"] + i * 0.1, 2), round(w["y"] + j * 0.1, 2)))
cands.sort()
for d, x, y in cands:
for rot in w.get("rot", [0]):
box = placed_box(pts, x, y, rot)
if box[0] < x0 + a.edge or box[1] < y0 + a.edge or box[2] > x1 - a.edge or box[3] > y1 - a.edge:
continue
if not clear(box, others, w.get("gap", a.gap)):
continue
if any(box[0] - m < kx1 and kx0 - m < box[2] and box[1] - m < ky1 and ky0 - m < box[3] for (kx0, ky0, kx1, ky1, m) in keep):
continue
best = (x, y, rot, box, d)
break
if best:
break
if best is None:
print("%-5s NO SPOT within %.1f mm of (%.1f, %.1f)" % (ref, R, w["x"], w["y"])); continue
x, y, rot, box, d = best
boxes[ref] = box
out[ref] = {"x": x, "y": y, "rotation": rot}
print("%-5s -> (%.2f, %.2f, %g) %.2f mm from the wish box x %.2f..%.2f y %.2f..%.2f" % (ref, x, y, rot, d, box[0], box[2], box[1], box[3]))
json.dump(out, open(a.out, "w"), indent=1)
if __name__ == "__main__":
main()