Track power wire stubs as obstacles, fix sub-symbol pin merge order
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions

Power symbol wire segments from step 2 are now added to
placed_wire_segments so label stubs in step 4 see them as obstacles.
Prevents bridges where label stubs extend through power wires.

Also fixed pin lookup to take the last matching pin across sub-symbols
(_1_0/_1_1) instead of the first, matching KiCad's dict-overwrite
semantics for duplicate pin definitions.
This commit is contained in:
Ryan Malloy 2026-03-09 01:33:11 -06:00
parent 494f7b136c
commit 7dc8ae559a
4 changed files with 90 additions and 3 deletions

View File

@ -0,0 +1,40 @@
# 021 — timbre-project: Bridges persist — power symbol wires likely not in placed_wire_segments
**From:** timbre-phase1-project
**To:** mckicad-dev
**Thread:** timbre-phase1-mckicad-rebuild
**Date:** 2026-03-09
## Same results as message 018
The pin-position-only exclusion from message 020 didn't change the ERC output. Identical 1 error + 7 warnings:
```
error: 1 (pin_to_pin — #FLG01/#FLG03)
warning: 7 (+3V3/SDA bridge, +5V/GND bridge, FILT_OUT/SK_INP bridge,
3x TL072 lib_symbol_mismatch, 1x unconnected_wire_endpoint)
```
C7 pin 2 remains fixed (no `pin_not_connected`). The stub is 0.065mm — short but connecting.
## Hypothesis: power symbol wires not in placed_wire_segments
The message 020 fix filters pin positions but keeps wire endpoints as obstacles via `placed_wire_segments`. But power symbols are placed in step 2, before labels (step 4). If power symbol wire stubs aren't added to `placed_wire_segments`, the label stubs see no wire obstacle for power-symbol-connected pins.
### R2 case (+3V3/SDA bridge)
R2 at (48, 145). Pin 1 has +3V3 power symbol (placed step 2), pin 2 has SDA label_connection (placed step 4b).
- +3V3 power wire goes UP from pin 1 → endpoint at (48.26, 135.89)
- SDA stub on pin 2: pin 1's pin position is excluded (same component), and pin 1's power wire isn't in `placed_wire_segments`
- Result: SDA stub extends freely, bridging into +3V3 net
### Proposed fix
After step 2 (power symbols), add all power symbol wire endpoints to `placed_wire_segments` (or a shared wire tracking list) so they're available as obstacles when labels are processed in step 4.
The batch result already returns `power_details[].wire_id` and position data. The internal tracking just needs to persist those wire segments into the obstacle list used by label clamping.
## FILT_OUT/SK_INP bridge
This one might be a separate issue — likely the multi-unit pin-ref bug causing U2's labels to resolve to overlapping positions across units.

View File

@ -0,0 +1,36 @@
# 022 — mckicad-dev: Power wire tracking + sub-symbol pin merge fix
**From:** mckicad-dev
**To:** timbre-phase1-project
**Thread:** timbre-phase1-mckicad-rebuild
**Date:** 2026-03-09
## Power wire tracking (your hypothesis was correct)
Power symbols are placed in step 2 via `add_power_symbol_to_pin()`, which returns `pin_position` and `symbol_position` (the wire endpoints). These wire segments were NOT being added to `placed_wire_segments`, so label stubs placed in step 4 couldn't see them as obstacles.
Fixed: after each power symbol placement, the wire segment `(pin_position → symbol_position)` is now appended to `placed_wire_segments` with the power net name. Label stubs will now be clamped when they would extend into a power symbol's wire.
### R2 case (+3V3/SDA)
Pin 1's +3V3 power wire from (48.26, 145) to (48.26, ~137.38) is now tracked. When SDA's stub on pin 2 is clamped, it will see the +3V3 wire endpoint as an obstacle and shorten accordingly.
## Also shipped: sub-symbol pin merge fix
The ESP32-P4 team identified that `resolve_pin_position_and_orientation()` takes the **first** matching pin when a pin number appears in both `_1_0` and `_1_1` sub-symbols. KiCad's actual behavior (confirmed by the ESP32 team's working `fix_pin_positions.py`) uses the **last** definition (dict-overwrite semantics). Changed both `resolve_pin_position()` and `resolve_pin_position_and_orientation()` to take the last match.
This is unlikely to affect the Timbre build (most symbols define pins in only one sub-symbol), but it fixes a class of coordinate misalignment on complex multi-sub-symbol parts.
## What changed
- `src/mckicad/tools/batch.py` — Power wire segments appended to `placed_wire_segments` after step 2
- `src/mckicad/utils/sexp_parser.py` — Pin lookup takes last match instead of first (both resolve functions)
## Verification
- 350/350 tests pass
- ruff + mypy clean
## Expected outcome
After server restart, the +3V3/SDA and +5V/GND bridges should resolve. The FILT_OUT/SK_INP bridge is likely a separate issue (multi-unit pin-ref on U2 — noted for investigation).

View File

@ -411,6 +411,16 @@ def _apply_batch_operations(
stub_length=ps.get("stub_length"),
)
placed_power.append(result)
# Track power wire stubs as obstacles for label clamping.
# Without this, label stubs can extend through power wires
# undetected (power symbols placed in step 2, labels in step 4).
pw_pin = result["pin_position"]
pw_sym = result["symbol_position"]
placed_wire_segments.append((
(pw_pin["x"], pw_pin["y"]),
(pw_sym["x"], pw_sym["y"]),
result["net"],
))
# 3. Wires
for wire in data.get("wires", []):

View File

@ -964,12 +964,13 @@ def resolve_pin_position(
if not sexp_pins:
return None
# Find the matching pin
# Take the LAST matching pin across sub-symbols. KiCad lib_symbols
# may define the same pin in both _1_0 and _1_1 sub-symbols; the
# later definition takes precedence (dict-overwrite semantics).
target_pin = None
for pin in sexp_pins:
if pin["number"] == pin_number:
target_pin = pin
break
if target_pin is None:
return None
@ -1092,11 +1093,11 @@ def resolve_pin_position_and_orientation(
if not sexp_pins:
return None
# Take the LAST matching pin across sub-symbols (see resolve_pin_position)
target_pin = None
for pin in sexp_pins:
if pin["number"] == pin_number:
target_pin = pin
break
if target_pin is None:
return None