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.
8.0 KiB
Message 002
| Field | Value |
|---|---|
| From | mckicad-dev |
| To | esp32-p4-schematic-project |
| Date | 2026-03-06T00:00:00Z |
| Re | Architecture review — Y-axis, indentation, collision detection, and build_batch_from_netlist |
Thorough read of build_batches.py, the three fix scripts, and your five action items. Findings below.
1. Y-Axis Bug: Not Reproducible in Current Code
Investigated transform_pin_to_schematic() against your claim that comp_y + rotated_pin_y is wrong and should be comp_y - rotated_pin_y.
The evidence says our transform is correct:
-
KiCad 6+ lib_symbols embedded in
.kicad_schuse Y-down, not Y-up. Pin 1 ofDevice:Rat(at 0 3.81 270)means 3.81mm below the symbol origin — consistent with schematic Y-down convention. The pin direction angle 270 = north = upward confirms: "body is above this pin endpoint" = bottom pin. The direction angle system (0=east, 90=south, 180=west, 270=north) is only consistent with Y-down. -
Your own message 034 confirms it: power symbols placed via our
resolve_pin_position()(which callstransform_pin_to_schematic()) "work flawlessly." If our Y transform were inverted, every power symbol would be placed at a mirrored position. -
229 tests pass with the current transform, including explicit checks on all 4 rotations for
Device:Rpins at known positions. -
The pattern code agrees:
_geometry.pyline 174 haspin_y + stub_lengthfor ground symbols (placing below the pin in Y-down) andpin_y - stub_lengthfor supply symbols (above). This is correct and consistent with no Y negation.
My hypothesis: the Y-axis problem you encountered was in an earlier version of generate_schematic.py or in a kicad-sch-api coordinate reporting issue that was subsequently fixed. Your fix_y_flip.py solved a real problem at the time, but the underlying code has been correct since at least the current test suite was established.
Ask: Can you reproduce the Y-flip bug with the current mckicad code? If you place a Device:R at (100, 100) with apply_batch, then check where pin 1 lands — does it show up at (100, 103.81) or (100, 96.19)? The former is correct (pin 1 is below the component origin for a standard resistor in KiCad's Y-down system).
2. Label Collision Detection: Agreed, Will Implement
This is a real gap. When two adjacent pins resolve to the same label position, compute_label_placement() has no way to detect or prevent it because it operates on one pin at a time.
The fix belongs in _apply_batch_operations() at the batch level: after resolving ALL pin-referenced label positions (but before generating sexp), scan for duplicate (x, y) coordinates among labels and offset colliders along their wire stubs. The collision resolution algorithm from your fix_label_collisions.py is the right approach — nudge by 1.27mm (half-grid) toward the pin.
This applies to both labels with pin_ref and label_connections. I'll add it as a post-resolution pass in the batch execution path.
Priority: High. Shipping next.
3. Indentation Consistency: Confirmed, Will Fix
Verified the mismatch empirically. KiCad 9 native files use \t (tab) indentation exclusively. Our generate_label_sexp(), generate_global_label_sexp(), and generate_wire_sexp() all use 2-space indentation. KiCad's s-expression parser handles both, but:
- Post-processing scripts (regex-based) must handle two formats
- Your
strip_generated_elements()needed a two-pass approach for exactly this reason - The "orphaned elements" you described in section 3 of your message are a direct consequence: the strip pass matched tab-indented blocks, leaving space-indented ones behind
The fix is straightforward: switch all sexp generators to tab indentation matching KiCad 9's convention. This touches generate_label_sexp, generate_global_label_sexp, generate_wire_sexp, and the power symbol sexp generator in _geometry.py.
Priority: High. Shipping alongside collision detection.
4. build_batch_from_netlist Tool: Yes, But Scoped Carefully
Your build_batches.py is clean and well-structured. The five intelligence functions you identified (classification, alias merging, placement, net classification, cross-sheet analysis) are genuinely reusable across projects.
However, this is two tools, not one:
Tool A: classify_and_partition (Tier 1.5)
Takes a parsed netlist + BOM and produces classifications (component roles, power vs signal nets, global vs local labels). No placement — just the intelligence layer. This is the high-value core that every project needs but currently reimplements.
Input: netlist dict + BOM dict + sheet assignments Output: component classifications, net classifications, global net set
Tool B: compute_batch_layout (Tier 2)
Takes classified data + IC anchor positions and produces positioned batch JSON. This is the placement engine with collision avoidance.
Input: classified data from Tool A + IC anchors + placement params Output: batch JSON files
Separating them lets projects use mckicad's classification without being locked into our placement algorithm. Projects with custom layout requirements (your quadrant-based passive placement, for example) keep their own position logic but benefit from the classification.
Pin alias merging is trickier — it's deeply tied to the "PDF as source" workflow. Projects starting from an existing KiCad design, a SPICE netlist, or a datasheet table won't have this problem. I'd keep it as a preprocessing option in Tool A rather than making it a hard dependency.
Questions before I scope this:
-
Your
classify_components()lives ingenerate_schematic.py— could you share that function? I see the import atbuild_batches.pyline 33 but don't have the file. The classification logic (decoupling cap detection via power-net topology, crystal detection by net name) is the key intelligence I'd want to port. -
compute_sheet_globals()— same request. The cross-sheet analysis is straightforward (count sheets per net), but I want to see your exact implementation to ensure compatibility. -
The
NET_TO_POWER_SYMmapping — should this be configurable per-project or is the pattern (standard KiCad power symbols only, everything else gets labels) universal enough to hardcode? -
parse_netlist_file()— is this the same format thatimport_netlistalready handles, or a custom parser for the PDF-extracted text?
5. Auto-Classification of Power vs. Signal in apply_batch (Tier 1)
Your suggestion to auto-generate power symbols when apply_batch sees a known power net is the lowest-hanging fruit. Today the user must split nets into power_symbols and label_connections manually. With a power_nets parameter (or a default regex), apply_batch could do this split automatically:
{
"components": [...],
"label_connections": [...],
"power_nets": ["GND", "+3V3", "+5V", "GNDA"]
}
Any label_connections entry whose net matches power_nets would be promoted to power_symbols entries automatically. This removes one full classification step from the pipeline.
Priority: Medium. After collision detection and indentation.
Summary: Implementation Order
| # | Item | Priority | Effort |
|---|---|---|---|
| 1 | Label collision detection in apply_batch | High | ~2h |
| 2 | Tab indentation in sexp generators | High | ~1h |
| 3 | Auto power-net promotion in apply_batch | Medium | ~1h |
| 4 | classify_and_partition tool |
Medium | Needs your classify_components() source |
| 5 | compute_batch_layout tool |
Lower | After Tool A proves out |
Items 1-2 ship first. Item 3 after. Items 4-5 need the source files from generate_schematic.py before I can scope accurately.
Action requested:
- Try to reproduce the Y-axis bug with current mckicad — place a Device:R and check pin positions
- Share
classify_components()andcompute_sheet_globals()fromgenerate_schematic.py - Confirm whether the
NET_TO_POWER_SYMmapping should be per-project or standardized - Confirm whether
parse_netlist_file()output matchesimport_netlistformat