feat: add hierarchy tools (hierarchical label + sheet pin)

Two new tools for managing hierarchical schematic connections:

- add_schematic_hierarchical_label: create sheet interface ports on
  sub-sheet schematics. These are the sub-sheet side of hierarchical
  connections, linking to sheet pins on the parent.

- add_sheet_pin: add pins to sheet symbol blocks on the parent
  schematic. Targets the correct sheet by matching the Sheetname
  property. The pinName must match a hierarchical_label in the
  sub-sheet.

Both tools use text-based S-expression insertion (not sexpdata
round-trip) to preserve KiCad's native file formatting. Labels
include proper justification based on orientation: left-justify for
rightward labels (0°), right-justify for leftward labels (180°).

Wire manager additions:
- _find_insertion_point(): locates sheet_instances block or final paren
- _text_insert(): inserts formatted S-expression text at the right position
- _make_hierarchical_label_text(): generates hierarchical_label S-expression
- _make_sheet_pin_text(): generates sheet pin S-expression
- WireManager.add_hierarchical_label(): static method for label insertion
- WireManager.add_sheet_pin(): static method for pin insertion into
  named sheet blocks

12 unit tests covering insertion, orientation/justification mapping,
parameter validation, multi-sheet targeting, and error handling.
This commit is contained in:
Leah Armstrong
2026-04-15 12:32:09 -04:00
parent eea43d18f1
commit 101b4e1dad
4 changed files with 749 additions and 0 deletions

View File

@@ -408,6 +408,8 @@ class KiCADInterface:
"find_orphaned_wires": self._handle_find_orphaned_wires,
"list_floating_labels": self._handle_list_floating_labels,
"snap_to_grid": self._handle_snap_to_grid,
"add_schematic_hierarchical_label": self._handle_add_schematic_hierarchical_label,
"add_sheet_pin": self._handle_add_sheet_pin,
"import_svg_logo": self._handle_import_svg_logo,
# UI/Process management commands
"check_kicad_ui": self._handle_check_kicad_ui,
@@ -2622,6 +2624,126 @@ class KiCADInterface:
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_add_schematic_hierarchical_label(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a hierarchical label to a sub-sheet schematic."""
logger.info("Adding hierarchical label to schematic")
try:
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
text = params.get("text")
position = params.get("position")
shape = params.get("shape", "bidirectional")
orientation = params.get("orientation", 0)
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
if not text:
return {"success": False, "message": "text is required"}
if not position or len(position) != 2:
return {"success": False, "message": "position [x, y] is required"}
if shape not in ("input", "output", "bidirectional"):
return {
"success": False,
"message": "shape must be input, output, or bidirectional",
}
sch_file = Path(schematic_path)
if not sch_file.exists():
return {
"success": False,
"message": f"Schematic not found: {schematic_path}",
}
success = WireManager.add_hierarchical_label(
sch_file, text, position, shape=shape, orientation=orientation
)
if success:
return {
"success": True,
"message": (
f"Added hierarchical_label '{text}' " f"at {position} shape={shape}"
),
}
return {"success": False, "message": "Failed to add hierarchical label"}
except Exception as e:
logger.error(f"Error adding hierarchical label: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_add_sheet_pin(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Add a sheet pin to a sheet block on the parent schematic."""
logger.info("Adding sheet pin to schematic")
try:
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
sheet_name = params.get("sheetName")
pin_name = params.get("pinName")
pin_type = params.get("pinType", "bidirectional")
position = params.get("position")
orientation = params.get("orientation", 0)
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
if not sheet_name:
return {"success": False, "message": "sheetName is required"}
if not pin_name:
return {"success": False, "message": "pinName is required"}
if not position or len(position) != 2:
return {"success": False, "message": "position [x, y] is required"}
if pin_type not in ("input", "output", "bidirectional"):
return {
"success": False,
"message": "pinType must be input, output, or bidirectional",
}
sch_file = Path(schematic_path)
if not sch_file.exists():
return {
"success": False,
"message": f"Schematic not found: {schematic_path}",
}
with open(sch_file, "r", encoding="utf-8") as f:
content = f.read()
modified, success = WireManager.add_sheet_pin(
content,
sheet_name,
pin_name,
pin_type,
position,
orientation=orientation,
)
if not success:
return {
"success": False,
"message": f"Sheet '{sheet_name}' not found in {schematic_path}",
}
with open(sch_file, "w", encoding="utf-8") as f:
f.write(modified)
return {
"success": True,
"message": (
f"Added sheet pin '{pin_name}' ({pin_type}) " f"to sheet '{sheet_name}'"
),
}
except Exception as e:
logger.error(f"Error adding sheet pin: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_run_erc(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Run Electrical Rules Check on a schematic via kicad-cli"""
logger.info("Running ERC on schematic")