feat: add wire/junction tools with pin-snapping and T-junction support

- Rename add_schematic_connection → add_schematic_wire with waypoints[] parameter
- Add snapToPins (default true) to snap wire endpoints to nearest pin
- Expose add_schematic_junction as an MCP tool
- Break existing wires at new wire endpoints for T-junction support
- Remove orphaned add_connection / add_wire / get_pin_location from ConnectionManager
- Update tool registry to reflect renamed schematic tools in TS layer
- Add 76 tests for wire/junction handler dispatch, schema validation, and WireManager corner cases
- Apply Black and Prettier formatting to changed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-24 21:11:47 +00:00
parent 1647c282f3
commit 3bd3c3a962
7 changed files with 1584 additions and 561 deletions

View File

@@ -376,8 +376,8 @@ class KiCADInterface:
"edit_schematic_component": self._handle_edit_schematic_component,
"get_schematic_component": self._handle_get_schematic_component,
"add_schematic_wire": self._handle_add_schematic_wire,
"add_schematic_connection": self._handle_add_schematic_connection,
"add_schematic_net_label": self._handle_add_schematic_net_label,
"add_schematic_junction": self._handle_add_schematic_junction,
"connect_to_net": self._handle_connect_to_net,
"connect_passthrough": self._handle_connect_passthrough,
"get_schematic_pin_locations": self._handle_get_schematic_pin_locations,
@@ -1137,40 +1137,117 @@ class KiCADInterface:
return {"success": False, "message": str(e)}
def _handle_add_schematic_wire(self, params):
"""Add a wire to a schematic using WireManager"""
"""Add a wire to a schematic using WireManager, with optional pin snapping"""
logger.info("Adding wire to schematic")
try:
from pathlib import Path
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
start_point = params.get("startPoint")
end_point = params.get("endPoint")
points = params.get("waypoints")
properties = params.get("properties", {})
snap_to_pins = params.get("snapToPins", True)
snap_tolerance = params.get("snapTolerance", 1.0)
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not start_point or not end_point:
if not points or len(points) < 2:
return {
"success": False,
"message": "Start and end points are required",
"message": "At least 2 waypoints are required",
}
# Make a mutable copy of points
points = [list(p) for p in points]
# Pin snapping: adjust first and last endpoints to nearest pin
snapped_info = []
if snap_to_pins:
from commands.pin_locator import PinLocator
locator = PinLocator()
sch_path = Path(schematic_path)
# Load schematic to iterate all symbols
from skip import Schematic as SkipSchematic
sch = SkipSchematic(str(sch_path))
# Collect all pin locations: list of (ref, pin_num, [x, y])
all_pins = []
for symbol in sch.symbol:
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
pin_locs = locator.get_all_symbol_pins(sch_path, ref)
for pin_num, coords in pin_locs.items():
all_pins.append((ref, pin_num, coords))
def find_nearest_pin(point, tolerance):
"""Find the nearest pin within tolerance of a point."""
best = None
best_dist = tolerance
for ref, pin_num, coords in all_pins:
dx = point[0] - coords[0]
dy = point[1] - coords[1]
dist = (dx * dx + dy * dy) ** 0.5
if dist <= best_dist:
best_dist = dist
best = (ref, pin_num, coords)
return best
# Snap first endpoint
match = find_nearest_pin(points[0], snap_tolerance)
if match:
ref, pin_num, coords = match
logger.info(
f"Snapped start point {points[0]} -> {coords} (pin {ref}/{pin_num})"
)
snapped_info.append(
f"start snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
)
points[0] = list(coords)
# Snap last endpoint
match = find_nearest_pin(points[-1], snap_tolerance)
if match:
ref, pin_num, coords = match
logger.info(
f"Snapped end point {points[-1]} -> {coords} (pin {ref}/{pin_num})"
)
snapped_info.append(
f"end snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
)
points[-1] = list(coords)
# Extract wire properties
stroke_width = properties.get("stroke_width", 0)
stroke_type = properties.get("stroke_type", "default")
# Use WireManager for S-expression manipulation
success = WireManager.add_wire(
Path(schematic_path),
start_point,
end_point,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
if len(points) == 2:
success = WireManager.add_wire(
Path(schematic_path),
points[0],
points[1],
stroke_width=stroke_width,
stroke_type=stroke_type,
)
else:
success = WireManager.add_polyline_wire(
Path(schematic_path),
points,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
if success:
return {"success": True, "message": "Wire added successfully"}
message = "Wire added successfully"
if snapped_info:
message += "; " + "; ".join(snapped_info)
return {"success": True, "message": message}
else:
return {"success": False, "message": "Failed to add wire"}
except Exception as e:
@@ -1184,6 +1261,38 @@ class KiCADInterface:
"errorDetails": traceback.format_exc(),
}
def _handle_add_schematic_junction(self, params):
"""Add a junction (connection dot) to a schematic using WireManager"""
logger.info("Adding junction to schematic")
try:
from pathlib import Path
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
position = params.get("position")
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not position:
return {"success": False, "message": "Position is required"}
success = WireManager.add_junction(Path(schematic_path), position)
if success:
return {"success": True, "message": "Junction added successfully"}
else:
return {"success": False, "message": "Failed to add junction"}
except Exception as e:
logger.error(f"Error adding junction to schematic: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"message": str(e),
"errorDetails": traceback.format_exc(),
}
def _handle_list_schematic_libraries(self, params):
"""List available symbol libraries"""
logger.info("Listing schematic libraries")
@@ -1393,54 +1502,6 @@ class KiCADInterface:
logger.error(f"Error exporting schematic to PDF: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_add_schematic_connection(self, params):
"""Add a pin-to-pin connection in schematic with automatic pin discovery and routing"""
logger.info("Adding pin-to-pin connection in schematic")
try:
from pathlib import Path
schematic_path = params.get("schematicPath")
source_ref = params.get("sourceRef")
source_pin = params.get("sourcePin")
target_ref = params.get("targetRef")
target_pin = params.get("targetPin")
routing = params.get(
"routing", "direct"
) # 'direct', 'orthogonal_h', 'orthogonal_v'
if not all(
[schematic_path, source_ref, source_pin, target_ref, target_pin]
):
return {"success": False, "message": "Missing required parameters"}
# Use ConnectionManager with new PinLocator and WireManager integration
success = ConnectionManager.add_connection(
Path(schematic_path),
source_ref,
source_pin,
target_ref,
target_pin,
routing=routing,
)
if success:
return {
"success": True,
"message": f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})",
}
else:
return {"success": False, "message": "Failed to add connection"}
except Exception as e:
logger.error(f"Error adding schematic connection: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"message": str(e),
"errorDetails": traceback.format_exc(),
}
def _handle_add_schematic_net_label(self, params):
"""Add a net label to schematic using WireManager"""
logger.info("Adding net label to schematic")