From 5c6b55453e6547ddd0b87408f3611ccf87a0cc89 Mon Sep 17 00:00:00 2001 From: karu2003 Date: Mon, 18 May 2026 20:40:09 +0200 Subject: [PATCH] feat(mcp): add route_arc_trace for true PCB arc primitives (SWIG + IPC) (#165) --- python/commands/routing.py | 75 +++++++++++++++++++++++++++++++++ python/kicad_api/ipc_backend.py | 65 ++++++++++++++++++++++++++++ python/kicad_interface.py | 58 +++++++++++++++++++++++++ src/tools/routing.ts | 43 +++++++++++++++++++ 4 files changed, 241 insertions(+) diff --git a/python/commands/routing.py b/python/commands/routing.py index a0054c8..7a251da 100644 --- a/python/commands/routing.py +++ b/python/commands/routing.py @@ -351,6 +351,81 @@ class RoutingCommands: "errorDetails": str(e), } + def route_arc_trace(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Route a copper arc trace from start/mid/end points.""" + try: + if not self.board: + return { + "success": False, + "message": "No board is loaded", + "errorDetails": "Load or create a board first", + } + + start = params.get("start") + mid = params.get("mid") + end = params.get("end") + layer = params.get("layer", "F.Cu") + width = params.get("width") + net = params.get("net") + + if not start or not mid or not end: + return { + "success": False, + "message": "Missing parameters", + "errorDetails": "start, mid and end points are required", + } + + layer_id = self.board.GetLayerID(layer) + if layer_id < 0: + return { + "success": False, + "message": "Invalid layer", + "errorDetails": f"Layer '{layer}' does not exist", + } + + start_point = self._get_point(start) + mid_point = self._get_point(mid) + end_point = self._get_point(end) + + arc = pcbnew.PCB_ARC(self.board) + arc.SetStart(start_point) + arc.SetMid(mid_point) + arc.SetEnd(end_point) + arc.SetLayer(layer_id) + + if width: + arc.SetWidth(int(width * 1000000)) + else: + arc.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth()) + + if net: + netinfo = self.board.GetNetInfo() + nets_map = netinfo.NetsByName() + if nets_map.has_key(net): + arc.SetNet(nets_map[net]) + + self.board.Add(arc) + + return { + "success": True, + "message": "Added arc trace", + "arc": { + "start": {"x": start_point.x / 1000000, "y": start_point.y / 1000000, "unit": "mm"}, + "mid": {"x": mid_point.x / 1000000, "y": mid_point.y / 1000000, "unit": "mm"}, + "end": {"x": end_point.x / 1000000, "y": end_point.y / 1000000, "unit": "mm"}, + "layer": layer, + "width": arc.GetWidth() / 1000000, + "net": net, + }, + } + except Exception as e: + logger.error(f"Error routing arc trace: {str(e)}") + return { + "success": False, + "message": "Failed to route arc trace", + "errorDetails": str(e), + } + def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]: """Add a via at the specified location""" try: diff --git a/python/kicad_api/ipc_backend.py b/python/kicad_api/ipc_backend.py index e2bc956..211807f 100644 --- a/python/kicad_api/ipc_backend.py +++ b/python/kicad_api/ipc_backend.py @@ -861,6 +861,71 @@ class IPCBoardAPI(BoardAPI): logger.error(f"Failed to add track: {e}") return False + def add_arc_track( + self, + start_x: float, + start_y: float, + mid_x: float, + mid_y: float, + end_x: float, + end_y: float, + width: float = 0.25, + layer: str = "F.Cu", + net_name: Optional[str] = None, + ) -> bool: + """Add a copper arc track to the board.""" + try: + from kipy.board_types import ArcTrack + from kipy.geometry import Vector2 + from kipy.proto.board.board_types_pb2 import BoardLayer + from kipy.util.units import from_mm + + board = self._get_board() + + arc = ArcTrack() + arc.start = Vector2.from_xy(from_mm(start_x), from_mm(start_y)) + arc.mid = Vector2.from_xy(from_mm(mid_x), from_mm(mid_y)) + arc.end = Vector2.from_xy(from_mm(end_x), from_mm(end_y)) + arc.width = from_mm(width) + + layer_map = { + "F.Cu": BoardLayer.BL_F_Cu, + "B.Cu": BoardLayer.BL_B_Cu, + "In1.Cu": BoardLayer.BL_In1_Cu, + "In2.Cu": BoardLayer.BL_In2_Cu, + } + arc.layer = layer_map.get(layer, BoardLayer.BL_F_Cu) + + if net_name: + nets = board.get_nets() + for net in nets: + if net.name == net_name: + arc.net = net + break + + commit = board.begin_commit() + board.create_items(arc) + board.push_commit(commit, "Added arc track") + + self._notify( + "arc_track_added", + { + "start": {"x": start_x, "y": start_y}, + "mid": {"x": mid_x, "y": mid_y}, + "end": {"x": end_x, "y": end_y}, + "width": width, + "layer": layer, + "net": net_name, + }, + ) + logger.info( + f"Added arc track start=({start_x}, {start_y}) mid=({mid_x}, {mid_y}) end=({end_x}, {end_y}) mm" + ) + return True + except Exception as e: + logger.error(f"Failed to add arc track: {e}") + return False + def add_via( self, x: float, diff --git a/python/kicad_interface.py b/python/kicad_interface.py index bbe421c..d26fbdb 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -364,6 +364,7 @@ class KiCADInterface: # Routing commands "add_net": self.routing_commands.add_net, "route_trace": self.routing_commands.route_trace, + "route_arc_trace": self.routing_commands.route_arc_trace, "add_via": self.routing_commands.add_via, "delete_trace": self.routing_commands.delete_trace, "query_traces": self.routing_commands.query_traces, @@ -488,6 +489,7 @@ class KiCADInterface: IPC_CAPABLE_COMMANDS = { # Routing commands "route_trace": "_ipc_route_trace", + "route_arc_trace": "_ipc_route_arc_trace", "add_via": "_ipc_add_via", "add_net": "_ipc_add_net", "delete_trace": "_ipc_delete_trace", @@ -606,6 +608,7 @@ class KiCADInterface: "rotate_component", "delete_component", "route_trace", + "route_arc_trace", "route_pad_to_pad", "add_via", "delete_trace", @@ -5006,6 +5009,61 @@ print("ok") logger.error(f"IPC route_trace error: {e}") return {"success": False, "message": str(e)} + def _ipc_route_arc_trace(self, params: Dict[str, Any]) -> Dict[str, Any]: + """IPC handler for route_arc_trace - adds copper arc with real-time UI update""" + try: + start = params.get("start", {}) + mid = params.get("mid", {}) + end = params.get("end", {}) + layer = params.get("layer", "F.Cu") + width = params.get("width", 0.25) + net = params.get("net") + + start_x = start.get("x", 0) + start_y = start.get("y", 0) + mid_x = mid.get("x", 0) + mid_y = mid.get("y", 0) + end_x = end.get("x", 0) + end_y = end.get("y", 0) + + if not hasattr(self.ipc_board_api, "add_arc_track"): + return { + "success": False, + "message": "IPC backend does not support arc track on this installation", + } + + success = self.ipc_board_api.add_arc_track( + start_x=start_x, + start_y=start_y, + mid_x=mid_x, + mid_y=mid_y, + end_x=end_x, + end_y=end_y, + width=width, + layer=layer, + net_name=net, + ) + + return { + "success": success, + "message": ( + "Added arc trace (visible in KiCAD UI)" + if success + else "Failed to add arc trace" + ), + "arc": { + "start": {"x": start_x, "y": start_y, "unit": "mm"}, + "mid": {"x": mid_x, "y": mid_y, "unit": "mm"}, + "end": {"x": end_x, "y": end_y, "unit": "mm"}, + "layer": layer, + "width": width, + "net": net, + }, + } + except Exception as e: + logger.error(f"IPC route_arc_trace error: {e}") + return {"success": False, "message": str(e)} + def _ipc_add_via(self, params: Dict[str, Any]) -> Dict[str, Any]: """IPC handler for add_via - adds via with real-time UI update""" try: diff --git a/src/tools/routing.ts b/src/tools/routing.ts index 3ead958..96a88e8 100644 --- a/src/tools/routing.ts +++ b/src/tools/routing.ts @@ -63,6 +63,49 @@ export function registerRoutingTools(server: McpServer, callKicadScript: Functio }, ); + // Route arc trace tool + server.tool( + "route_arc_trace", + "Route a copper arc trace defined by start/mid/end points. Uses true PCB arc primitives when available.", + { + start: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("Arc start position"), + mid: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("A point on arc midpoint"), + end: z + .object({ + x: z.number(), + y: z.number(), + unit: z.string().optional(), + }) + .describe("Arc end position"), + layer: z.string().describe("PCB layer"), + width: z.number().describe("Trace width in mm"), + net: z.string().optional().describe("Net name"), + }, + async (args: any) => { + const result = await callKicadScript("route_arc_trace", args); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + ); + // Add via tool server.tool( "add_via",