Skip to content

feat(schematic): join nearby pins under one power symbol - #857

Draft
pauliuszaleckas wants to merge 2 commits into
mixelpixx:mainfrom
pauliuszaleckas:feat/856-batch-power-symbol
Draft

pauliuszaleckas wants to merge 2 commits into
mixelpixx:mainfrom
pauliuszaleckas:feat/856-batch-power-symbol

Conversation

@pauliuszaleckas

@pauliuszaleckas pauliuszaleckas commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Draft, stacked on #782. The branch carries #782's commit f6cbf63 under its own 381a9a7, so the diff against main shows both. Only 381a9a7 belongs to this PR. It is opened early for visibility, which departs from docs/BRANCH_AND_PULL_REQUEST_WORKFLOW.md. Once #782 merges I will rebuild the branch as one commit on current main and mark it ready.

Konnect places one power symbol per pin. Pins that sit close together each get their own symbol: an MCU's VSS and VSSA, or a row of decoupling caps. Their arrows overlap and the values run together (GNDGND). Stacked pins get a duplicate symbol hidden exactly behind the first.

  • New tool batch_add_power_symbol (in sch_batch). It ties a list of pins to one rail the way a sheet is drawn by hand. Pins that face the same way, end on one row and lie within max_gap of a neighbour get a stub each, a bar across the stubs, and one power symbol on the bar. Every other pin gets a symbol on its endpoint.
  • add_power_symbol on a pin no longer adds a second symbol when the pin is already on power_net. It writes nothing and reports already_connected.

Closes #856

Approach

  • Resolve. Every pin goes through feat(schematic): place add_power_symbol on a named pin #782's resolve_power_pin, so a missing or ambiguous pin refuses the whole call before anything is written. Pins sharing an endpoint, such as the VSS pins of an STM32, fold into one point.
  • Already on the rail. ExistingPower asks the net graph whether the pin's net is a power_net symbol's net. It compares root_at identities, not names. A symbol reached through a wire therefore counts, as well as one on the pin itself. When there are several, the nearest is reported, because every same-named symbol on a sheet is one net.
  • Group. Points are grouped by outward direction and depth (the coordinate along that direction). Each group is sorted across the row and split wherever the gap exceeds max_gap.
  • Lay out. Each stub runs stub_length outward from its pin, the bar joins the stub ends, and the symbol sits on the middle stub's end. Junction dots go where a stub meets the bar mid-span, and where a new power pin lands mid-wire.
  • Guard. Before a group is joined, its stubs and bar are checked against every pin endpoint, label, junction, no-connect, sheet pin, wire and bus on the sheet, and against groups placed earlier in the call. Anything touched other than the group's own pins means the group is not joined. Its pins get their own symbols and not_joined_reason names the obstacle. Without this check a bar could short an unrelated net.
  • Write. One Schematic::overwrite. The readback reloads the file and checks every symbol through placed_component_readback, every wire by UUID and coordinates, and every junction by position. A mismatch is reported as mutation_outcome_uncertain.

Deviations from the issue:

  • Toolset. The tool is in sch_batch, not sch_wiring, because sch_wiring is at the 20-tool cap (no_toolset_exceeds_max_size).
  • Already-connected scope. The issue proposed checking for a symbol on the endpoint. This checks net identity instead, which also covers a pin wired to the rail. Measured without it: add_power_symbol on U1 pin 35, after a batch had wired the VSS pins to a GND symbol, still added a duplicate.

Architectural fit

This reuses resolve_power_pin and power_symbol_pin from #782, NetGraph (sch_connectivity.rs) for net identity, and find_t_junctions/pins_mid_segment for dots. The symbol-building code from handle_add_power_symbol moved unchanged into add_power_symbol_to, so both tools build symbols the same way. sch_batch::error_text became pub(crate).

Out of scope:

  • Avoiding symbol bodies. The guard checks connection points and wires, not body outlines, so a part crowded right against the pins can still be crossed.
  • Routing around obstacles.
  • batch_connect_to_net, which still places one label per pin.

Branch and dependencies

Compatibility and safety

  • New tool, minor release. Tool counts are updated: 229 registered, 236 total.
  • add_power_symbol with reference + pin_number now returns {already_connected: true, power_net, existing_reference, x, y} without writing when the pin is already on the rail. The x + y form is unchanged, so a caller who wants the duplicate can still place it that way.
  • docs/API_MIGRATIONS.md gets a new entry, placed above feat(schematic): place add_power_symbol on a named pin #782's. It only adds lines.
  • Ownership is checked before pins. An unowned sheet is a conflict whatever pins were named, as for the other placers. symbol_loading_and_erc_preflight_ownership_without_writing now covers this tool.

Validation

Changed tool behavior

Behavior Contract and evidence for this change
Accepted inputs and declared defaults power_net, pins: [{reference, pin_number}] (minItems 1), max_gap ≥ 0 (default 10.16 mm), stub_length > 0 and a multiple of 1.27 (default 2.54 mm). max_gap_decides_who_joins covers 7.6, 7.62 (inclusive) and 20.32.
Invalid/unsupported inputs and structured errors A missing pin or component is stale_target and an ambiguous one is ambiguous_target, from #782's resolver. A bad stub_length or empty pins is invalid_argument. Any refusal writes nothing, and refusals_write_nothing asserts the file is byte-identical.
Target, data source and prerequisite state The saved .kicad_sch is read once (read_consistent), and ownership is checked through sheet_instance_context and validate_sheet_instance_state before any pin is resolved. The power symbol must exist in the sheet or KiCad's power library.
Observed changes and preserved unrelated objects The tool adds stubs, bars, junctions and power:<net> symbols, and touches nothing else. The collision guard keeps every new wire off existing items: BLOCK stays dangling in KiCad's ERC, so it was not joined to GND.
Failure before/after mutation, including applied work Everything is planned in memory and written once. A conflict at write time leaves the file unchanged. A readback mismatch after the write is mutation_outcome_uncertain.
Recovery from partial/uncertain results without repeating applied work Re-running the same call is safe. Pins already on the rail are listed under already_connected and skipped, so no symbols are duplicated; a_stacked_pin_already_on_the_rail_gets_no_second_symbol checks this.

Fixture and oracle

crates/konnect-core/tests/fixtures/power_rows_kicad10.kicad_sch was built through Konnect against stock KiCad libraries and force-resaved with kicad-cli sch upgrade --force on KiCad 10.0.6. Its README has the provenance, the case table, and KiCad's ERC pin positions, which the tests use as literals.

The tests' two calls were run on a copy, and the result was checked with KiCad:

kicad-cli sch export netlist   →  GND:  C1.2 C2.2 C3.2 C4.2 C5.2 C6.2 C7.2 C8.2 R1.1 R1.2 U1.23 U1.35 U1.47 U1.8
                                   +3V3: C1.1 C2.1 C3.1 C4.1 U1.1 U1.24 U1.36 U1.48 U1.9
kicad-cli sch erc --severity-all → none of those pins unconnected; label 'BLOCK' still label_dangling

Cases the oracle can only corroborate one way:

  • Stacked VSS pins: KiCad reports them as one point, and they get one symbol.
  • C7.2 and C8.2 sit on one row 7.62 mm apart but face opposite ways, so they stay apart.
  • The C5/C6 bar would cross BLOCK. Drawn, it would put BLOCK on GND.

Negative controls

Run with cargo test -q --workspace --locked --lib --tests --no-fail-fast, with each guard removed in turn on 381a9a7:

Guard removed Failing tests
Collision check (blocking returns None) 2: a_bar_that_would_touch_a_label_is_not_drawn, a_row_shares_one_symbol_…
Already-on-rail check (ExistingPower::at returns None) 2: a_stacked_pin_already_on_the_rail_…, a_pin_wired_to_the_rail_…
max_gap split (never split a row) 2: a_row_shares_one_symbol_…, max_gap_decides_who_joins
Direction match (group by depth only) 1: a_row_shares_one_symbol_… (C7.2/C8.2)
Symbol on the middle stub (use the first) 3: a_row_shares_one_symbol_…, an_ic_supply_row_…, max_gap_decides_who_joins
Stacked-pin fold 1: a_row_shares_one_symbol_…
Junction dots not added 4: a_row_shares_one_symbol_…, an_ic_supply_row_…, max_gap_decides_who_joins, a_pin_wired_to_the_rail_…
Pins resolved before ownership 1: symbol_loading_and_erc_preflight_ownership_without_writing

The ownership test names a pin that does not exist (C201 pin 99), so only the ownership-first order reports conflict.

Gate on 381a9a7

  • cargo fmt --all -- --check: clean
  • cargo clippy --workspace --locked --all-targets -- -D warnings: clean
  • cargo test -p konnect-core --locked reliability_contract: 2 passed
  • cargo test --workspace --locked --lib --tests: 2383 passed, 0 failed, 45 ignored
  • cargo test --workspace --locked --doc: 9 passed, 2 ignored
  • cargo xtask fix-doc-counts --check: current (21 toolsets, 229 registered, 236 total)
  • Schematic viewer: not touched, not run

CI has not run yet. The branch inherits #782's docs/API_MIGRATIONS.md conflict with current main (git merge-tree shows only that file), and GitHub starts no pull_request workflows on a conflicting PR. The checks will run on the rebuilt branch after #782 merges.

Review checklist

  • The diff is focused and contains no generated output, personal data, or unrelated cleanup.
  • The branch includes current upstream/main, has no merge conflicts, and CI passed on this exact head. It does not: it is stacked on feat(schematic): place add_power_symbol on a named pin #782 and will be rebuilt after feat(schematic): place add_power_symbol on a named pin #782 merges.
  • For a fork PR, maintainer edits are enabled, or the author accepts responsibility for the final current-main refresh.
  • The branch was based on latest upstream/main, not a release tag (unless this is an approved backport).
  • The PR shows only its unique commits and diff; dependencies and series position are explicit. Dependencies are explicit, but the diff includes feat(schematic): place add_power_symbol on a named pin #782 until it merges.
  • Every review conversation is resolved; any post-review push or base refresh has been reviewed on the new exact head.
  • New names follow docs/NAMING_CONVENTIONS.md; public renames include compatibility handling.
  • New behavior and failure paths have regression coverage.
  • File mutations are atomic and preserve unrelated content.
  • IPC mutations verify the requested board; N/A, no IPC.
  • If tools were added/removed: counts and docs updated per CONTRIBUTING.md (registry tool_count, tool-directory.md, DEV.md stats, README count).

🤖 Generated with Claude Code

pauliuszaleckas and others added 2 commits October 4, 2026 19:43
Copying a pin's endpoint into x/y and picking a rotation by hand is where
mistakes slip in (mixelpixx#727). add_power_symbol now also takes reference +
pin_number: the symbol lands on the pin's endpoint, unsnapped, and faces
away from the body unless rotation is given. Mixed or half-given selectors,
missing pins and pins two symbols share are refused before writing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
batch_add_power_symbol ties a list of pins to one rail the way a sheet is
drawn by hand: pins facing the same way on one row, within max_gap of a
neighbour, get a stub each, a bar and a single symbol. A group whose wires
would touch anything else is left apart and says why.

add_power_symbol on a pin no longer adds a second symbol when the pin is
already on that rail, by net identity, so stacked pins and pins wired to a
symbol are covered (mixelpixx#856).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Join nearby pins on one rail and place a single power symbol (batch_add_power_symbol)

1 participant