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:
@@ -126,16 +126,42 @@ class PinLocator:
|
||||
logger.error("No lib_symbols section found in schematic")
|
||||
return {}
|
||||
|
||||
# Find the specific symbol definition
|
||||
for item in lib_symbols[1:]: # Skip 'lib_symbols' itself
|
||||
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol"):
|
||||
symbol_name = str(item[1]).strip('"')
|
||||
if symbol_name == lib_id:
|
||||
# Found the symbol, parse pins
|
||||
pins = self.parse_symbol_definition(item)
|
||||
self.pin_definition_cache[cache_key] = pins
|
||||
logger.info(f"Extracted {len(pins)} pins from {lib_id}")
|
||||
return pins
|
||||
# Find the specific symbol definition.
|
||||
# KiCad lib_symbols may use a different name than the instance lib_id:
|
||||
# instance lib_id: "stat-tis-custom:BAT_18650"
|
||||
# lib_symbols name: "BAT_18650_3" (prefix stripped, unit suffix added)
|
||||
# Strategy: exact match first, then bare-name prefix match.
|
||||
bare_name = lib_id.split(":")[-1] if ":" in lib_id else lib_id
|
||||
|
||||
best_match = None
|
||||
for item in lib_symbols[1:]:
|
||||
if not (isinstance(item, list) and len(item) > 1 and item[0] == Symbol("symbol")):
|
||||
continue
|
||||
symbol_name = str(item[1]).strip('"')
|
||||
if symbol_name == lib_id:
|
||||
best_match = item
|
||||
break
|
||||
if best_match is None:
|
||||
sn_bare = symbol_name.split(":")[-1] if ":" in symbol_name else symbol_name
|
||||
if sn_bare == bare_name or (
|
||||
sn_bare.startswith(bare_name)
|
||||
and len(sn_bare) > len(bare_name)
|
||||
and sn_bare[len(bare_name)] == "_"
|
||||
and sn_bare[len(bare_name) + 1 :].isdigit()
|
||||
):
|
||||
best_match = item
|
||||
|
||||
if best_match is not None:
|
||||
matched_name = str(best_match[1]).strip('"')
|
||||
pins = self.parse_symbol_definition(best_match)
|
||||
self.pin_definition_cache[cache_key] = pins
|
||||
if matched_name != lib_id:
|
||||
logger.info(
|
||||
f"Matched {lib_id} → lib_symbols '{matched_name}' ({len(pins)} pins)"
|
||||
)
|
||||
else:
|
||||
logger.info(f"Extracted {len(pins)} pins from {lib_id}")
|
||||
return pins
|
||||
|
||||
logger.warning(f"Symbol {lib_id} not found in lib_symbols")
|
||||
return {}
|
||||
@@ -228,8 +254,26 @@ class PinLocator:
|
||||
else:
|
||||
return None
|
||||
|
||||
# Pin definition angle + symbol rotation = absolute outward direction
|
||||
mirror_x = False
|
||||
mirror_y = False
|
||||
if hasattr(target_symbol, "mirror"):
|
||||
mirror_val = (
|
||||
str(target_symbol.mirror.value)
|
||||
if hasattr(target_symbol.mirror, "value")
|
||||
else ""
|
||||
)
|
||||
if mirror_val == "x":
|
||||
mirror_x = True
|
||||
elif mirror_val == "y":
|
||||
mirror_y = True
|
||||
|
||||
pin_def_angle = pins[pin_number].get("angle", 0)
|
||||
# Y-negate flips the angle across the x-axis
|
||||
pin_def_angle = (360 - pin_def_angle) % 360
|
||||
if mirror_x:
|
||||
pin_def_angle = (360 - pin_def_angle) % 360
|
||||
if mirror_y:
|
||||
pin_def_angle = (180 - pin_def_angle) % 360
|
||||
absolute_angle = (pin_def_angle + symbol_rotation) % 360
|
||||
return absolute_angle
|
||||
|
||||
@@ -270,12 +314,25 @@ class PinLocator:
|
||||
logger.error(f"Symbol {symbol_reference} not found in schematic")
|
||||
return None
|
||||
|
||||
# Get symbol position and rotation
|
||||
# Get symbol position, rotation, and mirror state
|
||||
symbol_at = target_symbol.at.value
|
||||
symbol_x = float(symbol_at[0])
|
||||
symbol_y = float(symbol_at[1])
|
||||
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
||||
|
||||
mirror_x = False
|
||||
mirror_y = False
|
||||
if hasattr(target_symbol, "mirror"):
|
||||
mirror_val = (
|
||||
str(target_symbol.mirror.value)
|
||||
if hasattr(target_symbol.mirror, "value")
|
||||
else ""
|
||||
)
|
||||
if mirror_val == "x":
|
||||
mirror_x = True
|
||||
elif mirror_val == "y":
|
||||
mirror_y = True
|
||||
|
||||
# Get symbol lib_id
|
||||
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
|
||||
if not lib_id:
|
||||
@@ -283,7 +340,8 @@ class PinLocator:
|
||||
return None
|
||||
|
||||
logger.debug(
|
||||
f"Symbol {symbol_reference}: pos=({symbol_x}, {symbol_y}), rot={symbol_rotation}, lib_id={lib_id}"
|
||||
f"Symbol {symbol_reference}: pos=({symbol_x}, {symbol_y}), rot={symbol_rotation}, "
|
||||
f"mirror_x={mirror_x}, mirror_y={mirror_y}, lib_id={lib_id}"
|
||||
)
|
||||
|
||||
# Get pin definitions for this symbol
|
||||
@@ -319,10 +377,21 @@ class PinLocator:
|
||||
|
||||
logger.debug(f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})")
|
||||
|
||||
# lib_symbols uses y-up; schematic uses y-down
|
||||
pin_rel_y = -pin_rel_y
|
||||
|
||||
# Mirror in local coords after y-negate (KiCad transform order)
|
||||
# mirror_x = flip across X axis → negate y
|
||||
# mirror_y = flip across Y axis → negate x
|
||||
if mirror_x:
|
||||
pin_rel_y = -pin_rel_y
|
||||
if mirror_y:
|
||||
pin_rel_x = -pin_rel_x
|
||||
|
||||
# Apply symbol rotation to pin position
|
||||
if symbol_rotation != 0:
|
||||
pin_rel_x, pin_rel_y = self.rotate_point(pin_rel_x, pin_rel_y, symbol_rotation)
|
||||
logger.debug(f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})")
|
||||
logger.debug(f"After transform (y-neg/mirror/rot): ({pin_rel_x}, {pin_rel_y})")
|
||||
|
||||
# Calculate absolute position
|
||||
abs_x = symbol_x + pin_rel_x
|
||||
|
||||
Reference in New Issue
Block a user