diff --git a/python/commands/schematic_analysis.py b/python/commands/schematic_analysis.py index b56f427..7eab32b 100644 --- a/python/commands/schematic_analysis.py +++ b/python/commands/schematic_analysis.py @@ -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 diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 47d5f60..2c5ef16 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -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""" diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index 98804b4..6eb3c4c 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -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", diff --git a/python/tests/test_schematic_analysis.py b/python/tests/test_schematic_analysis.py index 4f31446..b287f07 100644 --- a/python/tests/test_schematic_analysis.py +++ b/python/tests/test_schematic_analysis.py @@ -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: diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 262da24..ef8fb67 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -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(