A Python tool that parses real .kicad_sch files (KiCad 7/8 S-expression format) and generates interactive Three.js 3D PCB visualizations in a single standalone HTML file.
python kicad_sch_to_3d.py <input.kicad_sch> [output.html]# Examples
python kicad_sch_to_3d.py TPS54302_BuckConverter.kicad_sch
# → TPS54302_BuckConverter.3d.html
python kicad_sch_to_3d.py NE555_Astable.kicad_sch my_output.html
# → my_output.htmlNo dependencies required — pure Python 3 standard library.
┌──────────────────────────────────────────────────────────┐
│ .kicad_sch file │
│ (S-expression: symbols, wires, labels, junctions, ...) │
└──────────────┬───────────────────────────────────────────┘
│
┌───────▼───────┐
│ SExprParser │ Tokenizer + recursive descent
│ │ → nested Python lists
└───────┬───────┘
│
┌──────────▼──────────┐
│ KiCadSchExtractor │ Walk S-expr tree, extract:
│ │ - lib_symbols (pin defs)
│ │ - components (ref, value, xy)
│ │ - wires (x1,y1 → x2,y2)
│ │ - junctions, labels
└──────────┬──────────┘
│
┌──────▼──────┐
│ NetResolver │ BFS flood-fill from wire
│ │ endpoints + label/power coords
│ │ → assign net name per wire
└──────┬──────┘
│
┌──────────▼──────────┐
│ HTML Generator │ Serialize to JSON, inject into
│ │ Three.js template with:
│ │ - 3D component models
│ │ - Colored PCB traces
│ │ - Interactive UI (orbit, hover)
└──────────┬──────────┘
│
┌───────▼───────┐
│ .html output │ Standalone, zero-dependency
│ (Three.js r128│ Open in any browser
│ via CDN) │
└───────────────┘SExprParser)KiCad uses Lisp-style S-expressions. The parser handles:
| Token Type | Example | Python Output |
|---|---|---|
| List | (wire (pts ...)) | ['wire', ['pts', ...]] |
| Quoted string | "TPS54302" | 'TPS54302' |
| Integer | 42 | 42 |
| Float | 139.7 | 139.7 |
| Atom | default | 'default' |
| Comment | ; this is ignored | (skipped) |
Escaped characters in strings (\") are handled. Nested lists recurse to arbitrary depth.
KiCadSchExtractor)Walks the parsed tree and populates a KiSchematic data model:
@dataclass
class KiSchematic:
title: str # from (title_block ...)
date: str
rev: str
company: str
lib_symbols: dict # name → KiLibSymbol (pin definitions)
components: list # KiComponent (ref, value, x, y, angle, ...)
wires: list # KiWire (x1, y1, x2, y2)
junctions: list # KiJunction (x, y)
labels: list # KiLabel (text, x, y, type)Component classification uses both lib_id and reference prefix:
| Pattern | Type |
|---|---|
lib_id = C, C_Polarized or ref starts with C | Capacitor |
lib_id = R, R_Small or ref starts with R | Resistor |
lib_id = L or ref starts with L | Inductor |
ref starts with U or IC | IC |
#PWR, #FLG, or lib marked (power) | Power symbol |
NetResolver)Assigns a net name to every wire segment using BFS flood-fill:
NET_<index>Label "VCC" @ (160, 70)
↓ nearest endpoint
Wire endpoint (160, 72.39)
↓ flood-fill
Wire (160, 72.39)→(160, 80.01) = VCC
↓ shared endpoint
Wire (160, 80.01)→(160, 82.55) = VCC
...Converts all extracted data to JSON and embeds it in a self-contained HTML template:
// Auto-generated from parser output
const COMPS = [
{"ref":"U1","value":"NE555","x":0.0,"z":0.0,"type":"IC",...},
{"ref":"R1","value":"1kΩ","x":3.66,"z":-2.29,"type":"R",...},
...
];
const WIRES = [
{"x1":-1.2,"z1":-0.5,"x2":3.6,"z2":-0.5,"net":"VCC"},
...
];3D models are procedurally generated per component type:
| Type | 3D Model |
|---|---|
| IC | Dark box + marking surface + pin-1 dot + gull-wing pads |
| Capacitor (electrolytic) | Tall dark box + terminal pads |
| Capacitor (ceramic) | Short tan box |
| Resistor | Small dark box + end pads |
| Inductor | Large dark box (shielded style) |
| Action | Effect |
|---|---|
| Mouse drag | Orbit camera |
| Scroll wheel | Zoom in/out |
| Touch drag | Orbit (mobile) |
| Pinch | Zoom (mobile) |
| Hover component | Tooltip with ref, value, footprint, pins |
| PANEL → click net | Highlight all traces/components on that net |
| GLOW button | Toggle ambient glow under components |
| LABELS button | Toggle floating ref/value labels |
| RESET button | Reset camera to default view |
#0a3020) with copper grid overlay| Element | Parsed | Used in 3D |
|---|---|---|
(title_block ...) | ✅ | HUD display |
(lib_symbols ...) | ✅ | Pin names, component classification |
(symbol ...) (instances) | ✅ | 3D component placement |
(wire ...) | ✅ | PCB traces |
(junction ...) | ✅ | Junction spheres |
(label ...) | ✅ | Net name resolution |
(global_label ...) | ✅ | Net name resolution |
(hierarchical_label ...) | ✅ | Net name resolution |
(no_connect ...) | ❌ | Not yet |
(bus ...) | ❌ | Not yet |
(sheet ...) (hierarchy) | ❌ | Single-sheet only |
(polyline ...) / (arc ...) | ❌ | Decorative, not parsed |
NET_<n>, move the label closer to a wire endpoint in KiCadD[0..7] are not expanded into individual netsNET_0, NET_5, etc.The net resolver couldn't match a label to those wire segments. Common fixes:
(junction ...) at T-connections(label ...) nodes on isolated wire clustersThe parser uses the (at x y angle) field from each (symbol ...) instance. Verify coordinates match your KiCad layout. Components classified as power symbols (#PWR*, #FLG*) are hidden from the 3D view by design.
Ensure it's KiCad 7 or 8 format (starts with (kicad_sch (version 2023...)). KiCad 5/6 legacy formats use a different syntax and are not supported.
$ python kicad_sch_to_3d.py NE555_Astable.kicad_sch
[1/5] Reading NE555_Astable.kicad_sch ...
[2/5] Parsing S-expressions (21,740 chars) ...
[3/5] Extracting schematic data ...
Title: 555 Timer — Astable Multivibrator
Components: 8
Wires: 27
Junctions: 3
Labels: 6
Lib syms: 6
[4/5] Resolving nets ...
Nets found: 6 named, 1 unnamed
CTRL — 2 wire segments
DISCH — 5 wire segments
GND — 5 wire segments
OUT — 1 wire segments
TIMING — 8 wire segments
VCC — 5 wire segments
[5/5] Generating Three.js HTML ...
Output: NE555_Astable.3d.html (25,761 bytes)
Done!kicad_sch_to_3d.py # Main script (single file, no deps)
├── SExprParser # S-expression tokenizer & parser
├── KiCadSchExtractor # Schematic data extractor
├── NetResolver # BFS net connectivity resolver
├── generate_threejs_html() # Three.js HTML generator
├── classify_component() # Ref/lib_id → type classifier
└── main() # CLI entry pointThis tool is provided as-is for educational and prototyping purposes.