main
John Lauer README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/ 39a59ef 7d ago
"""Run with KiCad 10's python.exe. Stage 'base': footprints, nets, outline -> board + pads.json.
Stage 'final': base + tracks/vias/zones from routes.json, zones filled, saved."""
import json, os, sys, math
import pcbnew

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
STD_FP = r"C:\Users\john\AppData\Local\Programs\KiCad\10.0\share\kicad\footprints"
PRJ_FP = os.path.join(ROOT, "kicad", "lib")
OUT = os.path.join(ROOT, "kicad", "ocp-oculink-riser.kicad_pcb")
STAGE = sys.argv[1] if len(sys.argv) > 1 else "base"

nl = json.load(open(os.path.join(ROOT, "design", "netlist.json")))
lay = json.load(open(os.path.join(ROOT, "design", "layout.json")))
MM = pcbnew.FromMM


def V(x, y):
    return pcbnew.VECTOR2I(MM(x), MM(y))


LAYERS = {"L1": pcbnew.F_Cu, "L3": pcbnew.In2_Cu, "L5": pcbnew.In4_Cu, "L6": pcbnew.B_Cu,
          "L2": pcbnew.In1_Cu, "L4": pcbnew.In3_Cu}


def new_board():
    b = pcbnew.BOARD()
    b.SetCopperLayerCount(6)
    ds = b.GetDesignSettings()
    ds.SetBoardThickness(MM(1.6))
    # JLCPCB 6-layer standard process minimums (adom/pcb-design-rules jlcpcb-6L)
    ds.m_MinClearance = MM(0.09); ds.m_TrackMinWidth = MM(0.09)
    ds.m_ViasMinSize = MM(0.25); ds.m_ViasMinAnnularWidth = MM(0.05)
    ds.m_MinThroughDrill = MM(0.15); ds.m_HoleToHoleMin = MM(0.2)
    ds.m_HoleClearance = MM(0.12); ds.m_CopperEdgeClearance = MM(0.2)
    try:
        nc = ds.m_NetSettings.GetDefaultNetclass()
    except Exception:
        nc = None
    if nc is not None:
        nc.SetClearance(MM(0.12)); nc.SetTrackWidth(MM(0.15))
        nc.SetViaDiameter(MM(0.4)); nc.SetViaDrill(MM(0.2))
        try:
            nc.SetDiffPairWidth(MM(0.163)); nc.SetDiffPairGap(MM(0.15))
        except Exception:
            pass
    b.SetLayerName(pcbnew.In1_Cu, "In1.Cu")
    # nets
    nets = {}
    ncp = os.path.join(ROOT, "design", "nc_nets.json")
    nc = json.load(open(ncp)) if os.path.exists(ncp) else {}
    for name in sorted(set(nl["nets"]) | set(nc.values())):
        ni = pcbnew.NETINFO_ITEM(b, name)
        b.Add(ni)
        nets[name] = ni
    pin_net = {}
    for name, pins in nl["nets"].items():
        for p in pins:
            pin_net[p] = name
    pin_net.update(nc)
    # footprints
    for c in nl["components"]:
        lib, fpname = c["fp"].split(":")
        libdir = os.path.join(PRJ_FP, lib + ".pretty") if lib == "Riser" else os.path.join(STD_FP, lib + ".pretty")
        fp = pcbnew.FootprintLoad(libdir, fpname)
        if fp is None:
            raise SystemExit("cannot load " + c["fp"])
        fp.SetReference(c["ref"])
        fp.SetValue(c["value"])
        fp.SetFPIDAsString(c["fp"])
        for k, v in (("MPN", c.get("mpn", "")), ("Manufacturer", c.get("mfr", "")), ("Source", c.get("src", "")),
                     ("Description", c.get("desc", ""))):
            fp.SetField(k, v or "")
            f_ = fp.GetField(k)
            if f_ is not None:
                f_.SetVisible(False)
        # silk: the silkscreen step places every reference; until then they are hidden. Chip R/C keep no body silk
        # use the project's no-silk copies (Riser:*_NoSilk); ICs, LEDs and connectors keep their pin-1 / polarity marks.
        fp.Reference().SetVisible(False)
        x, y, rot, side = lay["placement"][c["ref"]]
        b.Add(fp)
        fp.SetPosition(V(x, y))
        fp.SetOrientationDegrees(rot)
        if side == "B":
            fp.Flip(V(x, y), pcbnew.FLIP_DIRECTION_TOP_BOTTOM)
        for pad in fp.Pads():
            key = f'{c["ref"]}.{pad.GetNumber()}'
            if key in pin_net:
                pad.SetNet(nets[pin_net[key]])
        # 3D model binding
        if c.get("model"):
            fp.Models().clear()
            m = pcbnew.FP_3DMODEL()
            m.m_Filename = c["model"]
            fp.Models().push_back(m)
    # outline: lines and arcs (every corner filleted)
    for e in lay["edges"]:
        s = pcbnew.PCB_SHAPE(b)
        if e[0] == "line":
            s.SetShape(pcbnew.SHAPE_T_SEGMENT)
            s.SetStart(V(e[1], e[2])); s.SetEnd(V(e[3], e[4]))
        else:
            s.SetShape(pcbnew.SHAPE_T_ARC)
            s.SetArcGeometry(V(e[1], e[2]), V(e[3], e[4]), V(e[5], e[6]))
        s.SetLayer(pcbnew.Edge_Cuts); s.SetWidth(MM(0.05))
        b.Add(s)
    return b, nets


def export_pads(b):
    out = []
    for fp in b.GetFootprints():
        for pad in fp.Pads():
            sz = pad.GetSize(pcbnew.F_Cu)
            lset = [pcbnew.BOARD.GetStandardLayerName(l) for l in pad.GetLayerSet().CuStack()]
            poly = pad.GetEffectivePolygon(pcbnew.F_Cu if pad.IsOnLayer(pcbnew.F_Cu) else pcbnew.B_Cu, pcbnew.ERROR_INSIDE)
            outline = []
            for oi in range(poly.OutlineCount()):
                ol = poly.Outline(oi)
                outline.append([(pcbnew.ToMM(ol.CPoint(i).x), pcbnew.ToMM(ol.CPoint(i).y)) for i in range(ol.PointCount())])
            dr = pad.GetDrillSize()
            out.append(dict(ref=fp.GetReference(), num=pad.GetNumber(), net=pad.GetNetname(),
                            x=pcbnew.ToMM(pad.GetPosition().x), y=pcbnew.ToMM(pad.GetPosition().y),
                            w=pcbnew.ToMM(sz.x), h=pcbnew.ToMM(sz.y), layers=lset, poly=outline,
                            drill=[pcbnew.ToMM(dr.x), pcbnew.ToMM(dr.y)], attr=str(pad.GetAttribute())))
    return out


def add_routes(b, nets, which="routes.json"):
    r = json.load(open(os.path.join(ROOT, "design", which)))
    for (lay_, net, x1, y1, x2, y2, w) in r["tracks"]:
        t = pcbnew.PCB_TRACK(b)
        t.SetStart(V(x1, y1)); t.SetEnd(V(x2, y2)); t.SetWidth(MM(w))
        t.SetLayer(LAYERS[lay_]); t.SetNet(nets[net])
        b.Add(t)
    for (x, y, net, d, h) in r["vias"]:
        v = pcbnew.PCB_VIA(b)
        v.SetPosition(V(x, y)); v.SetWidth(MM(d)); v.SetDrill(MM(h))
        v.SetViaType(pcbnew.VIATYPE_THROUGH)
        v.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
        v.SetNet(nets[net])
        b.Add(v)
    for z in r.get("zones", []):
        zone = pcbnew.ZONE(b)
        zone.SetLayer(LAYERS[z["layer"]] if z["layer"] in LAYERS else getattr(pcbnew, z["layer"]))
        if z.get("keepout"):
            zone.SetIsRuleArea(True)
            zone.SetDoNotAllowTracks(z.get("no_tracks", True)); zone.SetDoNotAllowVias(z.get("no_vias", True))
            zone.SetDoNotAllowZoneFills(z.get("no_pour", True)); zone.SetDoNotAllowPads(False)
            zone.SetDoNotAllowFootprints(False)
            if z.get("layers"):
                ls = pcbnew.LSET()
                for ln in z["layers"]:
                    ls.AddLayer(LAYERS[ln])
                zone.SetLayerSet(ls)
        else:
            zone.SetNet(nets[z["net"]])
            zone.SetMinThickness(MM(0.15))
            zone.SetLocalClearance(MM(z.get("clearance", 0.2)))
            zone.SetAssignedPriority(z.get("priority", 0))
            zone.SetPadConnection(pcbnew.ZONE_CONNECTION_THERMAL if z.get("thermal", True) else pcbnew.ZONE_CONNECTION_FULL)
            zone.SetThermalReliefGap(MM(0.25)); zone.SetThermalReliefSpokeWidth(MM(0.3))
            zone.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_ALWAYS)
        ol = zone.Outline()
        ol.NewOutline()
        for (x, y) in z["poly"]:
            ol.Append(MM(x), MM(y))
        b.Add(zone)
    for t in r.get("texts", []):
        tx = pcbnew.PCB_TEXT(b)
        tx.SetText(t["text"]); tx.SetPosition(V(t["x"], t["y"]))
        tx.SetLayer(getattr(pcbnew, t["layer"].replace(".", "_")))
        tx.SetTextSize(V(t["size"], t["size"])); tx.SetTextThickness(MM(t["size"] * 0.15))
        if t.get("angle"):
            tx.SetTextAngleDegrees(t["angle"])
        if t["layer"].startswith("B."):
            tx.SetMirrored(True)
        b.Add(tx)


if __name__ == "__main__":
    b, nets = new_board()
    if STAGE == "base":
        json.dump(export_pads(b), open(os.path.join(ROOT, "design", "pads.json"), "w"))
        pcbnew.SaveBoard(OUT.replace(".kicad_pcb", "-base.kicad_pcb"), b)
        print("base ok", len(b.GetFootprints()), "footprints")
    elif STAGE == "start":
        # the run's start board: interface parts in place, the rest staged beside the outline, live copper held back
        st = json.load(open(os.path.join(ROOT, "design", "routes_start.json")))
        for fp in b.GetFootprints():
            ref = fp.GetReference()
            if ref in st["stage"]:
                x, y, rot, side = st["stage"][ref]
                fp.SetPosition(V(x, y)); fp.SetOrientationDegrees(rot)
        add_routes(b, nets, "routes_start.json")
        filler = pcbnew.ZONE_FILLER(b)
        filler.Fill(b.Zones())
        out = os.path.join(ROOT, "aiflow", "start", "ocp-oculink-riser.kicad_pcb")
        os.makedirs(os.path.dirname(out), exist_ok=True)
        pcbnew.SaveBoard(out, b)
        print("start ok", len(b.GetTracks()), "items")
    else:
        add_routes(b, nets)
        filler = pcbnew.ZONE_FILLER(b)
        filler.Fill(b.Zones())
        pcbnew.SaveBoard(OUT, b)
        print("final ok", len(b.GetTracks()), "track/via items,", b.GetAreaCount(), "zones")