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:
@@ -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)}")
|
||||
|
||||
Reference in New Issue
Block a user