-
-
Save crides/6d12d1033368e24873b0142941311e5d to your computer and use it in GitHub Desktop.
# Extracts QMK `info.json` layout data from kicad pcb | |
# You need to have the `pcbnew` module importable (should be installed when kicad is installed) | |
import re, pcbnew, json | |
from typing import Callable | |
from functools import reduce | |
kicad7 = pcbnew.Version().startswith("7") | |
if kicad7: | |
iu2mm = lambda iu: iu / pcbnew.PCB_IU_PER_MM | |
PrimPointType = lambda *a: pcbnew.VECTOR2I(pcbnew.wxPoint(*a)) | |
def check_layers(fp: pcbnew.FOOTPRINT) -> list: | |
return pcbnew.LSET.AllNonCuMask().Seq() | |
else: | |
iu2mm = pcbnew.Iu2Millimeter | |
PrimPointType = pcbnew.wxPoint | |
def check_layers(fp: pcbnew.FOOTPRINT) -> list: | |
layers = (pcbnew.LSET_PhysicalLayersMask() | |
.removeLayer(pcbnew.F_SilkS).removeLayer(pcbnew.B_SilkS) | |
.addLayer(pcbnew.F_CrtYd).addLayer(pcbnew.B_CrtYd).addLayer(pcbnew.Margin).addLayer(pcbnew.Edge_Cuts)) | |
l = fp.GetLayerSet() | |
return l.RemoveLayerSet(layers).Seq() | |
def get_layout(fn: str, f: Callable[[pcbnew.FOOTPRINT], bool]) -> None: | |
board: pcbnew.BOARD = pcbnew.LoadBoard(fn) | |
switches = sorted((fp for fp in board.GetFootprints() if f(fp)), key=lambda sw: int(re.search("\\d+", sw.GetReference()).group(0))) | |
def get_params(fp: pcbnew.FOOTPRINT) -> list[float]: | |
deg = fp.GetOrientationDegrees() | |
if deg != 0: | |
deg -= 90 * round(deg / 90) # snap to nearest 90deg | |
if kicad7: | |
rot = pcbnew.EDA_ANGLE(-deg * 10, pcbnew.DEGREES_T) | |
else: | |
rot = -deg * 10 | |
fp.Rotate(fp.GetCenter(), rot) | |
bb = fp.GetBoundingBox(False, False) | |
gis = fp.GraphicalItems() | |
def get_param(layer: int) -> list[float] | None: | |
gs = [g for g in gis if g.GetLayer() == layer] | |
if len(gs) == 0: | |
return None | |
bbs = [g.GetBoundingBox() for g in gs] | |
merged = reduce(lambda a, b: a.Merge(b) or a, bbs) | |
if not kicad7: | |
merged = merged.getWxRect() | |
width = list(set(g.GetWidth() for g in gs)) | |
if len(width) > 1: | |
raise ValueError(f"more than 1 width: {width} on layer: {pcbnew.LayerName(layer)}") | |
width = width[0] | |
pos = bb.GetOrigin() + PrimPointType(width / 2, width / 2) | |
w, h = merged.GetWidth() - width, merged.GetHeight() - width | |
return [iu2mm(i) for i in [pos.x, pos.y, w, h]] | |
p = max((p for layer in check_layers(fp) if (p := get_param(layer)) != None), key=lambda p: p[2:4]) | |
if deg != 0: | |
p += [-deg] | |
return p | |
params = [get_params(sw) for sw in switches] | |
min_size = min(s for p in params for s in p[2:4]) | |
min_x, min_y = min(p[0] for p in params), min(p[1] for p in params) | |
params = [[round(i / min_size, 3) for i in [(p[0] - min_x), (p[1] - min_y), p[2], p[3]]] + p[4:5] for p in params] | |
def to_dict(p) -> dict[str, float]: | |
d = {"x": p[0], "y": p[1]} | |
if p[3] == 1: | |
d["w"] = p[2] | |
elif p[2] == 1: | |
d["h"] = p[3] | |
if len(p) == 5: | |
d["r"] = p[4] | |
return d | |
print(json.dumps({"layouts": {"default": {"layout": [to_dict(p) for p in params]}}})) | |
# Example: | |
# `f` is a footprint filtering function to get the switches. docs for the `FOOTPRINT` type: https://docs.kicad.org/doxygen-python-6.0/classpcbnew_1_1FOOTPRINT.html | |
get_layout("/home/steven/gitproj/fusion/fusion.kicad_pcb", lambda sw: str(sw.GetReference()).startswith("K")) |
that's not a bug, but rather a design assumption. It doesn't assume anything about the switch, but rather assumes that there's a keycap-like graphic and use that to determine the switch orientation and sizes. I could make it such that, if there's no graphic, then I only set the positions.
Hi, could you share a .kicad_pcb file that works with this script? I tried it on Lily58 and corne-classic but I get a ValueError: max() arg is an empty sequence
error for both. Or if you give a hint on what to modify in the script that's also appreciated.
@torjek see my comment above. Any of my projects should work with this script (see the fissure or fusion). As for modifying the script to suit other footprints, I can't say much without the exact PCB/footprints you have, although you could look at line 38 and change the layers removed on line 12 (but the keycap dimension extraction logic will probably not work if the layer used has other graphics in it).
I got an error
ValueError: max() arg is an empty sequence
on the [L38].(https://gist.github.com/crides/6d12d1033368e24873b0142941311e5d#file-layout-py-L38)