From 1d390f4fed6436b56ee28f045b0a72eb1a86ee4d Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 6 Mar 2026 11:29:52 +0100 Subject: [PATCH] fix: schematic pin connection reliability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add_schematic_net_label: warn in description that coords must be exact pin endpoints; recommend connect_to_net instead - connect_to_net: stub wire direction now follows pin angle (was hardcoded +X) - pin_locator.py: add get_pin_angle() and _get_lib_id() helpers - new tool: get_schematic_pin_locations(schematicPath, reference) → returns exact x/y of every pin endpoint, so Claude can place labels correctly --- python/commands/connection_schematic.py | 11 ++++- python/commands/pin_locator.py | 57 +++++++++++++++++++++++++ python/kicad_interface.py | 42 ++++++++++++++++++ python/schemas/tool_schemas.py | 21 ++++++++- src/tools/schematic.ts | 32 ++++++++++++++ 5 files changed, 161 insertions(+), 2 deletions(-) diff --git a/python/commands/connection_schematic.py b/python/commands/connection_schematic.py index 286b67f..d18e298 100644 --- a/python/commands/connection_schematic.py +++ b/python/commands/connection_schematic.py @@ -256,7 +256,16 @@ class ConnectionManager: return False # Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing) - stub_end = [pin_loc[0] + 2.54, pin_loc[1]] + # Stub direction follows the pin's outward angle from the PinLocator + pin_angle_deg = getattr(locator, '_last_pin_angle', 0) + try: + pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0 + except Exception: + pin_angle_deg = 0 + import math as _math + angle_rad = _math.radians(pin_angle_deg) + stub_end = [round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4), + round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4)] # Create wire stub using WireManager wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end) diff --git a/python/commands/pin_locator.py b/python/commands/pin_locator.py index bf4367d..94a5b35 100644 --- a/python/commands/pin_locator.py +++ b/python/commands/pin_locator.py @@ -179,6 +179,63 @@ class PinLocator: return (rotated_x, rotated_y) + def _get_lib_id(self, schematic_path: Path, symbol_reference: str) -> Optional[str]: + """Helper: return the lib_id string for a placed symbol""" + try: + sch_key = str(schematic_path) + if sch_key not in self._schematic_cache: + self._schematic_cache[sch_key] = Schematic(sch_key) + sch = self._schematic_cache[sch_key] + for symbol in sch.symbol: + if symbol.property.Reference.value == symbol_reference: + return symbol.lib_id.value if hasattr(symbol, "lib_id") else None + except Exception: + pass + return None + + def get_pin_angle( + self, schematic_path: Path, symbol_reference: str, pin_number: str + ) -> Optional[float]: + """ + Get the outward angle of a pin endpoint in degrees (0=right, 90=up, 180=left, 270=down). + This is the direction a wire stub must extend to stay connected to the pin. + + Returns angle in degrees, or None if pin not found. + """ + try: + sch_key = str(schematic_path) + if sch_key not in self._schematic_cache: + self._schematic_cache[sch_key] = Schematic(sch_key) + sch = self._schematic_cache[sch_key] + + target_symbol = None + for symbol in sch.symbol: + if symbol.property.Reference.value == symbol_reference: + target_symbol = symbol + break + + if not target_symbol: + return None + + symbol_at = target_symbol.at.value + symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0 + + lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None + if not lib_id: + return None + + pins = self.get_symbol_pins(schematic_path, lib_id) + if pin_number not in pins: + return None + + # Pin definition angle + symbol rotation = absolute outward direction + pin_def_angle = pins[pin_number].get("angle", 0) + absolute_angle = (pin_def_angle + symbol_rotation) % 360 + return absolute_angle + + except Exception: + return None + def get_pin_location( self, schematic_path: Path, symbol_reference: str, pin_number: str ) -> Optional[List[float]]: diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 891a6d1..c6b4355 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -375,6 +375,7 @@ class KiCADInterface: "add_schematic_connection": self._handle_add_schematic_connection, "add_schematic_net_label": self._handle_add_schematic_net_label, "connect_to_net": self._handle_connect_to_net, + "get_schematic_pin_locations": self._handle_get_schematic_pin_locations, "get_net_connections": self._handle_get_net_connections, "generate_netlist": self._handle_generate_netlist, "list_schematic_libraries": self._handle_list_schematic_libraries, @@ -1271,6 +1272,47 @@ class KiCADInterface: "errorDetails": traceback.format_exc(), } + def _handle_get_schematic_pin_locations(self, params): + """Return exact pin endpoint coordinates for a schematic component""" + logger.info("Getting schematic pin locations") + try: + from pathlib import Path + from commands.pin_locator import PinLocator + + schematic_path = params.get("schematicPath") + reference = params.get("reference") + + if not all([schematic_path, reference]): + return {"success": False, "message": "Missing required parameters: schematicPath, reference"} + + locator = PinLocator() + all_pins = locator.get_all_symbol_pins(Path(schematic_path), reference) + + if not all_pins: + return {"success": False, "message": f"No pins found for {reference} — check reference and schematic path"} + + # Enrich with pin names and angles from the symbol definition + pins_def = locator.get_symbol_pins( + Path(schematic_path), + locator._get_lib_id(Path(schematic_path), reference), + ) if hasattr(locator, "_get_lib_id") else {} + + result = {} + for pin_num, coords in all_pins.items(): + entry = {"x": coords[0], "y": coords[1]} + if pin_num in pins_def: + entry["name"] = pins_def[pin_num].get("name", pin_num) + entry["angle"] = locator.get_pin_angle(Path(schematic_path), reference, pin_num) or 0 + result[pin_num] = entry + + return {"success": True, "reference": reference, "pins": result} + + except Exception as e: + logger.error(f"Error getting pin locations: {e}") + import traceback + logger.error(traceback.format_exc()) + return {"success": False, "message": str(e)} + def _handle_get_net_connections(self, params): """Get all connections for a named net""" logger.info("Getting net connections") diff --git a/python/schemas/tool_schemas.py b/python/schemas/tool_schemas.py index fb02be9..223b8fb 100644 --- a/python/schemas/tool_schemas.py +++ b/python/schemas/tool_schemas.py @@ -1388,7 +1388,7 @@ SCHEMATIC_TOOLS = [ { "name": "add_schematic_net_label", "title": "Add Net Label", - "description": "Adds a net label to assign a name to a wire/net on the schematic.", + "description": "Adds a net label at exact coordinates on a schematic wire or pin endpoint. WARNING: x/y must match an existing wire endpoint or pin endpoint exactly — placing the label even 0.01mm away from a pin will result in an unconnected pin ERC error. To connect a component pin to a net by reference and pin number (recommended), use connect_to_net instead.", "inputSchema": { "type": "object", "properties": { @@ -1463,6 +1463,25 @@ SCHEMATIC_TOOLS = [ "required": ["schematicPath", "netName"] } }, + { + "name": "get_schematic_pin_locations", + "title": "Get Schematic Pin Locations", + "description": "Returns the exact absolute coordinates of all pins on a schematic component. Use this BEFORE placing net labels with add_schematic_net_label to get the correct x/y position for each pin endpoint.", + "inputSchema": { + "type": "object", + "properties": { + "schematicPath": { + "type": "string", + "description": "Path to the schematic file" + }, + "reference": { + "type": "string", + "description": "Component reference designator (e.g., U1, R1, J2)" + } + }, + "required": ["schematicPath", "reference"] + } + }, { "name": "generate_netlist", "title": "Generate Netlist", diff --git a/src/tools/schematic.ts b/src/tools/schematic.ts index 4db17fc..9407b93 100644 --- a/src/tools/schematic.ts +++ b/src/tools/schematic.ts @@ -385,6 +385,38 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp }, ); + // Get pin locations for a schematic component + server.tool( + "get_schematic_pin_locations", + "Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.", + { + schematicPath: z.string().describe("Path to the schematic file"), + reference: z.string().describe("Component reference designator (e.g. U1, R1, J2)"), + }, + async (args: { schematicPath: string; reference: string }) => { + const result = await callKicadScript("get_schematic_pin_locations", args); + if (result.success && result.pins) { + const lines = Object.entries(result.pins as Record).map( + ([pinNum, data]: [string, any]) => + ` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°` + ); + return { + content: [{ + type: "text", + text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`, + }], + }; + } else { + return { + content: [{ + type: "text", + text: `Failed to get pin locations: ${result.message || "Unknown error"}`, + }], + }; + } + }, + ); + // Generate netlist server.tool( "generate_netlist",