soldermaskdocs

KiCad and 3D models

Gerbers are the end of the line — they carry copper, not intent. So every version also gets a board a person can open and change, and the parts on it are drawn from real geometry.

The KiCad board

kicad.py writes <board>.kicad_pcb in KiCad 7's format (read by 7, 8, 9 and 10): the layer table, design rules, the net table, one footprint per part with its LCSC, manufacturer and purpose as properties, pads with nets, fab/silk/courtyard outlines, the board edge, and — when routed — every segment and via. UUIDs are deterministic, so the file diffs between runs.

The pipeline's frame is millimetres with y up; KiCad's is y down. The transform is proven in the module and then checked: kicad.verify() re-parses the file it just wrote, reapplies KiCad's rotation convention to every pad, and compares each pad centre with the placer's own position to 0.1 µm. A board whose .kicad_pcb sits in a different frame from its Gerbers is valid, openable and wrong in exactly the way the CPL refuses to risk. When the optional kiutils library is present the file is also read by it; its absence is reported, not treated as a pass.

The KiCad schematic

kicad_sch.py writes <board>.kicad_sch beside the board, in the same KiCad 7 format, from the sheet sheet.py lays out for the workbench — one layout, two renderers, so the file KiCad opens is the drawing on screen. Every part is its KiCad library symbol (the drawing symbols.py read from the legacy library, written back in the s-expression form), placed where the sheet put it, with Reference, Value, Footprint, and the LCSC code, manufacturer and purpose as hidden fields; supply pins end in power symbols named for the net (the name is the symbol's Value and its pin's name, which is the one spelling KiCad 7 and KiCad 8+ both read); signal nets are wires and junctions where the sheet drew them and local labels where it did not; a pin on no net is a no_connect; a PWR_FLAG sits on each supply net that no power-output pin drives.

Then it is read back. kicad_sch.connectivity() reads the sheet's geometry the way KiCad does — a pin is on a wire when its end sits on it, two wires touch when an end of one sits on the other, a label or a port names its group — and compares the result net for net with the board; a mismatch is a gap on the version. With kicad-cli installed the command line also asks KiCad itself: kicad-cli sch export netlist on the written file, compared net for net against the design, and kicad-cli sch erc. On 17 Sep 2026, on all 17 projects in the tree, KiCad's netlist was the design's on every net and ERC found 0 errors; the four lines the check did raise were true of the boards and not the drawings — a SOT-223 tab pad that the library's three-pin symbol has no pin for (three boards, said as U1 pad 4 (+3V3) has no pin in its drawing and is not on the sheet), and one old version whose regulator had pad 1 on two nets.

What the file does not carry: a hierarchy (one sheet), footprint or symbol libraries beside it (the symbols are embedded, as KiCad itself embeds them; the footprints are named, not shipped), and any position for a part whose drawing the library lacks — those draw as a box with the pin names the resolver gave them.

Vendor 3D models

EasyEDA publishes a 3D model for every part LCSC sells, keyed by the LCSC code the parts stage already chose. models3d.py reads the model's uuid and offset off the footprint (eeformat.model), fetches it — a non-standard OBJ with inline material blocks, parsed the way easyeda2kicad's exporter does — welds and quantises it, centres it on the footprint origin with the part's bottom on the board, and caches the result under .cache/models3d (absent models are cached too, so a build does not re-ask; a model server that did not answer is not, since 26 Sep 2026, when a timeout stopped being written down as an absence). The STEP is fetched only for KiCad's copy. Models past 40,000 triangles are skipped in favour of the primitive; a self-contained HTML file has a byte budget.

Two consumers, one source:

  • meshes.json — the viewer's table, one mesh per distinct LCSC code, inlined into the standalone export and loaded beside the board in the app.
  • models/<lcsc>.wrl — KiCad VRML built from the same mesh, so the two pictures cannot drift apart. The .kicad_pcb footprints point at ${KIPRJMOD}/models/<lcsc>.wrl, which resolves both when a person opens the board and when kicad-cli is told where the project is.

Orientation. Vendor models are drawn in the vendor's frame and the pipeline's footprints mostly are not — EasyEDA lays a SOT-223 on its side where the IPC table stands it up. turn_for() measures the turn from the pads (easyeda.best_turn) rather than guessing it from the shape, and bakes it into the VRML vertices. When the two footprints share no pad names the orientation is unchecked and the board says so; when the best fit still sits more than 0.35 mm off, the note says check it against the datasheet.

What this is not

Vendor and community geometry, not verified CAD. Good enough to see that a connector faces the wrong way; not good enough to check a mechanical fit.

With KiCad installed

kicadcli.py uses kicad-cli when it can be found (kicad-cli on the path, Homebrew, /Applications/KiCad/KiCad.app) and proven to run. Then the command line also runs a DRC on the copper (drc n errors, m warnings), a raytraced render and a GLB export, and cross-checks KiCad's own Gerber plot against the pipeline's layer by layer to 0.012 mm. KiCad is 1.3 GB and cannot be pip-installed, so a build without it loses the renders and nothing else: kicad-cli not installed — no DRC, render or GLB (brew install --cask kicad). --no-render skips the raytrace and GLB while keeping the DRC, which is the part worth having.

On this page