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.

Revisions

  1. crides revised this gist Nov 26, 2023. 1 changed file with 29 additions and 12 deletions.
    41 changes: 29 additions & 12 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -5,37 +5,54 @@
    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)))

    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) -> list[float]:
    deg = fp.GetOrientationDegrees()
    if deg != 0:
    deg -= 90 * round(deg / 90) # snap to nearest 90deg
    fp.Rotate(fp.GetCenter(), -deg * 10)
    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)
    l = fp.GetLayerSet()
    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).getWxRect()
    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() + pcbnew.wxPoint(width / 2, width / 2)
    pos = bb.GetOrigin() + PrimPointType(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)]
    p = max((p for layer in l.RemoveLayerSet(layers).Seq() if (p := get_param(layer)) != None), key=lambda p: p[2:4])
    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
    @@ -57,4 +74,4 @@ def to_dict(p) -> dict[str, float]:

    # 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/musk/architeuthis_dux.kicad_pcb", lambda sw: str(sw.GetReference()).startswith("S"))
    get_layout("/home/steven/gitproj/fusion/fusion.kicad_pcb", lambda sw: str(sw.GetReference()).startswith("K"))
  2. crides revised this gist Feb 3, 2023. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -21,8 +21,10 @@ def get_params(fp: pcbnew.FOOTPRINT) -> list[float]:
    bb = fp.GetBoundingBox(False, False)
    l = fp.GetLayerSet()
    gis = fp.GraphicalItems()
    def get_param(layer: int) -> list[float]:
    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).getWxRect()
    width = list(set(g.GetWidth() for g in gs))
    @@ -33,7 +35,7 @@ def get_param(layer: int) -> list[float]:
    w, h = merged.GetWidth() - width, merged.GetHeight() - width
    iu2mm = pcbnew.Iu2Millimeter
    return [iu2mm(pos.x), iu2mm(pos.y), iu2mm(w), iu2mm(h)]
    p = max((get_param(layer) for layer in l.RemoveLayerSet(layers).Seq()), key=lambda p: p[2:4])
    p = max((p for layer in l.RemoveLayerSet(layers).Seq() if (p := get_param(layer)) != None), key=lambda p: p[2:4])
    if deg != 0:
    p += [-deg]
    return p
    @@ -55,4 +57,4 @@ def to_dict(p) -> dict[str, float]:

    # 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")
    get_layout("/home/steven/gitproj/musk/architeuthis_dux.kicad_pcb", lambda sw: str(sw.GetReference()).startswith("S"))
  3. crides revised this gist Feb 3, 2023. 1 changed file with 19 additions and 10 deletions.
    29 changes: 19 additions & 10 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    # 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, pprint
    import re, pcbnew, json
    from typing import Callable
    from functools import reduce

    @@ -13,36 +13,45 @@ def get_layout(fn: str, f: Callable[[pcbnew.FOOTPRINT], bool]) -> None:
    .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]:
    def get_params(fp: pcbnew.FOOTPRINT) -> list[float]:
    deg = fp.GetOrientationDegrees()
    if deg != 0:
    deg -= 90 * round(deg / 90) # snap to nearest 90deg
    fp.Rotate(fp.GetCenter(), -deg * 10)
    bb = fp.GetBoundingBox(False, False)
    l = fp.GetLayerSet()
    gis = fp.GraphicalItems()
    def get_param(layer: int) -> tuple[float, float, float, float]:
    def get_param(layer: int) -> list[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}")
    raise ValueError(f"more than 1 width: {width} on layer: {pcbnew.LayerName(layer)}")
    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:])
    return [iu2mm(pos.x), iu2mm(pos.y), iu2mm(w), iu2mm(h)]
    p = max((get_param(layer) for layer in l.RemoveLayerSet(layers).Seq()), 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:])
    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]
    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
    pprint.pprint([to_dict(p) for p in params])
    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
  4. crides revised this gist Feb 3, 2023. 1 changed file with 3 additions and 0 deletions.
    3 changes: 3 additions & 0 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,6 @@
    # 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, pprint
    from typing import Callable
    from functools import reduce
  5. crides revised this gist Feb 3, 2023. 1 changed file with 3 additions and 6 deletions.
    9 changes: 3 additions & 6 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -7,12 +7,8 @@ def get_layout(fn: str, f: Callable[[pcbnew.FOOTPRINT], bool]) -> None:
    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))
    .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)
    @@ -46,4 +42,5 @@ def to_dict(p) -> dict[str, float]:
    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")
  6. crides created this gist Feb 3, 2023.
    49 changes: 49 additions & 0 deletions layout.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,49 @@
    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:
    get_layout("musk.kicad_pcb", lambda sw: sw.GetValue() == "diode-choc")