--- jupyter: description: 'From a printed circuit board to a 3D model and a mesh in Python with planex2d: read a KiCad board with its copper named by net, find the largest net, build the board in 3D from its stackup, write STEP for the enclosure, and mesh a net''s copper with netgen.' --- # From PCB board to 3D model or mesh A circuit board goes to mechanical CAD as a 3D model, to fit its enclosure, and to a field solver as a mesh of its copper. planex2d reads the board with every copper layer named by net, builds the whole board in 3D from its stackup, writes it as STEP, and passes the copper of one net to netgen. KiCad boards are read directly, and boards from Altium, Eagle, PADS, EasyEDA, CADSTAR, Fabmaster, P-CAD, gEDA, Cadence Allegro and SolidWorks PCB through KiCad 10's `kicad-cli`. The first cell is the complete workflow, ready to copy and run, and the section Step by step runs it in parts, with the plots one looks at along the way. The example is the `pic_programmer` demo from KiCad's own demo projects, [demos/pic_programmer](https://gitlab.com/kicad/code/kicad/-/tree/master/demos/pic_programmer) in the KiCad repository, licensed [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) by the KiCad project. The pictures on this page made from it are under the same license. The board file is [pic_programmer.kicad_pcb](../integrations/pcb/pic_programmer.kicad_pcb). ```python nbsphinx="hidden" import shutil from docs_figures import Row shutil.copy("../integrations/pcb/pic_programmer.kicad_pcb", "pic_programmer.kicad_pcb") ``` ```python import ngsolve as ngs from ngsolve_webgpu.jupyter import Draw import planex as px board = px.FromKiCad("pic_programmer.kicad_pcb") area = {} for f in board["B.Cu"].faces: area[f.name] = area.get(f.name, 0.0) + f.mass print("largest nets on B.Cu", sorted(area.items(), key=lambda kv: -kv[1])[:3]) gnd = px.Unify(list(board["B.Cu"].faces(name="GND"))) model = px.FromKiCad("pic_programmer.kicad_pcb", px.stackups.kicad) model.WriteStep("pic_programmer.step") gnd.name = "GND" mesh = ngs.Mesh(px.OCCGeometry(gnd).GenerateMesh(maxh=0.002)) print(mesh.ne, "elements |", mesh.GetMaterials()) Draw(mesh) ``` ## Step by step ### Reading the board `px.FromKiCad` returns one shape per layer, by KiCad's layer names, in meters. The copper layers have their faces named by net and their drill holes cut out, and `Edge.Cuts` is the board's outline as a face. The warning names what is not read yet, the texts. See [KiCad](../integrations/pcb/kicad.md) for the layers and [KiCad, Altium, Eagle and more](../integrations/pcb/index.md) for the boards of other programs. ```python import ngsolve as ngs from ngsolve_webgpu.jupyter import Draw import planex as px board = px.FromKiCad("pic_programmer.kicad_pcb") print(list(board)) print("board", board["Edge.Cuts"].bounding_box) ``` ### Finding a net The faces of a copper layer carry their nets as names, so the copper of one net is a selection by name. Summed by net, the bottom layer's largest copper is the ground pour, `GND`, far larger than any other net (Figure 1). `px.Unify` joins its faces into one region (see [Boolean operations](../core/booleans/index.md)). ```python area = {} for f in board["B.Cu"].faces: area[f.name] = area.get(f.name, 0.0) + f.mass print(sorted(area.items(), key=lambda kv: -kv[1])[:4]) gnd = px.Unify(list(board["B.Cu"].faces(name="GND"))) print("GND copper on B.Cu", gnd.mass) # px.Plot(gnd) ``` ```python nbsphinx="hide-input" Row((board["Edge.Cuts"], "The board, the GND pour on the bottom layer in cyan", {"fill": False, "highlight": list(gnd.edges), "names": False}), caption="The bottom layer's ground pour") ``` ### The board in 3D With a stackup instead of a layer name, `px.FromKiCad` returns the whole board as one OpenCASCADE shape, with the dielectric drilled through, the hole walls, and the copper layers at their heights, named by net. `px.stackups.kicad` takes the stackup stored in the board file, here a core of 1.51 mm, and `px.stackups` also holds manufacturers' tables. `WriteStep` writes the model for the CAD program that designs the enclosure (see [To OCC shapes](../integrations/occ/export.md)). ```python model = px.FromKiCad("pic_programmer.kicad_pcb", px.stackups.kicad) print(len(model.solids), "solid,", len(model.faces), "faces") model.WriteStep("pic_programmer.step") Draw(model, settings={"Objects": {"Edges": False}}) ``` ### Meshing a net's copper The ground pour's name becomes the material of the mesh, and netgen meshes it with elements of at most 2 mm, ready for a field or current solve in NGSolve (see [Meshing a face](../integrations/netgen/meshing.md)). gmsh takes the same face just as easily through `WriteGeo` (see [gmsh and FEniCSx](../integrations/fenicsx.md)). ```python gnd.name = "GND" mesh = ngs.Mesh(px.OCCGeometry(gnd).GenerateMesh(maxh=0.002)) print(mesh.ne, "elements |", mesh.GetMaterials()) Draw(mesh) ``` ## Other formats | Program | Board file | See | |---|---|---| | KiCad | `.kicad_pcb` | [KiCad](../integrations/pcb/kicad.md) | | Altium Designer | `.PcbDoc` | [Altium](../integrations/pcb/altium.md) | | Eagle | `.brd` | [Eagle](../integrations/pcb/eagle.md) | | EasyEDA | `.zip`, `.epro`, `.json` | [EasyEDA](../integrations/pcb/easyeda.md) | | PADS | `.asc` | [PADS](../integrations/pcb/pads.md) | | Allegro, CADSTAR, Fabmaster, P-CAD, gEDA, SolidWorks PCB | | [Other formats](../integrations/pcb/other-formats.md) | ## Pitfalls - **Units.** The layers come in meters, so `maxh=0.002` is 2 mm. - **Copper without thickness.** In the 3D model, the copper layers are faces at their heights, without a thickness of their own. The dielectric carries the board's thickness. - **Boards from other programs.** They need KiCad 10's `kicad-cli`, which fills their copper zones before reading. Without it, the call raises an error that names the missing tool. - **Large models.** A board with many holes gives a model of many faces, and writing it as STEP takes OpenCASCADE a while.