refactor: consolidate get_pin_net into get_wire_connections

get_pin_net was a superset of get_wire_connections with the same
coordinate-based flood-fill but two extra response fields (net, query_point)
and a reference+pin input mode. Having both tools confused LLM tool selection.

get_wire_connections now:
- Returns net (label name or null) and query_point in all response paths
- Accepts reference+pin input in addition to x/y coordinates,
  resolving the pin endpoint via PinLocator internally

get_pin_net tool, handler, schema, TS registration, and tests removed.
test_wire_connectivity.py updated with coverage for all new behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-04-12 17:02:35 +01:00
parent f73d8d9795
commit 4f733bb3db
6 changed files with 259 additions and 569 deletions

View File

@@ -220,7 +220,7 @@ def _find_pins_on_net(
def get_wire_connections(
schematic: Any, schematic_path: str, x_mm: float, y_mm: float
) -> Optional[Dict]:
"""Find all component pins reachable from a point via connected wires, net labels, and power symbols.
"""Find the net name and all component pins reachable from a point via connected wires.
The query point (x_mm, y_mm) must be exactly on a wire endpoint or junction (exact IU match).
Interior (mid-segment) points are not matched —
@@ -229,67 +229,20 @@ def get_wire_connections(
Net labels and power symbols are traversed: wires on the same named net are
treated as connected even when they are not geometrically adjacent.
Returns dict with keys:
- "pins": list of {"component": str, "pin": str}
- "wires": list of {"start": {"x", "y"}, "end": {"x", "y"}} in mm
Or None if no wire endpoint found within tolerance of the query point.
"""
all_wires = _parse_wires(schematic)
if not all_wires:
return {"pins": [], "wires": []}
adjacency, iu_to_wires = _build_adjacency(all_wires)
point_to_label, label_to_points = _parse_virtual_connections(schematic, schematic_path)
visited, net_points = _find_connected_wires(
x_mm,
y_mm,
all_wires,
iu_to_wires,
adjacency,
point_to_label=point_to_label,
label_to_points=label_to_points,
)
if visited is None:
return None
wires_out = [
{
"start": {
"x": all_wires[i][0][0] / _IU_PER_MM,
"y": all_wires[i][0][1] / _IU_PER_MM,
},
"end": {
"x": all_wires[i][-1][0] / _IU_PER_MM,
"y": all_wires[i][-1][1] / _IU_PER_MM,
},
}
for i in visited
]
if not hasattr(schematic, "symbol"):
return {"pins": [], "wires": wires_out}
pins = _find_pins_on_net(net_points, schematic_path, schematic)
return {"pins": pins, "wires": wires_out}
def get_pin_net(schematic: Any, schematic_path: str, x_mm: float, y_mm: float) -> Optional[Dict]:
"""Return the net name and all connected pins for the wire network at (x_mm, y_mm).
Returns dict with keys:
- "net": str or None (net label/power name, None if unnamed)
- "pins": list of {"component": str, "pin": str}
- "wires": list of {"start": {"x", "y"}, "end": {"x", "y"}} in mm
- "query_point": {"x": float, "y": float}
Or None if no wire endpoint found at the query point.
Or None if no wire endpoint found within tolerance of the query point.
"""
all_wires = _parse_wires(schematic)
query_point = {"x": x_mm, "y": y_mm}
if not all_wires:
return {"net": None, "pins": [], "wires": [], "query_point": {"x": x_mm, "y": y_mm}}
return {"net": None, "pins": [], "wires": [], "query_point": query_point}
adjacency, iu_to_wires = _build_adjacency(all_wires)
point_to_label, label_to_points = _parse_virtual_connections(schematic, schematic_path)
visited, net_points = _find_connected_wires(
@@ -326,11 +279,11 @@ def get_pin_net(schematic: Any, schematic_path: str, x_mm: float, y_mm: float) -
for i in visited
]
pins: List[Dict] = []
if hasattr(schematic, "symbol"):
pins = _find_pins_on_net(net_points, schematic_path, schematic)
if not hasattr(schematic, "symbol"):
return {"net": net, "pins": [], "wires": wires_out, "query_point": query_point}
return {"net": net, "pins": pins, "wires": wires_out, "query_point": {"x": x_mm, "y": y_mm}}
pins = _find_pins_on_net(net_points, schematic_path, schematic)
return {"net": net, "pins": pins, "wires": wires_out, "query_point": query_point}
def count_pins_on_net(

View File

@@ -383,7 +383,6 @@ class KiCADInterface:
"get_net_connections": self._handle_get_net_connections,
"get_wire_connections": self._handle_get_wire_connections,
"get_net_at_point": self._handle_get_net_at_point,
"get_pin_net": self._handle_get_pin_net,
"run_erc": self._handle_run_erc,
"generate_netlist": self._handle_generate_netlist,
"sync_schematic_to_board": self._handle_sync_schematic_to_board,
@@ -2515,29 +2514,57 @@ class KiCADInterface:
return {"success": False, "message": str(e)}
def _handle_get_wire_connections(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Find all component pins reachable from a point via connected wires"""
"""Find net name and all component pins reachable from a point or component pin."""
logger.info("Getting wire connections")
try:
from pathlib import Path
from commands.pin_locator import PinLocator
from commands.wire_connectivity import get_wire_connections
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "Missing required parameter: schematicPath"}
reference = params.get("reference")
pin = params.get("pin")
x = params.get("x")
y = params.get("y")
if not (schematic_path and x is not None and y is not None):
has_ref_pin = reference is not None and pin is not None
has_coords = x is not None and y is not None
if has_ref_pin and has_coords:
return {
"success": False,
"message": "Missing required parameters: schematicPath, x, y",
"message": "Supply either {reference, pin} or {x, y}, not both",
}
try:
x, y = float(x), float(y)
except (TypeError, ValueError):
if not has_ref_pin and not has_coords:
if reference is not None or pin is not None:
return {
"success": False,
"message": "Both reference and pin are required together",
}
return {
"success": False,
"message": "Parameters x and y must be numeric",
"message": "Must supply either {reference, pin} or {x, y}",
}
if has_ref_pin:
location = PinLocator().get_pin_location(Path(schematic_path), reference, str(pin))
if location is None:
return {
"success": False,
"message": f"Pin {pin} not found on {reference}",
}
x, y = location[0], location[1]
else:
try:
x, y = float(x), float(y)
except (TypeError, ValueError):
return {"success": False, "message": "Parameters x and y must be numeric"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
@@ -2549,7 +2576,7 @@ class KiCADInterface:
if result is None:
return {
"success": False,
"message": f"No wire found at ({x},{y}) within tolerance",
"message": f"No wire found at ({x},{y}) — point may not be connected",
}
return {"success": True, **result}
@@ -2595,70 +2622,6 @@ class KiCADInterface:
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_get_pin_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
"""Find net name and all connected pins for a reference+pin or coordinate query"""
logger.info("Getting pin net")
try:
from pathlib import Path
from commands.pin_locator import PinLocator
from commands.wire_connectivity import get_pin_net
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "Missing required parameter: schematicPath"}
reference = params.get("reference")
pin = params.get("pin")
x = params.get("x")
y = params.get("y")
has_ref_pin = reference is not None and pin is not None
has_coords = x is not None and y is not None
if not has_ref_pin and not has_coords:
return {
"success": False,
"message": "Must supply either {reference, pin} or {x, y}",
}
if has_ref_pin:
location = PinLocator().get_pin_location(Path(schematic_path), reference, str(pin))
if location is None:
return {
"success": False,
"message": f"Pin {pin} not found on {reference}",
}
x, y = location[0], location[1]
else:
try:
x, y = float(x), float(y)
except (TypeError, ValueError):
return {"success": False, "message": "Parameters x and y must be numeric"}
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
if not hasattr(schematic, "wire"):
return {"success": False, "message": "Schematic has no wires"}
result = get_pin_net(schematic, schematic_path, x, y)
if result is None:
return {
"success": False,
"message": f"No wire found at ({x},{y}) — pin may not be connected",
}
return {"success": True, **result}
except Exception as e:
logger.error(f"Error getting pin net: {str(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")

View File

@@ -1503,24 +1503,41 @@ SCHEMATIC_TOOLS = [
{
"name": "get_wire_connections",
"title": "Get Wire Connections",
"description": "Returns all wires and component pins connected to the wire at a given point, by flood-filling through touching wires.",
"description": (
"Returns the net name and all wires and component pins connected at a given point. "
"Accepts either a component reference + pin number (e.g. reference='U1', pin='3') "
"or a schematic coordinate (x, y in mm). "
"The response includes: 'net' (label name or null for unnamed nets), "
"'pins' (all component pins on the net), 'wires' (all wire segments on the net), "
"and 'query_point' (the resolved coordinate used). "
"The query point must be at a wire endpoint or junction — wire midpoints are not matched. "
"Use get_schematic_pin_locations or list_schematic_wires to obtain exact endpoint coordinates."
),
"inputSchema": {
"type": "object",
"properties": {
"schematicPath": {
"type": "string",
"description": "Path to schematic file",
"description": "Path to the schematic file (.kicad_sch)",
},
"reference": {
"type": "string",
"description": "Component reference (e.g. U1, R1). Pair with pin.",
},
"pin": {
"type": "string",
"description": "Pin number or name (e.g. '3', 'SDA'). Pair with reference.",
},
"x": {
"type": "number",
"description": "X coordinate of the point on the wire",
"description": "X coordinate of a wire endpoint in mm. Pair with y.",
},
"y": {
"type": "number",
"description": "Y coordinate of the point on the wire",
"description": "Y coordinate of a wire endpoint in mm. Pair with x.",
},
},
"required": ["schematicPath", "x", "y"],
"required": ["schematicPath"],
},
},
{
@@ -1552,44 +1569,6 @@ SCHEMATIC_TOOLS = [
"required": ["schematicPath", "x", "y"],
},
},
{
"name": "get_pin_net",
"title": "Get Pin Net",
"description": (
"Returns the net name and all connected component pins for a query. "
"Accepts either a component reference + pin number (e.g. reference='U1', pin='3') "
"or a schematic coordinate (x, y in mm). "
"If a net label or power symbol is reachable from the query point, its name is "
"returned as 'net'; otherwise 'net' is null (unnamed net). "
"The 'pins' list contains every component pin on that same net."
),
"inputSchema": {
"type": "object",
"properties": {
"schematicPath": {
"type": "string",
"description": "Path to the schematic file (.kicad_sch)",
},
"reference": {
"type": "string",
"description": "Component reference (e.g. U1, R1). Pair with pin.",
},
"pin": {
"type": "string",
"description": "Pin number or name (e.g. '3', 'SDA'). Pair with reference.",
},
"x": {
"type": "number",
"description": "X coordinate of a wire endpoint in mm. Pair with y.",
},
"y": {
"type": "number",
"description": "Y coordinate of a wire endpoint in mm. Pair with x.",
},
},
"required": ["schematicPath"],
},
},
{
"name": "get_schematic_pin_locations",
"title": "Get Schematic Pin Locations",