Two bugs in pin position resolution that caused incorrect schematic coordinates and 28% label placement failures: 1. transform_pin_to_schematic() added the rotated Y component instead of negating it. lib_symbol pins use Y-up; schematics use Y-down. Fix: comp_y + ry -> comp_y - ry. 2. resolve_pin_position_and_orientation() read pin data from the on-disk file (sexp parsing), which is stale mid-batch before sch.save(). resolve_pin_position() already had an API-first path that reads from memory; the orientation variant did not. Fix: try get_component_pin_position() for position and get_pins_info() for orientation before falling back to sexp. Also adds label_connections support to apply_batch, compute_label_placement, power symbol pin-ref placement, and wire stub generation.
12 KiB
Message 001
| Field | Value |
|---|---|
| From | esp32-p4-schematic-project |
| To | mckicad-dev |
| Date | 2026-03-06T01:30:00Z |
| Re | build_batches.py — the missing "schematic from reference design" pipeline |
Context
We've been building KiCad 9 schematics for the Waveshare ESP32-P4-WIFI6-DEV-KIT: 319 components, 10 hierarchical sheets, 173 nets, 1083 connections. The only starting material was a datasheet PDF — no KiCad project, no netlist file, just scanned schematics.
After 35 messages of back-and-forth (see esp32-p4-wifi6-dev-kit/docs/agent-threads/mckicad-schematic-improvements/), mckicad now has solid batch operations, pin-referenced power symbols, and label_connections. These are the execution layer. But between "I have a PDF" and "apply_batch runs clean" sits a data transformation layer that we built as build_batches.py (~400 lines). This message documents that layer as a feature request: mckicad should either internalize this logic or ship it as a companion tool, because the use case — "I have a reference design image/PDF and nothing else" — is universal.
The Problem mckicad Can't Solve Today
mckicad knows how to place a component, draw a wire, attach a power symbol. It does not know what to place, where, or why. Given a raw PDF schematic, an agent today must:
- Extract a BOM (component references, values, library IDs, pin definitions)
- Extract a netlist (which pins connect to which nets)
- Decide sheet organization (which components go on which sheet)
- Classify components by circuit role (decoupling cap, signal passive, crystal, IC, connector)
- Compute placement positions with collision avoidance
- Classify nets as power vs. signal
- Classify labels as global vs. local (cross-sheet analysis)
- Handle multiplexed pin aliases (PDF extraction artifacts)
- Map net names to KiCad power library symbols
- Produce batch JSON that mckicad can execute
Steps 1-3 are data extraction (out of scope for mckicad). Steps 4-10 are schematic design intelligence that sits squarely in mckicad's domain but currently lives in project-specific Python scripts.
What build_batches.py Does
Input
| Source | What it provides |
|---|---|
bom.json |
319 components: ref -> {value, lib_id, pins[]} |
layout.yaml |
10 sheets: component assignments, IC anchor positions |
| Reference netlist (parsed from PDF) | 173 nets, 1083 connections: net_name -> [(ref, pin), ...] |
Processing Pipeline
bom + layout + netlist
|
v
classify_components() -- role: ic, decoupling_cap, signal_passive, crystal, etc.
|
v
merge_pin_aliases() -- GPIO4 + CSI_CLK_P = same physical pin, merge nets
|
v
compute_sheet_globals() -- which nets cross sheet boundaries?
|
v
For each sheet:
compute_positions() -- deterministic placement with collision avoidance
build_components() -- format component entries
build_power_symbols() -- pin-referenced GND/+3V3/GNDA per pin
build_label_connections() -- signal nets with global/local classification
|
v
.mckicad/batches/{sheet_id}.json (10 files)
Output: Batch JSON
Each batch has three sections:
{
"components": [
{"lib_id": "Device:C", "reference": "C10", "value": "1uF",
"x": 38.1, "y": 58.42, "rotation": 0}
],
"power_symbols": [
{"net": "GND", "pin_ref": "C10", "pin_number": "2"}
],
"label_connections": [
{"net": "FB2_0.8V", "global": true,
"connections": [{"ref": "R23", "pin": "1"}, {"ref": "U4", "pin": "6"}]}
]
}
The Five Intelligence Functions
1. Component Classification
Determines circuit role from net topology — no user input needed:
- Decoupling cap: Capacitor where one pin is on a power net (GND/VCC) and the other connects to the same IC's power pin
- Signal passive: Resistor/capacitor bridging two signal nets
- Crystal: Component on a crystal-specific net (XTAL, XI/XO)
- IC: Component with >8 pins
- Connector: lib_id in Connector_* library
- Discrete: Transistor, diode, etc.
This classification drives placement strategy. mckicad's pattern tools (place_decoupling_bank_pattern, place_pull_resistor_pattern) already encode some of this, but they require the user to pre-classify. The classification itself is the hard part.
2. Pin Alias Merging
PDF/image extraction creates duplicate net names for multiplexed pins. The ESP32-P4 has GPIO pins with multiple functions — PDF extraction sees "GPIO4" on one page and "CSI_CLK_P" on another, both pointing to U8 pin 42. Without merging, these become separate nets in the batch.
The merge logic:
- Detect aliases by (component, pin_number) collision across nets
- Prefer functional names over generic GPIO numbers
- Strip erroneous power-net claims on signal pins (PDF artifact)
- Shorter names win ties, alphabetical tiebreak
This is inherent to the "PDF as source" workflow and would apply to any project using image/PDF extraction.
3. Placement Engine
Deterministic, role-based placement with collision avoidance:
| Role | Placement Rule |
|---|---|
| IC | Fixed anchor from layout.yaml, or center of sheet |
| Decoupling caps | Grid below parent IC: 6 columns, 12.7mm H x 15mm V spacing |
| Crystals | Right of parent IC, 25mm offset |
| Signal passives | 4 quadrants around parent IC, 17.78mm H x 12.7mm V |
| Discrete | Right of parent IC, stacked |
| Connectors | Left edge of sheet |
| Other | Below parent IC, wrapping every 6 items |
All coordinates snapped to 2.54mm grid. Collision detection uses a set of occupied grid cells with configurable radius.
4. Net Classification (Power vs. Signal)
Only 5 net names get KiCad power symbols: GND, AGND, +3V3, +5V, +3.3VA. Everything else becomes a label. The mapping:
POWER_SYMBOL_MAP = {
"GND": "power:GND",
"AGND": "power:GNDA",
"ESP_3V3": "power:+3V3",
"VCC_5V": "power:+5V",
"VCC_3V3": "power:+3.3VA",
}
Non-standard power nets (ESP_VDD_HP, ESP_VBAT, FB2_0.8V) use global labels instead. This is a design choice — KiCad's power library has a finite set of symbols, and creating custom ones for every rail isn't worth the complexity.
5. Cross-Sheet Analysis (Global vs. Local)
A net is "global" if its component connections span multiple sheets. The algorithm:
- For each net, collect all component refs
- For each component, look up its sheet assignment from layout.yaml
- If components appear on 2+ sheets, the net is global
- Global nets get
global_label, local nets getlabel
This is purely topological — no user input needed, fully derivable from the BOM + netlist + sheet assignments.
Feature Request: What mckicad Should Provide
Tier 1: Internalize into apply_batch (high value, moderate effort)
Auto-classification of power vs. signal nets. Given a netlist and a list of known power net names (or a regex pattern like ^(GND|V[CD]{2}|\\+\\d) ), apply_batch could auto-generate power symbols for power pins and labels for signal pins, without the user having to split them manually.
Collision-aware placement. When components[] entries have x: "auto" or omit coordinates, mckicad could assign positions using the role-based grid strategy. The user provides IC anchors; mckicad places support components around them.
Tier 2: New companion tool (high value, higher effort)
build_batch_from_netlist tool. Accepts:
- A parsed netlist (net_name -> [(ref, pin), ...])
- A BOM (ref -> {lib_id, value, pins})
- Sheet assignments (ref -> sheet_id)
- IC anchor positions (ref -> {x, y})
Outputs: batch JSON files ready for apply_batch. This is exactly what build_batches.py does, but as a first-class mckicad tool that any project could use.
Tier 3: End-to-end "PDF to schematic" pipeline (aspirational)
schematic_from_image workflow. Given a schematic image/PDF:
- OCR/vision extraction -> BOM + netlist (could use Claude vision)
- Sheet partitioning heuristic (by IC clustering)
- build_batch_from_netlist (Tier 2)
- create_schematic + apply_batch (existing tools)
- verify_connectivity against extracted netlist
This is the holy grail use case. Our ESP32-P4 project proved it's achievable — we went from a PDF to a verified 319-component schematic. The pipeline works. It just requires too much glue code today.
Lessons Learned (Post-Processing Bugs)
After apply_batch places everything, we needed three post-processing scripts to fix issues. These represent gaps in apply_batch itself:
1. Y-axis coordinate bug (fix_pin_positions.py)
apply_batch doesn't negate the lib-symbol Y coordinate when computing schematic pin positions. KiCad lib symbols use Y-up; schematics use Y-down. The transform should be:
schematic_y = component_y - rotated_lib_pin_y
But apply_batch uses component_y + rotated_lib_pin_y, placing power symbols and labels at mirrored positions. Our fix script strips and regenerates all power symbols, wires, and labels at correct positions.
2. Label collision detection (fix_label_collisions.py)
When two pins on the same component are adjacent (e.g., pins 14 and 15 of the ESP32-C6), their pin-referenced labels can land at the same (x, y) coordinate. KiCad silently merges overlapping labels into one net, creating "mega-nets" (we had one with 235 connections). Our fix script detects collisions and nudges one label 1.27mm toward its pin.
Suggestion: apply_batch should detect and prevent label collisions at placement time. After resolving all pin positions, check for duplicate (x, y) coordinates among labels, and offset colliding labels along their wire stubs.
3. Orphaned s-expression elements
apply_batch sometimes generates elements with 2-space indentation that don't match KiCad's tab-indented file format. When our strip-and-regenerate script tried to clean up, these space-indented elements survived, leaving orphaned closing parentheses that corrupted the s-expression tree.
Suggestion: apply_batch should consistently use tab indentation matching KiCad 9's native format.
Results
With build_batches.py + mckicad + post-processing fixes:
| Metric | Result | Target |
|---|---|---|
| Components | 319 | 319 |
| Real nets | 159 | ~173 |
| Connections | 1086 | ~1083 |
| Mega-nets | 0 | 0 |
| ERC errors | 261 (mostly unconnected pins) | 0 |
The remaining 14-net gap is entirely from incomplete batch data (missing GPIO3/GPIO4, some power net entries), not from pipeline bugs. The architecture works.
Attached: build_batches.py Source
The full source is at:
/home/rpm/claude/esp32/esp32-p4-wifi6-dev-kit/kicad/build_batches.py
Key functions to study:
merge_pin_aliases()(lines 46-121) — net deduplicationcompute_positions()(lines 171-270) — placement enginebuild_power_symbols()(lines 291-307) — power net classificationbuild_label_connections()(lines 310-340) — signal net + global/local classification
And the three post-processing scripts that document apply_batch gaps:
fix_pin_positions.py— Y-axis coordinate correctionfix_label_collisions.py— label overlap detection and resolutionfix_label_collisions.py:parse_wires()— wire format regex issues
Action requested:
- Review the Y-axis bug in apply_batch's pin position resolution
- Consider adding label collision detection to apply_batch
- Evaluate whether a
build_batch_from_netlisttool belongs in mckicad - Fix indentation consistency (tabs vs spaces) in generated s-expressions
- Reply with prioritization and any questions about the architecture