refactor: remove find_unconnected_pins tool (redundant with run_erc)

KiCad's native ERC already checks for unconnected pins with better
accuracy (hierarchical sheets, bus connections, custom rules). Remove
the reimplemented version and its dead helper _parse_no_connects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-15 14:15:46 +00:00
parent dc3dc06af1
commit 1ef4ce5cab
5 changed files with 0 additions and 227 deletions

View File

@@ -135,20 +135,6 @@ def _parse_symbols(sexp_data: list) -> List[Dict[str, Any]]:
return symbols
def _parse_no_connects(sexp_data: list) -> Set[Tuple[float, float]]:
"""Parse all no_connect elements and return their positions as (x, y) tuples in mm."""
positions: Set[Tuple[float, float]] = set()
for item in sexp_data:
if not isinstance(item, list) or len(item) < 2:
continue
if item[0] != Symbol("no_connect"):
continue
for sub in item:
if isinstance(sub, list) and len(sub) >= 3 and sub[0] == Symbol("at"):
positions.add((float(sub[1]), float(sub[2])))
break
return positions
def _parse_lib_symbol_graphics(symbol_def: list) -> List[Tuple[float, float]]:
"""
@@ -434,107 +420,6 @@ def _compute_symbol_bbox_direct(
return (min_x, min_y, max_x, max_y)
# ---------------------------------------------------------------------------
# Tool 2: find_unconnected_pins
# ---------------------------------------------------------------------------
def find_unconnected_pins(schematic_path: Path) -> List[Dict[str, Any]]:
"""
Find all component pins with no wire, label, or power symbol touching them.
Returns list of dicts: {reference, libId, pinNumber, pinName, position: {x, y}}
"""
sexp_data = _load_sexp(schematic_path)
symbols = _parse_symbols(sexp_data)
wires = _parse_wires(sexp_data)
labels = _parse_labels(sexp_data)
no_connects = _parse_no_connects(sexp_data)
# Build set of "connected" positions in mm
connected: Set[Tuple[float, float]] = set()
# Wire endpoints
for w in wires:
connected.add(w["start"])
connected.add(w["end"])
# Label positions
for lbl in labels:
connected.add((lbl["x"], lbl["y"]))
# Power symbol positions (they implicitly connect)
for sym in symbols:
if sym["is_power"]:
connected.add((sym["x"], sym["y"]))
tolerance = 0.05 # mm
def _snap(v: float) -> int:
"""Snap coordinate to grid for O(1) set lookup."""
return round(v / tolerance)
connected_grid: set = set()
for pos in connected:
connected_grid.add((_snap(pos[0]), _snap(pos[1])))
no_connect_grid: set = set()
for pos in no_connects:
no_connect_grid.add((_snap(pos[0]), _snap(pos[1])))
def is_connected(px: float, py: float) -> bool:
sx, sy = _snap(px), _snap(py)
# Check the snapped cell and immediate neighbors to handle edge cases
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
if (sx + dx, sy + dy) in connected_grid:
return True
return False
def is_no_connect(px: float, py: float) -> bool:
sx, sy = _snap(px), _snap(py)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
if (sx + dx, sy + dy) in no_connect_grid:
return True
return False
lib_defs = _extract_lib_symbols(sexp_data)
unconnected = []
for sym in symbols:
ref = sym["reference"]
# Skip power symbols, templates, and empty references
if sym["is_power"] or ref.startswith("_TEMPLATE") or not ref:
continue
lib_data = lib_defs.get(sym["lib_id"], {})
pin_defs = lib_data.get("pins", {})
if not pin_defs:
continue
pin_positions = _compute_pin_positions_direct(sym, pin_defs)
if not pin_positions:
continue
for pin_num, pos in pin_positions.items():
px, py = pos[0], pos[1]
if is_no_connect(px, py):
continue
if is_connected(px, py):
continue
pin_name = pin_defs.get(pin_num, {}).get("name", pin_num)
unconnected.append({
"reference": ref,
"libId": sym["lib_id"],
"pinNumber": pin_num,
"pinName": pin_name,
"position": {"x": round(px, 4), "y": round(py, 4)},
})
return unconnected
# ---------------------------------------------------------------------------
# Tool 3: find_overlapping_elements

View File

@@ -400,7 +400,6 @@ class KiCADInterface:
"export_schematic_svg": self._handle_export_schematic_svg,
# Schematic analysis tools (read-only)
"get_schematic_view_region": self._handle_get_schematic_view_region,
"find_unconnected_pins": self._handle_find_unconnected_pins,
"find_overlapping_elements": self._handle_find_overlapping_elements,
"get_elements_in_region": self._handle_get_elements_in_region,
"find_wires_crossing_symbols": self._handle_find_wires_crossing_symbols,
@@ -2660,29 +2659,6 @@ class KiCADInterface:
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_find_unconnected_pins(self, params):
"""Find all component pins with no wire, label, or power symbol touching them"""
logger.info("Finding unconnected pins in schematic")
try:
from pathlib import Path
from commands.schematic_analysis import find_unconnected_pins
schematic_path = params.get("schematicPath")
if not schematic_path:
return {"success": False, "message": "schematicPath is required"}
result = find_unconnected_pins(Path(schematic_path))
return {
"success": True,
"unconnectedPins": result,
"count": len(result),
"message": f"Found {len(result)} unconnected pin(s)",
}
except Exception as e:
logger.error(f"Error finding unconnected pins: {e}")
import traceback
logger.error(traceback.format_exc())
return {"success": False, "message": str(e)}
def _handle_find_overlapping_elements(self, params):
"""Detect spatially overlapping symbols, wires, and labels"""

View File

@@ -1726,21 +1726,6 @@ SCHEMATIC_TOOLS = [
"required": ["schematicPath", "x1", "y1", "x2", "y2"]
}
},
{
"name": "find_unconnected_pins",
"title": "Find Unconnected Pins",
"description": "Lists all component pins in the schematic that have no wire, label, or power symbol touching them. Useful for checking connectivity before running ERC. Skips power symbols, template symbols, and pins with no_connect flags.",
"inputSchema": {
"type": "object",
"properties": {
"schematicPath": {
"type": "string",
"description": "Path to the .kicad_sch schematic file"
}
},
"required": ["schematicPath"]
}
},
{
"name": "find_overlapping_elements",
"title": "Find Overlapping Elements",

View File

@@ -23,7 +23,6 @@ from commands.schematic_analysis import (
_parse_wires,
_parse_labels,
_parse_symbols,
_parse_no_connects,
_load_sexp,
_extract_lib_symbols,
_parse_lib_symbol_graphics,
@@ -35,7 +34,6 @@ from commands.schematic_analysis import (
_check_wire_overlap,
_compute_symbol_bbox_direct,
compute_symbol_bbox,
find_unconnected_pins,
find_overlapping_elements,
get_elements_in_region,
find_wires_crossing_symbols,
@@ -220,16 +218,6 @@ class TestSexpParsers:
assert symbols[1]["reference"] == "#PWR01"
assert symbols[1]["is_power"] is True
def test_parse_no_connects(self):
sexp = sexpdata.loads("""(kicad_sch
(no_connect (at 10 20) (uuid "x"))
(no_connect (at 30 40) (uuid "y"))
)""")
nc = _parse_no_connects(sexp)
assert (10.0, 20.0) in nc
assert (30.0, 40.0) in nc
assert len(nc) == 2
# ===================================================================
# Unit tests — analysis functions with mocked PinLocator
@@ -376,44 +364,6 @@ class TestComputeSymbolBbox:
# Integration tests — full schematic parsing
# ===================================================================
@pytest.mark.integration
class TestIntegrationFindUnconnectedPins:
"""Integration test using real schematic files."""
def test_component_with_no_wires_has_unconnected_pins(self):
"""A resistor placed with no wires should have 2 unconnected pins."""
extra = _make_resistor_sexp("R1", 100, 100)
tmp = _make_temp_schematic(extra)
result = find_unconnected_pins(tmp)
r1_pins = [p for p in result if p["reference"] == "R1"]
assert len(r1_pins) == 2
def test_pin_with_wire_is_connected(self):
"""A wire endpoint exactly at a pin position should mark it connected."""
# R1 at (100,100), rotation 0 → pin 1 at (100, 103.81), pin 2 at (100, 96.19)
extra = _make_resistor_sexp("R1", 100, 100) + """
(wire (pts (xy 100 103.81) (xy 100 120))
(stroke (width 0) (type default))
(uuid "w1"))
"""
tmp = _make_temp_schematic(extra)
result = find_unconnected_pins(tmp)
r1_pins = [p for p in result if p["reference"] == "R1"]
# Pin 1 should be connected (wire at 100, 103.81), pin 2 still unconnected
assert len(r1_pins) == 1
assert r1_pins[0]["pinNumber"] == "2"
def test_no_connect_suppresses_pin(self):
"""A no_connect at a pin position should not report it as unconnected."""
extra = _make_resistor_sexp("R1", 100, 100) + """
(no_connect (at 100 96.19) (uuid "nc1"))
(no_connect (at 100 103.81) (uuid "nc2"))
"""
tmp = _make_temp_schematic(extra)
result = find_unconnected_pins(tmp)
r1_pins = [p for p in result if p["reference"] == "R1"]
assert len(r1_pins) == 0
@pytest.mark.integration
class TestIntegrationFindWiresCrossingSymbols:

View File

@@ -1135,29 +1135,6 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
},
);
// Find unconnected pins
server.tool(
"find_unconnected_pins",
"List all component pins in the schematic that have no wire, label, or power symbol touching them. Useful for checking connectivity before running ERC.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("find_unconnected_pins", args);
if (result.success) {
const pins: any[] = result.unconnectedPins || [];
const lines = [`Found ${pins.length} unconnected pin(s):`];
pins.slice(0, 50).forEach((p: any) => {
lines.push(` ${p.reference} pin ${p.pinNumber} (${p.pinName}) @ (${p.position.x}, ${p.position.y})`);
});
if (pins.length > 50) lines.push(` ... and ${pins.length - 50} more`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }],
};
},
);
// Find overlapping elements
server.tool(