diff --git a/python/commands/board/__init__.py b/python/commands/board/__init__.py index 374d0b3..08b974e 100644 --- a/python/commands/board/__init__.py +++ b/python/commands/board/__init__.py @@ -75,3 +75,8 @@ class BoardCommands: """Get a 2D image of the PCB""" self.view_commands.board = self.board return self.view_commands.get_board_2d_view(params) + + def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Get the bounding box extents of the board""" + self.view_commands.board = self.board + return self.view_commands.get_board_extents(params) diff --git a/python/commands/board/size.py b/python/commands/board/size.py index d8ba8a6..a4f9e2e 100644 --- a/python/commands/board/size.py +++ b/python/commands/board/size.py @@ -16,7 +16,7 @@ class BoardSizeCommands: self.board = board def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]: - """Set the size of the PCB board""" + """Set the size of the PCB board by creating edge cuts outline""" try: if not self.board: return { @@ -36,35 +36,33 @@ class BoardSizeCommands: "errorDetails": "Both width and height are required" } - # Convert to internal units (nanometers) - scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm - width_nm = int(width * scale) - height_nm = int(height * scale) + # Create board outline using BoardOutlineCommands + # This properly creates edge cuts on Edge.Cuts layer + from commands.board.outline import BoardOutlineCommands + outline_commands = BoardOutlineCommands(self.board) - # Set board size using KiCAD 9.0 API - # Note: In KiCAD 9.0, SetSize takes two separate parameters instead of VECTOR2I - board_box = self.board.GetBoardEdgesBoundingBox() - try: - # Try KiCAD 9.0+ API (two parameters) - board_box.SetSize(width_nm, height_nm) - except TypeError: - # Fall back to older API (VECTOR2I) - board_box.SetSize(pcbnew.VECTOR2I(width_nm, height_nm)) + # Create rectangular outline centered at origin + result = outline_commands.add_board_outline({ + "shape": "rectangle", + "centerX": width / 2, # Center X + "centerY": height / 2, # Center Y + "width": width, + "height": height, + "unit": unit + }) - # Note: SetBoardEdgesBoundingBox might not exist in all versions - # The board bounding box is typically derived from actual edge cuts - # For now, we'll just note the size was calculated - logger.info(f"Board size set to {width}x{height} {unit}") - - return { - "success": True, - "message": f"Set board size to {width}x{height} {unit}", - "size": { - "width": width, - "height": height, - "unit": unit + if result.get("success"): + return { + "success": True, + "message": f"Created board outline: {width}x{height} {unit}", + "size": { + "width": width, + "height": height, + "unit": unit + } } - } + else: + return result except Exception as e: logger.error(f"Error setting board size: {str(e)}") diff --git a/python/commands/board/view.py b/python/commands/board/view.py index 8760880..aaad991 100644 --- a/python/commands/board/view.py +++ b/python/commands/board/view.py @@ -176,3 +176,57 @@ class BoardViewCommands: } # Note: LT_USER was removed in KiCAD 9.0 return type_map.get(type_id, "unknown") + + def get_board_extents(self, params: Dict[str, Any]) -> Dict[str, Any]: + """Get the bounding box extents of the board""" + try: + if not self.board: + return { + "success": False, + "message": "No board is loaded", + "errorDetails": "Load or create a board first" + } + + # Get unit preference (default to mm) + unit = params.get("unit", "mm") + scale = 1000000 if unit == "mm" else 25400000 # nm to mm or inch + + # Get board bounding box + board_box = self.board.GetBoardEdgesBoundingBox() + + # Extract bounds in nanometers, then convert + left = board_box.GetLeft() / scale + top = board_box.GetTop() / scale + right = board_box.GetRight() / scale + bottom = board_box.GetBottom() / scale + width = board_box.GetWidth() / scale + height = board_box.GetHeight() / scale + + # Get center point + center_x = board_box.GetCenter().x / scale + center_y = board_box.GetCenter().y / scale + + return { + "success": True, + "extents": { + "left": left, + "top": top, + "right": right, + "bottom": bottom, + "width": width, + "height": height, + "center": { + "x": center_x, + "y": center_y + }, + "unit": unit + } + } + + except Exception as e: + logger.error(f"Error getting board extents: {str(e)}") + return { + "success": False, + "message": "Failed to get board extents", + "errorDetails": str(e) + } diff --git a/python/commands/export.py b/python/commands/export.py index 059f32b..0fca2e6 100644 --- a/python/commands/export.py +++ b/python/commands/export.py @@ -287,7 +287,11 @@ class ExportCommands: } def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]: - """Export 3D model files""" + """Export 3D model files using kicad-cli (KiCAD 9.0 compatible)""" + import subprocess + import platform + import shutil + try: if not self.board: return { @@ -310,46 +314,108 @@ class ExportCommands: "errorDetails": "outputPath parameter is required" } + # Get board file path + board_file = self.board.GetFileName() + if not board_file or not os.path.exists(board_file): + return { + "success": False, + "message": "Board file not found", + "errorDetails": "Board must be saved before exporting 3D models" + } + # Create output directory if it doesn't exist output_path = os.path.abspath(os.path.expanduser(output_path)) os.makedirs(os.path.dirname(output_path), exist_ok=True) - # Get 3D viewer - viewer = self.board.Get3DViewer() - if not viewer: + # Find kicad-cli executable + kicad_cli = self._find_kicad_cli() + if not kicad_cli: return { "success": False, - "message": "3D viewer not available", - "errorDetails": "Could not initialize 3D viewer" + "message": "kicad-cli not found", + "errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH." } - # Set export options - viewer.SetCopperLayersOn(include_copper) - viewer.SetSolderMaskLayersOn(include_solder_mask) - viewer.SetSilkScreenLayersOn(include_silkscreen) - viewer.Set3DModelsOn(include_components) + # Build command based on format + format_upper = format.upper() + + if format_upper == "STEP": + cmd = [ + kicad_cli, + 'pcb', 'export', 'step', + '--output', output_path, + '--force' # Overwrite existing file + ] + + # Add options based on parameters + if not include_components: + cmd.append('--no-components') + if include_copper: + cmd.extend(['--include-tracks', '--include-pads', '--include-zones']) + if include_silkscreen: + cmd.append('--include-silkscreen') + if include_solder_mask: + cmd.append('--include-soldermask') + + cmd.append(board_file) + + elif format_upper == "VRML": + cmd = [ + kicad_cli, + 'pcb', 'export', 'vrml', + '--output', output_path, + '--units', 'mm', # Use mm for consistency + '--force' + ] + + if not include_components: + # Note: VRML export doesn't have a direct --no-components flag + # The models will be included by default, but can be controlled via 3D settings + pass + + cmd.append(board_file) - # Export based on format - if format == "STEP": - viewer.ExportSTEPFile(output_path) - elif format == "VRML": - viewer.ExportVRMLFile(output_path) else: return { "success": False, "message": "Unsupported format", - "errorDetails": f"Format {format} is not supported" + "errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'." + } + + # Execute kicad-cli command + logger.info(f"Running 3D export command: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300 # 5 minute timeout for 3D export + ) + + if result.returncode != 0: + logger.error(f"3D export command failed: {result.stderr}") + return { + "success": False, + "message": "3D export command failed", + "errorDetails": result.stderr } return { "success": True, - "message": f"Exported {format} file", + "message": f"Exported {format_upper} file", "file": { "path": output_path, - "format": format + "format": format_upper } } + except subprocess.TimeoutExpired: + logger.error("3D export command timed out") + return { + "success": False, + "message": "3D export timed out", + "errorDetails": "Export took longer than 5 minutes" + } except Exception as e: logger.error(f"Error exporting 3D model: {str(e)}") return { @@ -495,3 +561,44 @@ class ExportCommands: import json with open(path, 'w') as f: json.dump({"components": components}, f, indent=2) + + def _find_kicad_cli(self) -> Optional[str]: + """Find kicad-cli executable in system PATH or common locations + + Returns: + Path to kicad-cli executable, or None if not found + """ + import shutil + import platform + + # Try system PATH first + cli_path = shutil.which("kicad-cli") + if cli_path: + return cli_path + + # Try platform-specific default locations + system = platform.system() + + if system == "Windows": + possible_paths = [ + r"C:\Program Files\KiCad\9.0\bin\kicad-cli.exe", + r"C:\Program Files\KiCad\8.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\9.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\8.0\bin\kicad-cli.exe", + ] + elif system == "Darwin": # macOS + possible_paths = [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "/usr/local/bin/kicad-cli", + ] + else: # Linux + possible_paths = [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli", + ] + + for path in possible_paths: + if os.path.exists(path): + return path + + return None