Skip to content

Instantly share code, notes, and snippets.

@crides
Last active November 26, 2023 20:04
Show Gist options
  • Select an option

  • Save crides/6d12d1033368e24873b0142941311e5d to your computer and use it in GitHub Desktop.

Select an option

Save crides/6d12d1033368e24873b0142941311e5d to your computer and use it in GitHub Desktop.
import re, pcbnew, pprint
from typing import Callable
from functools import reduce
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)))
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))
def get_params(fp: pcbnew.FOOTPRINT) -> tuple[float, float, float, float]:
bb = fp.GetBoundingBox(False, False)
l = fp.GetLayerSet()
gis = fp.GraphicalItems()
def get_param(layer: int) -> tuple[float, float, float, float]:
gs = [g for g in gis if g.GetLayer() == layer]
bbs = [g.GetBoundingBox() for g in gs]
merged = reduce(lambda a, b: a.Merge(b) or a, bbs).getWxRect()
width = list(set(g.GetWidth() for g in gs))
if len(width) > 1:
raise ValueError(f"more than 1 width: {width}")
width = width[0]
pos = bb.GetOrigin() + pcbnew.wxPoint(width / 2, width / 2)
w, h = merged.GetWidth() - width, merged.GetHeight() - width
iu2mm = pcbnew.Iu2Millimeter
return iu2mm(pos.x), iu2mm(pos.y), iu2mm(w), iu2mm(h)
return max((get_param(layer) for layer in l.RemoveLayerSet(layers).Seq()), key=lambda p: p[2:])
params = [get_params(sw) for sw in switches]
min_size = min(s for p in params for s in p[2:])
min_x, max_y = min(p[0] for p in params), max(p[1] for p in params)
params = [((p[0] - min_x) / min_size, (max_y - p[1]) / min_size, p[2] / min_size, p[3] / min_size) 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]
return d
pprint.pprint([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("musk.kicad_pcb", lambda sw: sw.GetValue() == "diode-choc")
@nanpuhaha
Copy link

I got an error ValueError: max() arg is an empty sequence on the [L38].(https://gist.github.com/crides/6d12d1033368e24873b0142941311e5d#file-layout-py-L38)

@crides
Copy link
Author

crides commented Mar 27, 2023

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.

@torjek
Copy link

torjek commented Aug 10, 2023

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.

@crides
Copy link
Author

crides commented Aug 10, 2023

@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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment