feat: multi-sheet net connectivity + sexp-based parsing reliability

Adds robust multi-sheet (hierarchical) net connectivity for KiCad
schematics and switches the wire/label parsing to a direct sexpdata
pipeline that bypasses kicad-skip's collection iteration, which was
silently dropping wires, labels, and symbol instances on real-world
schematics.

python/commands/wire_connectivity.py
  - New sexpdata helpers: _load_sexp, _parse_wires_sexp,
    _parse_labels_sexp, _parse_symbol_instances_sexp,
    _parse_hierarchical_labels_sexp, _discover_sub_sheets.
  - _build_adjacency now detects T-junctions (endpoint landing on
    another wire's interior segment) so adjacency captures connections
    KiCad doesn't represent as separate wire segments.
  - _find_connected_wires gains an interior-segment fallback so labels
    placed mid-wire still seed BFS correctly.
  - _parse_virtual_connections gathers label / global_label /
    hierarchical_label and power-symbol pin positions, with a
    kicad-skip fallback for unit tests that mock the schematic.
  - _find_pins_on_net rebuilds pin positions from sexpdata symbol
    instances (with mirror_x/mirror_y/rotation handling) and uses a
    plus/minus 1 IU tolerance for floating-point edge cases.
  - get_connections_for_net walks the top sheet plus every recursively
    discovered sub-sheet, deduping pins across sheets.

python/commands/pin_locator.py
  - lib_id matching now falls back to a bare-name + unit-suffix match
    so instances like "stat-tis-custom:BAT_18650" resolve to
    lib_symbols entries like "BAT_18650_3".
  - Pin position math now y-negates lib_symbols coords, applies
    mirror_x/mirror_y in local coords before rotation, and propagates
    the same transform into get_pin_orientation so downstream callers
    get a correct outward angle for mirrored symbols.

python/commands/connection_schematic.py
  - generate_netlist now collects nets from both label and
    global_label and routes them through get_connections_for_net so
    netlists reflect cross-sheet connectivity instead of single-sheet
    label-only nets.

python/kicad_interface.py
  - list_schematic_nets aggregates net names across the top sheet and
    all sub-sheets via the sexp helpers, then resolves connections
    using get_connections_for_net.
  - get_net_connections delegates to get_connections_for_net for
    consistent multi-sheet results.
This commit is contained in:
William Viana
2026-04-19 23:42:15 -07:00
parent 5b7434fdef
commit 046f33d876
4 changed files with 599 additions and 82 deletions

View File

@@ -1963,13 +1963,15 @@ class KiCADInterface:
"""List all nets in a schematic with their connections"""
logger.info("Listing schematic nets")
try:
from pathlib import Path
from commands.wire_connectivity import (
_build_adjacency,
_discover_sub_sheets,
_load_sexp,
_parse_labels_sexp,
_parse_virtual_connections,
_parse_wires,
count_pins_on_net,
get_connections_for_net,
)
schematic_path = params.get("schematicPath")
@@ -1980,16 +1982,39 @@ class KiCADInterface:
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Get all net names from labels and global labels
net_names = set()
if hasattr(schematic, "label"):
for label in schematic.label:
if hasattr(label, "value"):
net_names.add(label.value)
if hasattr(schematic, "global_label"):
for label in schematic.global_label:
if hasattr(label, "value"):
net_names.add(label.value)
# Collect net names from the top-level sheet using sexpdata.
# Falls back to kicad-skip's label collections when the file
# cannot be read (e.g. mocked schematics in unit tests).
net_names: set = set()
sexp_loaded = False
try:
sexp = _load_sexp(schematic_path)
sexp_loaded = True
_, label_to_points = _parse_labels_sexp(sexp)
net_names.update(label_to_points.keys())
except Exception as e:
logger.debug(
f"Could not parse labels from {schematic_path} via sexp ({e}); "
"falling back to kicad-skip label collections"
)
for attr in ("label", "global_label"):
if not hasattr(schematic, attr):
continue
for label in getattr(schematic, attr):
if hasattr(label, "value"):
net_names.add(label.value)
# Collect net names from all sub-sheets (only when the parent
# sheet was readable; fake/mock paths skip recursion entirely).
if sexp_loaded:
sub_sheets = _discover_sub_sheets(schematic_path)
for sub_path in sub_sheets:
try:
sub_sexp = _load_sexp(sub_path)
_, sub_label_to_points = _parse_labels_sexp(sub_sexp)
net_names.update(sub_label_to_points.keys())
except Exception as e:
logger.warning(f"Error reading sub-sheet {sub_path}: {e}")
# Pre-build shared wire graph structures for efficiency
all_wires = _parse_wires(schematic)
@@ -2001,9 +2026,7 @@ class KiCADInterface:
nets = []
for net_name in sorted(net_names):
connections = ConnectionManager.get_net_connections(
schematic, net_name, Path(schematic_path)
)
connections = get_connections_for_net(schematic, schematic_path, net_name)
pin_count = count_pins_on_net(
schematic,
schematic_path,
@@ -2608,6 +2631,8 @@ class KiCADInterface:
"""Get all connections for a named net"""
logger.info("Getting net connections")
try:
from commands.wire_connectivity import get_connections_for_net
schematic_path = params.get("schematicPath")
net_name = params.get("netName")
@@ -2618,7 +2643,7 @@ class KiCADInterface:
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
connections = ConnectionManager.get_net_connections(schematic, net_name)
connections = get_connections_for_net(schematic, schematic_path, net_name)
return {"success": True, "connections": connections}
except Exception as e:
logger.error(f"Error getting net connections: {str(e)}")