#
tokens: 24313/50000 5/81 files (page 3/3)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 3. Use http://codebase.md/mixelpixx/kicad-mcp-server?page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── config
│   ├── claude-desktop-config.json
│   ├── default-config.json
│   ├── linux-config.example.json
│   ├── macos-config.example.json
│   └── windows-config.example.json
├── CONTRIBUTING.md
├── docs
│   ├── BUILD_AND_TEST_SESSION.md
│   ├── CLIENT_CONFIGURATION.md
│   ├── IPC_API_MIGRATION_PLAN.md
│   ├── JLCPCB_INTEGRATION_PLAN.md
│   ├── KNOWN_ISSUES.md
│   ├── LIBRARY_INTEGRATION.md
│   ├── LINUX_COMPATIBILITY_AUDIT.md
│   ├── REALTIME_WORKFLOW.md
│   ├── ROADMAP.md
│   ├── STATUS_SUMMARY.md
│   ├── UI_AUTO_LAUNCH.md
│   ├── VISUAL_FEEDBACK.md
│   ├── WEEK1_SESSION1_SUMMARY.md
│   └── WEEK1_SESSION2_SUMMARY.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│   ├── commands
│   │   ├── __init__.py
│   │   ├── board
│   │   │   ├── __init__.py
│   │   │   ├── layers.py
│   │   │   ├── outline.py
│   │   │   ├── size.py
│   │   │   └── view.py
│   │   ├── board.py
│   │   ├── component_schematic.py
│   │   ├── component.py
│   │   ├── connection_schematic.py
│   │   ├── design_rules.py
│   │   ├── export.py
│   │   ├── library_schematic.py
│   │   ├── project.py
│   │   ├── routing.py
│   │   └── schematic.py
│   ├── kicad_api
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── factory.py
│   │   ├── ipc_backend.py
│   │   └── swig_backend.py
│   ├── kicad_interface.py
│   ├── requirements.txt
│   └── utils
│       ├── __init__.py
│       ├── kicad_process.py
│       └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│   ├── auto_refresh_kicad.sh
│   └── install-linux.sh
├── src
│   ├── config.ts
│   ├── index.ts
│   ├── kicad-server.ts
│   ├── logger.ts
│   ├── prompts
│   │   ├── component.ts
│   │   ├── design.ts
│   │   ├── index.ts
│   │   └── routing.ts
│   ├── resources
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── index.ts
│   │   ├── library.ts
│   │   └── project.ts
│   ├── server.ts
│   ├── tools
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── component.txt
│   │   ├── design-rules.ts
│   │   ├── export.ts
│   │   ├── index.ts
│   │   ├── project.ts
│   │   ├── routing.ts
│   │   ├── schematic.ts
│   │   └── ui.ts
│   └── utils
│       └── resource-helpers.ts
├── tests
│   ├── __init__.py
│   └── test_platform_helper.py
├── tsconfig-json.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/kicad-server.ts:
--------------------------------------------------------------------------------

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { spawn, ChildProcess } from 'child_process';
import { existsSync } from 'fs';
import path from 'path';

// Import all tool definitions for reference
// import { registerBoardTools } from './tools/board.js';
// import { registerComponentTools } from './tools/component.js';
// import { registerRoutingTools } from './tools/routing.js';
// import { registerDesignRuleTools } from './tools/design-rules.js';
// import { registerExportTools } from './tools/export.js';
// import { registerProjectTools } from './tools/project.js';
// import { registerSchematicTools } from './tools/schematic.js';

class KiCADServer {
  private server: Server;
  private pythonProcess: ChildProcess | null = null;
  private kicadScriptPath: string;
  private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
  private processingRequest = false;

  constructor() {
    // Set absolute path to the Python KiCAD interface script
    // Using a hardcoded path to avoid cwd() issues when running from Cline
    this.kicadScriptPath = 'c:/repo/KiCAD-MCP/python/kicad_interface.py';
    
    // Check if script exists
    if (!existsSync(this.kicadScriptPath)) {
      throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
    }
    
    // Initialize the server
    this.server = new Server(
      {
        name: 'kicad-mcp-server',
        version: '1.0.0'
      },
      {
        capabilities: {
          tools: {
            // Empty object here, tools will be registered dynamically
          }
        }
      }
    );
    
    // Initialize handler with direct pass-through to Python KiCAD interface
    // We don't register TypeScript tools since we'll handle everything in Python

    // Register tool list handler
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        // Project tools
        {
          name: 'create_project',
          description: 'Create a new KiCAD project',
          inputSchema: {
            type: 'object',
            properties: {
              projectName: { type: 'string', description: 'Name of the new project' },
              path: { type: 'string', description: 'Path where to create the project' },
              template: { type: 'string', description: 'Optional template to use' }
            },
            required: ['projectName']
          }
        },
        {
          name: 'open_project',
          description: 'Open an existing KiCAD project',
          inputSchema: {
            type: 'object',
            properties: {
              filename: { type: 'string', description: 'Path to the project file' }
            },
            required: ['filename']
          }
        },
        {
          name: 'save_project',
          description: 'Save the current KiCAD project',
          inputSchema: {
            type: 'object',
            properties: {
              filename: { type: 'string', description: 'Optional path to save to' }
            }
          }
        },
        {
          name: 'get_project_info',
          description: 'Get information about the current project',
          inputSchema: {
            type: 'object',
            properties: {}
          }
        },
        
        // Board tools
        {
          name: 'set_board_size',
          description: 'Set the size of the PCB board',
          inputSchema: {
            type: 'object',
            properties: {
              width: { type: 'number', description: 'Board width' },
              height: { type: 'number', description: 'Board height' },
              unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
            },
            required: ['width', 'height']
          }
        },
        {
          name: 'add_board_outline',
          description: 'Add a board outline to the PCB',
          inputSchema: {
            type: 'object',
            properties: {
              shape: { type: 'string', description: 'Shape of outline (rectangle, circle, polygon, rounded_rectangle)' },
              width: { type: 'number', description: 'Width for rectangle shapes' },
              height: { type: 'number', description: 'Height for rectangle shapes' },
              radius: { type: 'number', description: 'Radius for circle shapes' },
              cornerRadius: { type: 'number', description: 'Corner radius for rounded rectangles' },
              points: { type: 'array', description: 'Array of points for polygon shapes' },
              centerX: { type: 'number', description: 'X coordinate of center' },
              centerY: { type: 'number', description: 'Y coordinate of center' },
              unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
            }
          }
        },
        
        // Component tools
        {
          name: 'place_component',
          description: 'Place a component on the PCB',
          inputSchema: {
            type: 'object',
            properties: {
              componentId: { type: 'string', description: 'Component ID/footprint to place' },
              position: { type: 'object', description: 'Position coordinates' },
              reference: { type: 'string', description: 'Component reference designator' },
              value: { type: 'string', description: 'Component value' },
              rotation: { type: 'number', description: 'Rotation angle in degrees' },
              layer: { type: 'string', description: 'Layer to place component on' }
            },
            required: ['componentId', 'position']
          }
        },
        
        // Routing tools
        {
          name: 'add_net',
          description: 'Add a new net to the PCB',
          inputSchema: {
            type: 'object',
            properties: {
              name: { type: 'string', description: 'Net name' },
              class: { type: 'string', description: 'Net class' }
            },
            required: ['name']
          }
        },
        {
          name: 'route_trace',
          description: 'Route a trace between two points or pads',
          inputSchema: {
            type: 'object',
            properties: {
              start: { type: 'object', description: 'Start point or pad' },
              end: { type: 'object', description: 'End point or pad' },
              layer: { type: 'string', description: 'Layer to route on' },
              width: { type: 'number', description: 'Track width' },
              net: { type: 'string', description: 'Net name' }
            },
            required: ['start', 'end']
          }
        },
        
        // Schematic tools
        {
          name: 'create_schematic',
          description: 'Create a new KiCAD schematic',
          inputSchema: {
            type: 'object',
            properties: {
              projectName: { type: 'string', description: 'Name of the schematic project' },
              path: { type: 'string', description: 'Path where to create the schematic file' },
              metadata: { type: 'object', description: 'Optional metadata for the schematic' }
            },
            required: ['projectName']
          }
        },
        {
          name: 'load_schematic',
          description: 'Load an existing KiCAD schematic',
          inputSchema: {
            type: 'object',
            properties: {
              filename: { type: 'string', description: 'Path to the schematic file to load' }
            },
            required: ['filename']
          }
        },
        {
          name: 'add_schematic_component',
          description: 'Add a component to a KiCAD schematic',
          inputSchema: {
            type: 'object',
            properties: {
              schematicPath: { type: 'string', description: 'Path to the schematic file' },
              component: { 
                type: 'object', 
                description: 'Component definition',
                properties: {
                  type: { type: 'string', description: 'Component type (e.g., R, C, LED)' },
                  reference: { type: 'string', description: 'Reference designator (e.g., R1, C2)' },
                  value: { type: 'string', description: 'Component value (e.g., 10k, 0.1uF)' },
                  library: { type: 'string', description: 'Symbol library name' },
                  x: { type: 'number', description: 'X position in schematic' },
                  y: { type: 'number', description: 'Y position in schematic' },
                  rotation: { type: 'number', description: 'Rotation angle in degrees' },
                  properties: { type: 'object', description: 'Additional properties' }
                },
                required: ['type', 'reference']
              }
            },
            required: ['schematicPath', 'component']
          }
        },
        {
          name: 'add_schematic_wire',
          description: 'Add a wire connection to a KiCAD schematic',
          inputSchema: {
            type: 'object',
            properties: {
              schematicPath: { type: 'string', description: 'Path to the schematic file' },
              startPoint: { 
                type: 'array', 
                description: 'Starting point coordinates [x, y]',
                items: { type: 'number' },
                minItems: 2,
                maxItems: 2
              },
              endPoint: { 
                type: 'array', 
                description: 'Ending point coordinates [x, y]',
                items: { type: 'number' },
                minItems: 2,
                maxItems: 2
              }
            },
            required: ['schematicPath', 'startPoint', 'endPoint']
          }
        },
        {
          name: 'list_schematic_libraries',
          description: 'List available KiCAD symbol libraries',
          inputSchema: {
            type: 'object',
            properties: {
              searchPaths: { 
                type: 'array', 
                description: 'Optional search paths for libraries',
                items: { type: 'string' }
              }
            }
          }
        },
        {
          name: 'export_schematic_pdf',
          description: 'Export a KiCAD schematic to PDF',
          inputSchema: {
            type: 'object',
            properties: {
              schematicPath: { type: 'string', description: 'Path to the schematic file' },
              outputPath: { type: 'string', description: 'Path for the output PDF file' }
            },
            required: ['schematicPath', 'outputPath']
          }
        }
      ]
    }));

    // Register tool call handler
    this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
      const toolName = request.params.name;
      const args = request.params.arguments || {};
      
      // Pass all commands directly to KiCAD Python interface
      try {
        return await this.callKicadScript(toolName, args);
      } catch (error) {
        console.error(`Error executing tool ${toolName}:`, error);
        throw new Error(`Unknown tool: ${toolName}`);
      }
    });
  }

  async start() {
    try {
      console.error('Starting KiCAD MCP server...');
      
      // Start the Python process for KiCAD scripting
      console.error(`Starting Python process with script: ${this.kicadScriptPath}`);
      const pythonExe = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
      
      console.error(`Using Python executable: ${pythonExe}`);
      this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
        stdio: ['pipe', 'pipe', 'pipe'],
        env: {
          ...process.env,
          PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
        }
      });
      
      // Listen for process exit
      this.pythonProcess.on('exit', (code, signal) => {
        console.error(`Python process exited with code ${code} and signal ${signal}`);
        this.pythonProcess = null;
      });
      
      // Listen for process errors
      this.pythonProcess.on('error', (err) => {
        console.error(`Python process error: ${err.message}`);
      });
      
      // Set up error logging for stderr
      if (this.pythonProcess.stderr) {
        this.pythonProcess.stderr.on('data', (data: Buffer) => {
          console.error(`Python stderr: ${data.toString()}`);
        });
      }
      
      // Connect to transport
      const transport = new StdioServerTransport();
      await this.server.connect(transport);
      console.error('KiCAD MCP server running');
      
      // Keep the process running
      process.on('SIGINT', () => {
        if (this.pythonProcess) {
          this.pythonProcess.kill();
        }
        this.server.close().catch(console.error);
        process.exit(0);
      });
      
    } catch (error: unknown) {
      if (error instanceof Error) {
        console.error('Failed to start MCP server:', error.message);
      } else {
        console.error('Failed to start MCP server: Unknown error');
      }
      process.exit(1);
    }
  }

  private async callKicadScript(command: string, params: any): Promise<any> {
    return new Promise((resolve, reject) => {
      // Check if Python process is running
      if (!this.pythonProcess) {
        console.error('Python process is not running');
        reject(new Error("Python process for KiCAD scripting is not running"));
        return;
      }
      
      // Add request to queue
      this.requestQueue.push({
        request: { command, params },
        resolve,
        reject
      });
      
      // Process the queue if not already processing
      if (!this.processingRequest) {
        this.processNextRequest();
      }
    });
  }
  
  private processNextRequest(): void {
    // If no more requests or already processing, return
    if (this.requestQueue.length === 0 || this.processingRequest) {
      return;
    }
    
    // Set processing flag
    this.processingRequest = true;
    
    // Get the next request
    const { request, resolve, reject } = this.requestQueue.shift()!;
    
    try {
      console.error(`Processing KiCAD command: ${request.command}`);
      
      // Format the command and parameters as JSON
      const requestStr = JSON.stringify(request);
      
      // Set up response handling
      let responseData = '';
      
      // Clear any previous listeners
      if (this.pythonProcess?.stdout) {
        this.pythonProcess.stdout.removeAllListeners('data');
      }
      
      // Set up new listeners
      if (this.pythonProcess?.stdout) {
        this.pythonProcess.stdout.on('data', (data: Buffer) => {
          const chunk = data.toString();
          console.error(`Received data chunk: ${chunk.length} bytes`);
          responseData += chunk;
          
          // Check if we have a complete response
          try {
            // Try to parse the response as JSON
            const result = JSON.parse(responseData);
            
            // If we get here, we have a valid JSON response
            console.error(`Completed KiCAD command: ${request.command} with result: ${JSON.stringify(result)}`);
            
            // Reset processing flag
            this.processingRequest = false;
            
            // Process next request if any
            setTimeout(() => this.processNextRequest(), 0);
            
            // Clear listeners
            if (this.pythonProcess?.stdout) {
              this.pythonProcess.stdout.removeAllListeners('data');
            }
            
            // Resolve with the expected MCP tool response format
            if (result.success) {
              resolve({
                content: [
                  {
                    type: 'text',
                    text: JSON.stringify(result, null, 2)
                  }
                ]
              });
            } else {
              resolve({
                content: [
                  {
                    type: 'text',
                    text: result.errorDetails || result.message || 'Unknown error'
                  }
                ],
                isError: true
              });
            }
          } catch (e) {
            // Not a complete JSON yet, keep collecting data
          }
        });
      }
      
      // Set a timeout
      const timeout = setTimeout(() => {
        console.error(`Command timeout: ${request.command}`);
        
        // Clear listeners
        if (this.pythonProcess?.stdout) {
          this.pythonProcess.stdout.removeAllListeners('data');
        }
        
        // Reset processing flag
        this.processingRequest = false;
        
        // Process next request
        setTimeout(() => this.processNextRequest(), 0);
        
        // Reject the promise
        reject(new Error(`Command timeout: ${request.command}`));
      }, 30000); // 30 seconds timeout
      
      // Write the request to the Python process
      console.error(`Sending request: ${requestStr}`);
      this.pythonProcess?.stdin?.write(requestStr + '\n');
    } catch (error) {
      console.error(`Error processing request: ${error}`);
      
      // Reset processing flag
      this.processingRequest = false;
      
      // Process next request
      setTimeout(() => this.processNextRequest(), 0);
      
      // Reject the promise
      reject(error);
    }
  }
}

// Start the server
const server = new KiCADServer();
server.start().catch(console.error);

```

--------------------------------------------------------------------------------
/python/commands/export.py:
--------------------------------------------------------------------------------

```python
"""
Export command implementations for KiCAD interface
"""

import os
import pcbnew
import logging
from typing import Dict, Any, Optional, List, Tuple
import base64

logger = logging.getLogger('kicad_interface')

class ExportCommands:
    """Handles export-related KiCAD operations"""

    def __init__(self, board: Optional[pcbnew.BOARD] = None):
        """Initialize with optional board instance"""
        self.board = board

    def export_gerber(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Export Gerber files"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            output_dir = params.get("outputDir")
            layers = params.get("layers", [])
            use_protel_extensions = params.get("useProtelExtensions", False)
            generate_drill_files = params.get("generateDrillFiles", True)
            generate_map_file = params.get("generateMapFile", False)
            use_aux_origin = params.get("useAuxOrigin", False)

            if not output_dir:
                return {
                    "success": False,
                    "message": "Missing output directory",
                    "errorDetails": "outputDir parameter is required"
                }

            # Create output directory if it doesn't exist
            output_dir = os.path.abspath(os.path.expanduser(output_dir))
            os.makedirs(output_dir, exist_ok=True)

            # Create plot controller
            plotter = pcbnew.PLOT_CONTROLLER(self.board)
            
            # Set up plot options
            plot_opts = plotter.GetPlotOptions()
            plot_opts.SetOutputDirectory(output_dir)
            plot_opts.SetFormat(pcbnew.PLOT_FORMAT_GERBER)
            plot_opts.SetUseGerberProtelExtensions(use_protel_extensions)
            plot_opts.SetUseAuxOrigin(use_aux_origin)
            plot_opts.SetCreateGerberJobFile(generate_map_file)
            plot_opts.SetSubtractMaskFromSilk(True)

            # Plot specified layers or all copper layers
            plotted_layers = []
            if layers:
                for layer_name in layers:
                    layer_id = self.board.GetLayerID(layer_name)
                    if layer_id >= 0:
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)
            else:
                for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
                    if self.board.IsLayerEnabled(layer_id):
                        layer_name = self.board.GetLayerName(layer_id)
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)

            # Generate drill files if requested
            drill_files = []
            if generate_drill_files:
                drill_writer = pcbnew.EXCELLON_WRITER(self.board)
                drill_writer.SetFormat(True)
                drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
                
                merge_npth = False  # Keep plated/non-plated holes separate
                drill_writer.SetOptions(merge_npth)
                
                drill_writer.CreateDrillandMapFilesSet(output_dir, True, generate_map_file)
                
                # Get list of generated drill files
                for file in os.listdir(output_dir):
                    if file.endswith(".drl") or file.endswith(".cnc"):
                        drill_files.append(file)

            return {
                "success": True,
                "message": "Exported Gerber files",
                "files": {
                    "gerber": plotted_layers,
                    "drill": drill_files,
                    "map": ["job.gbrjob"] if generate_map_file else []
                },
                "outputDir": output_dir
            }

        except Exception as e:
            logger.error(f"Error exporting Gerber files: {str(e)}")
            return {
                "success": False,
                "message": "Failed to export Gerber files",
                "errorDetails": str(e)
            }

    def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Export PDF files"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            output_path = params.get("outputPath")
            layers = params.get("layers", [])
            black_and_white = params.get("blackAndWhite", False)
            frame_reference = params.get("frameReference", True)
            page_size = params.get("pageSize", "A4")

            if not output_path:
                return {
                    "success": False,
                    "message": "Missing output path",
                    "errorDetails": "outputPath parameter is required"
                }

            # 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)

            # Create plot controller
            plotter = pcbnew.PLOT_CONTROLLER(self.board)
            
            # Set up plot options
            plot_opts = plotter.GetPlotOptions()
            plot_opts.SetOutputDirectory(os.path.dirname(output_path))
            plot_opts.SetFormat(pcbnew.PLOT_FORMAT_PDF)
            plot_opts.SetPlotFrameRef(frame_reference)
            plot_opts.SetPlotValue(True)
            plot_opts.SetPlotReference(True)
            plot_opts.SetMonochrome(black_and_white)

            # Set page size
            page_sizes = {
                "A4": (297, 210),
                "A3": (420, 297),
                "A2": (594, 420),
                "A1": (841, 594),
                "A0": (1189, 841),
                "Letter": (279.4, 215.9),
                "Legal": (355.6, 215.9),
                "Tabloid": (431.8, 279.4)
            }
            if page_size in page_sizes:
                height, width = page_sizes[page_size]
                plot_opts.SetPageSettings((width, height))

            # Plot specified layers or all enabled layers
            plotted_layers = []
            if layers:
                for layer_name in layers:
                    layer_id = self.board.GetLayerID(layer_name)
                    if layer_id >= 0:
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)
            else:
                for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
                    if self.board.IsLayerEnabled(layer_id):
                        layer_name = self.board.GetLayerName(layer_id)
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)

            return {
                "success": True,
                "message": "Exported PDF file",
                "file": {
                    "path": output_path,
                    "layers": plotted_layers,
                    "pageSize": page_size
                }
            }

        except Exception as e:
            logger.error(f"Error exporting PDF file: {str(e)}")
            return {
                "success": False,
                "message": "Failed to export PDF file",
                "errorDetails": str(e)
            }

    def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Export SVG files"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            output_path = params.get("outputPath")
            layers = params.get("layers", [])
            black_and_white = params.get("blackAndWhite", False)
            include_components = params.get("includeComponents", True)

            if not output_path:
                return {
                    "success": False,
                    "message": "Missing output path",
                    "errorDetails": "outputPath parameter is required"
                }

            # 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)

            # Create plot controller
            plotter = pcbnew.PLOT_CONTROLLER(self.board)
            
            # Set up plot options
            plot_opts = plotter.GetPlotOptions()
            plot_opts.SetOutputDirectory(os.path.dirname(output_path))
            plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG)
            plot_opts.SetPlotValue(include_components)
            plot_opts.SetPlotReference(include_components)
            plot_opts.SetMonochrome(black_and_white)

            # Plot specified layers or all enabled layers
            plotted_layers = []
            if layers:
                for layer_name in layers:
                    layer_id = self.board.GetLayerID(layer_name)
                    if layer_id >= 0:
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)
            else:
                for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
                    if self.board.IsLayerEnabled(layer_id):
                        layer_name = self.board.GetLayerName(layer_id)
                        plotter.PlotLayer(layer_id)
                        plotted_layers.append(layer_name)

            return {
                "success": True,
                "message": "Exported SVG file",
                "file": {
                    "path": output_path,
                    "layers": plotted_layers
                }
            }

        except Exception as e:
            logger.error(f"Error exporting SVG file: {str(e)}")
            return {
                "success": False,
                "message": "Failed to export SVG file",
                "errorDetails": str(e)
            }

    def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Export 3D model files"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            output_path = params.get("outputPath")
            format = params.get("format", "STEP")
            include_components = params.get("includeComponents", True)
            include_copper = params.get("includeCopper", True)
            include_solder_mask = params.get("includeSolderMask", True)
            include_silkscreen = params.get("includeSilkscreen", True)

            if not output_path:
                return {
                    "success": False,
                    "message": "Missing output path",
                    "errorDetails": "outputPath parameter is required"
                }

            # 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:
                return {
                    "success": False,
                    "message": "3D viewer not available",
                    "errorDetails": "Could not initialize 3D viewer"
                }

            # Set export options
            viewer.SetCopperLayersOn(include_copper)
            viewer.SetSolderMaskLayersOn(include_solder_mask)
            viewer.SetSilkScreenLayersOn(include_silkscreen)
            viewer.Set3DModelsOn(include_components)

            # 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"
                }

            return {
                "success": True,
                "message": f"Exported {format} file",
                "file": {
                    "path": output_path,
                    "format": format
                }
            }

        except Exception as e:
            logger.error(f"Error exporting 3D model: {str(e)}")
            return {
                "success": False,
                "message": "Failed to export 3D model",
                "errorDetails": str(e)
            }

    def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Export Bill of Materials"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            output_path = params.get("outputPath")
            format = params.get("format", "CSV")
            group_by_value = params.get("groupByValue", True)
            include_attributes = params.get("includeAttributes", [])

            if not output_path:
                return {
                    "success": False,
                    "message": "Missing output path",
                    "errorDetails": "outputPath parameter is required"
                }

            # 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 all components
            components = []
            for module in self.board.GetFootprints():
                component = {
                    "reference": module.GetReference(),
                    "value": module.GetValue(),
                    "footprint": module.GetFootprintName(),
                    "layer": self.board.GetLayerName(module.GetLayer())
                }

                # Add requested attributes
                for attr in include_attributes:
                    if hasattr(module, f"Get{attr}"):
                        component[attr] = getattr(module, f"Get{attr}")()

                components.append(component)

            # Group by value if requested
            if group_by_value:
                grouped = {}
                for comp in components:
                    key = f"{comp['value']}_{comp['footprint']}"
                    if key not in grouped:
                        grouped[key] = {
                            "value": comp["value"],
                            "footprint": comp["footprint"],
                            "quantity": 1,
                            "references": [comp["reference"]]
                        }
                    else:
                        grouped[key]["quantity"] += 1
                        grouped[key]["references"].append(comp["reference"])
                components = list(grouped.values())

            # Export based on format
            if format == "CSV":
                self._export_bom_csv(output_path, components)
            elif format == "XML":
                self._export_bom_xml(output_path, components)
            elif format == "HTML":
                self._export_bom_html(output_path, components)
            elif format == "JSON":
                self._export_bom_json(output_path, components)
            else:
                return {
                    "success": False,
                    "message": "Unsupported format",
                    "errorDetails": f"Format {format} is not supported"
                }

            return {
                "success": True,
                "message": f"Exported BOM to {format}",
                "file": {
                    "path": output_path,
                    "format": format,
                    "componentCount": len(components)
                }
            }

        except Exception as e:
            logger.error(f"Error exporting BOM: {str(e)}")
            return {
                "success": False,
                "message": "Failed to export BOM",
                "errorDetails": str(e)
            }

    def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
        """Export BOM to CSV format"""
        import csv
        with open(path, 'w', newline='') as f:
            writer = csv.DictWriter(f, fieldnames=components[0].keys())
            writer.writeheader()
            writer.writerows(components)

    def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
        """Export BOM to XML format"""
        import xml.etree.ElementTree as ET
        root = ET.Element("bom")
        for comp in components:
            comp_elem = ET.SubElement(root, "component")
            for key, value in comp.items():
                elem = ET.SubElement(comp_elem, key)
                elem.text = str(value)
        tree = ET.ElementTree(root)
        tree.write(path, encoding='utf-8', xml_declaration=True)

    def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
        """Export BOM to HTML format"""
        html = ["<html><head><title>Bill of Materials</title></head><body>"]
        html.append("<table border='1'><tr>")
        # Headers
        for key in components[0].keys():
            html.append(f"<th>{key}</th>")
        html.append("</tr>")
        # Data
        for comp in components:
            html.append("<tr>")
            for value in comp.values():
                html.append(f"<td>{value}</td>")
            html.append("</tr>")
        html.append("</table></body></html>")
        with open(path, 'w') as f:
            f.write("\n".join(html))

    def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
        """Export BOM to JSON format"""
        import json
        with open(path, 'w') as f:
            json.dump({"components": components}, f, indent=2)

```

--------------------------------------------------------------------------------
/python/kicad_interface.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
KiCAD Python Interface Script for Model Context Protocol

This script handles communication between the MCP TypeScript server
and KiCAD's Python API (pcbnew). It receives commands via stdin as
JSON and returns responses via stdout also as JSON.
"""

import sys
import json
import traceback
import logging
import os
from typing import Dict, Any, Optional

# Configure logging
log_dir = os.path.join(os.path.expanduser('~'), '.kicad-mcp', 'logs')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, 'kicad_interface.log')

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(log_file),
        logging.StreamHandler(sys.stderr)
    ]
)
logger = logging.getLogger('kicad_interface')

# Log Python environment details
logger.info(f"Python version: {sys.version}")
logger.info(f"Python executable: {sys.executable}")

# Add utils directory to path for imports
utils_dir = os.path.join(os.path.dirname(__file__))
if utils_dir not in sys.path:
    sys.path.insert(0, utils_dir)

# Import platform helper and add KiCAD paths
from utils.platform_helper import PlatformHelper
from utils.kicad_process import check_and_launch_kicad, KiCADProcessManager

logger.info(f"Detecting KiCAD Python paths for {PlatformHelper.get_platform_name()}...")
paths_added = PlatformHelper.add_kicad_to_python_path()

if paths_added:
    logger.info("Successfully added KiCAD Python paths to sys.path")
else:
    logger.warning("No KiCAD Python paths found - attempting to import pcbnew from system path")

logger.info(f"Current Python path: {sys.path}")

# Check if auto-launch is enabled
AUTO_LAUNCH_KICAD = os.environ.get("KICAD_AUTO_LAUNCH", "false").lower() == "true"
if AUTO_LAUNCH_KICAD:
    logger.info("KiCAD auto-launch enabled")

# Import KiCAD's Python API
try:
    logger.info("Attempting to import pcbnew module...")
    import pcbnew  # type: ignore
    logger.info(f"Successfully imported pcbnew module from: {pcbnew.__file__}")
    logger.info(f"pcbnew version: {pcbnew.GetBuildVersion()}")
except ImportError as e:
    logger.error(f"Failed to import pcbnew module: {e}")
    logger.error(f"Current sys.path: {sys.path}")
    error_response = {
        "success": False,
        "message": "Failed to import pcbnew module",
        "errorDetails": f"Error: {str(e)}\nPython path: {sys.path}"
    }
    print(json.dumps(error_response))
    sys.exit(1)
except Exception as e:
    logger.error(f"Unexpected error importing pcbnew: {e}")
    logger.error(traceback.format_exc())
    error_response = {
        "success": False,
        "message": "Error importing pcbnew module",
        "errorDetails": str(e)
    }
    print(json.dumps(error_response))
    sys.exit(1)

# Import command handlers
try:
    logger.info("Importing command handlers...")
    from commands.project import ProjectCommands
    from commands.board import BoardCommands
    from commands.component import ComponentCommands
    from commands.routing import RoutingCommands
    from commands.design_rules import DesignRuleCommands
    from commands.export import ExportCommands
    from commands.schematic import SchematicManager
    from commands.component_schematic import ComponentManager
    from commands.connection_schematic import ConnectionManager
    from commands.library_schematic import LibraryManager
    logger.info("Successfully imported all command handlers")
except ImportError as e:
    logger.error(f"Failed to import command handlers: {e}")
    error_response = {
        "success": False,
        "message": "Failed to import command handlers",
        "errorDetails": str(e)
    }
    print(json.dumps(error_response))
    sys.exit(1)

class KiCADInterface:
    """Main interface class to handle KiCAD operations"""
    
    def __init__(self):
        """Initialize the interface and command handlers"""
        self.board = None
        self.project_filename = None
        
        logger.info("Initializing command handlers...")
        
        # Initialize command handlers
        self.project_commands = ProjectCommands(self.board)
        self.board_commands = BoardCommands(self.board)
        self.component_commands = ComponentCommands(self.board)
        self.routing_commands = RoutingCommands(self.board)
        self.design_rule_commands = DesignRuleCommands(self.board)
        self.export_commands = ExportCommands(self.board)
        
        # Schematic-related classes don't need board reference
        # as they operate directly on schematic files
        
        # Command routing dictionary
        self.command_routes = {
            # Project commands
            "create_project": self.project_commands.create_project,
            "open_project": self.project_commands.open_project,
            "save_project": self.project_commands.save_project,
            "get_project_info": self.project_commands.get_project_info,
            
            # Board commands
            "set_board_size": self.board_commands.set_board_size,
            "add_layer": self.board_commands.add_layer,
            "set_active_layer": self.board_commands.set_active_layer,
            "get_board_info": self.board_commands.get_board_info,
            "get_layer_list": self.board_commands.get_layer_list,
            "get_board_2d_view": self.board_commands.get_board_2d_view,
            "add_board_outline": self.board_commands.add_board_outline,
            "add_mounting_hole": self.board_commands.add_mounting_hole,
            "add_text": self.board_commands.add_text,
            "add_board_text": self.board_commands.add_text,  # Alias for TypeScript tool
            
            # Component commands
            "place_component": self.component_commands.place_component,
            "move_component": self.component_commands.move_component,
            "rotate_component": self.component_commands.rotate_component,
            "delete_component": self.component_commands.delete_component,
            "edit_component": self.component_commands.edit_component,
            "get_component_properties": self.component_commands.get_component_properties,
            "get_component_list": self.component_commands.get_component_list,
            "place_component_array": self.component_commands.place_component_array,
            "align_components": self.component_commands.align_components,
            "duplicate_component": self.component_commands.duplicate_component,
            
            # Routing commands
            "add_net": self.routing_commands.add_net,
            "route_trace": self.routing_commands.route_trace,
            "add_via": self.routing_commands.add_via,
            "delete_trace": self.routing_commands.delete_trace,
            "get_nets_list": self.routing_commands.get_nets_list,
            "create_netclass": self.routing_commands.create_netclass,
            "add_copper_pour": self.routing_commands.add_copper_pour,
            "route_differential_pair": self.routing_commands.route_differential_pair,
            
            # Design rule commands
            "set_design_rules": self.design_rule_commands.set_design_rules,
            "get_design_rules": self.design_rule_commands.get_design_rules,
            "run_drc": self.design_rule_commands.run_drc,
            "get_drc_violations": self.design_rule_commands.get_drc_violations,
            
            # Export commands
            "export_gerber": self.export_commands.export_gerber,
            "export_pdf": self.export_commands.export_pdf,
            "export_svg": self.export_commands.export_svg,
            "export_3d": self.export_commands.export_3d,
            "export_bom": self.export_commands.export_bom,
            
            # Schematic commands
            "create_schematic": self._handle_create_schematic,
            "load_schematic": self._handle_load_schematic,
            "add_schematic_component": self._handle_add_schematic_component,
            "add_schematic_wire": self._handle_add_schematic_wire,
            "list_schematic_libraries": self._handle_list_schematic_libraries,
            "export_schematic_pdf": self._handle_export_schematic_pdf,

            # UI/Process management commands
            "check_kicad_ui": self._handle_check_kicad_ui,
            "launch_kicad_ui": self._handle_launch_kicad_ui
        }
        
        logger.info("KiCAD interface initialized")

    def handle_command(self, command: str, params: Dict[str, Any]) -> Dict[str, Any]:
        """Route command to appropriate handler"""
        logger.info(f"Handling command: {command}")
        logger.debug(f"Command parameters: {params}")
        
        try:
            # Get the handler for the command
            handler = self.command_routes.get(command)
            
            if handler:
                # Execute the command
                result = handler(params)
                logger.debug(f"Command result: {result}")
                
                # Update board reference if command was successful
                if result.get("success", False):
                    if command == "create_project" or command == "open_project":
                        logger.info("Updating board reference...")
                        # Get board from the project commands handler
                        self.board = self.project_commands.board
                        self._update_command_handlers()
                
                return result
            else:
                logger.error(f"Unknown command: {command}")
                return {
                    "success": False,
                    "message": f"Unknown command: {command}",
                    "errorDetails": "The specified command is not supported"
                }
                
        except Exception as e:
            # Get the full traceback
            traceback_str = traceback.format_exc()
            logger.error(f"Error handling command {command}: {str(e)}\n{traceback_str}")
            return {
                "success": False,
                "message": f"Error handling command: {command}",
                "errorDetails": f"{str(e)}\n{traceback_str}"
            }

    def _update_command_handlers(self):
        """Update board reference in all command handlers"""
        logger.debug("Updating board reference in command handlers")
        self.project_commands.board = self.board
        self.board_commands.board = self.board
        self.component_commands.board = self.board
        self.routing_commands.board = self.board
        self.design_rule_commands.board = self.board
        self.export_commands.board = self.board
        
    # Schematic command handlers
    def _handle_create_schematic(self, params):
        """Create a new schematic"""
        logger.info("Creating schematic")
        try:
            project_name = params.get("projectName")
            path = params.get("path", ".")
            metadata = params.get("metadata", {})
            
            if not project_name:
                return {"success": False, "message": "Project name is required"}
            
            schematic = SchematicManager.create_schematic(project_name, metadata)
            file_path = f"{path}/{project_name}.kicad_sch"
            success = SchematicManager.save_schematic(schematic, file_path)
            
            return {"success": success, "file_path": file_path}
        except Exception as e:
            logger.error(f"Error creating schematic: {str(e)}")
            return {"success": False, "message": str(e)}
    
    def _handle_load_schematic(self, params):
        """Load an existing schematic"""
        logger.info("Loading schematic")
        try:
            filename = params.get("filename")
            
            if not filename:
                return {"success": False, "message": "Filename is required"}
            
            schematic = SchematicManager.load_schematic(filename)
            success = schematic is not None
            
            if success:
                metadata = SchematicManager.get_schematic_metadata(schematic)
                return {"success": success, "metadata": metadata}
            else:
                return {"success": False, "message": "Failed to load schematic"}
        except Exception as e:
            logger.error(f"Error loading schematic: {str(e)}")
            return {"success": False, "message": str(e)}
    
    def _handle_add_schematic_component(self, params):
        """Add a component to a schematic"""
        logger.info("Adding component to schematic")
        try:
            schematic_path = params.get("schematicPath")
            component = params.get("component", {})
            
            if not schematic_path:
                return {"success": False, "message": "Schematic path is required"}
            if not component:
                return {"success": False, "message": "Component definition is required"}
            
            schematic = SchematicManager.load_schematic(schematic_path)
            if not schematic:
                return {"success": False, "message": "Failed to load schematic"}
            
            component_obj = ComponentManager.add_component(schematic, component)
            success = component_obj is not None
            
            if success:
                SchematicManager.save_schematic(schematic, schematic_path)
                return {"success": True}
            else:
                return {"success": False, "message": "Failed to add component"}
        except Exception as e:
            logger.error(f"Error adding component to schematic: {str(e)}")
            return {"success": False, "message": str(e)}
    
    def _handle_add_schematic_wire(self, params):
        """Add a wire to a schematic"""
        logger.info("Adding wire to schematic")
        try:
            schematic_path = params.get("schematicPath")
            start_point = params.get("startPoint")
            end_point = params.get("endPoint")
            
            if not schematic_path:
                return {"success": False, "message": "Schematic path is required"}
            if not start_point or not end_point:
                return {"success": False, "message": "Start and end points are required"}
            
            schematic = SchematicManager.load_schematic(schematic_path)
            if not schematic:
                return {"success": False, "message": "Failed to load schematic"}
            
            wire = ConnectionManager.add_wire(schematic, start_point, end_point)
            success = wire is not None
            
            if success:
                SchematicManager.save_schematic(schematic, schematic_path)
                return {"success": True}
            else:
                return {"success": False, "message": "Failed to add wire"}
        except Exception as e:
            logger.error(f"Error adding wire to schematic: {str(e)}")
            return {"success": False, "message": str(e)}
    
    def _handle_list_schematic_libraries(self, params):
        """List available symbol libraries"""
        logger.info("Listing schematic libraries")
        try:
            search_paths = params.get("searchPaths")
            
            libraries = LibraryManager.list_available_libraries(search_paths)
            return {"success": True, "libraries": libraries}
        except Exception as e:
            logger.error(f"Error listing schematic libraries: {str(e)}")
            return {"success": False, "message": str(e)}
    
    def _handle_export_schematic_pdf(self, params):
        """Export schematic to PDF"""
        logger.info("Exporting schematic to PDF")
        try:
            schematic_path = params.get("schematicPath")
            output_path = params.get("outputPath")
            
            if not schematic_path:
                return {"success": False, "message": "Schematic path is required"}
            if not output_path:
                return {"success": False, "message": "Output path is required"}
            
            import subprocess
            result = subprocess.run(
                ["kicad-cli", "sch", "export", "pdf", "--output", output_path, schematic_path],
                capture_output=True, 
                text=True
            )
            
            success = result.returncode == 0
            message = result.stderr if not success else ""
            
            return {"success": success, "message": message}
        except Exception as e:
            logger.error(f"Error exporting schematic to PDF: {str(e)}")
            return {"success": False, "message": str(e)}

    def _handle_check_kicad_ui(self, params):
        """Check if KiCAD UI is running"""
        logger.info("Checking if KiCAD UI is running")
        try:
            manager = KiCADProcessManager()
            is_running = manager.is_running()
            processes = manager.get_process_info() if is_running else []

            return {
                "success": True,
                "running": is_running,
                "processes": processes,
                "message": "KiCAD is running" if is_running else "KiCAD is not running"
            }
        except Exception as e:
            logger.error(f"Error checking KiCAD UI status: {str(e)}")
            return {"success": False, "message": str(e)}

    def _handle_launch_kicad_ui(self, params):
        """Launch KiCAD UI"""
        logger.info("Launching KiCAD UI")
        try:
            project_path = params.get("projectPath")
            auto_launch = params.get("autoLaunch", AUTO_LAUNCH_KICAD)

            # Convert project path to Path object if provided
            from pathlib import Path
            path_obj = Path(project_path) if project_path else None

            result = check_and_launch_kicad(path_obj, auto_launch)

            return {
                "success": True,
                **result
            }
        except Exception as e:
            logger.error(f"Error launching KiCAD UI: {str(e)}")
            return {"success": False, "message": str(e)}

def main():
    """Main entry point"""
    logger.info("Starting KiCAD interface...")
    interface = KiCADInterface()
    
    try:
        logger.info("Processing commands from stdin...")
        # Process commands from stdin
        for line in sys.stdin:
            try:
                # Parse command
                logger.debug(f"Received input: {line.strip()}")
                command_data = json.loads(line)
                command = command_data.get("command")
                params = command_data.get("params", {})
                
                if not command:
                    logger.error("Missing command field")
                    response = {
                        "success": False,
                        "message": "Missing command",
                        "errorDetails": "The command field is required"
                    }
                else:
                    # Handle command
                    response = interface.handle_command(command, params)
                
                # Send response
                logger.debug(f"Sending response: {response}")
                print(json.dumps(response))
                sys.stdout.flush()
                
            except json.JSONDecodeError as e:
                logger.error(f"Invalid JSON input: {str(e)}")
                response = {
                    "success": False,
                    "message": "Invalid JSON input",
                    "errorDetails": str(e)
                }
                print(json.dumps(response))
                sys.stdout.flush()
                
    except KeyboardInterrupt:
        logger.info("KiCAD interface stopped")
        sys.exit(0)
        
    except Exception as e:
        logger.error(f"Unexpected error: {str(e)}\n{traceback.format_exc()}")
        sys.exit(1)

if __name__ == "__main__":
    main()

```

--------------------------------------------------------------------------------
/python/commands/routing.py:
--------------------------------------------------------------------------------

```python
"""
Routing-related command implementations for KiCAD interface
"""

import os
import pcbnew
import logging
import math
from typing import Dict, Any, Optional, List, Tuple

logger = logging.getLogger('kicad_interface')

class RoutingCommands:
    """Handles routing-related KiCAD operations"""

    def __init__(self, board: Optional[pcbnew.BOARD] = None):
        """Initialize with optional board instance"""
        self.board = board

    def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Add a new net to the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            name = params.get("name")
            net_class = params.get("class")

            if not name:
                return {
                    "success": False,
                    "message": "Missing net name",
                    "errorDetails": "name parameter is required"
                }

            # Create new net
            netinfo = self.board.GetNetInfo()
            net = netinfo.FindNet(name)
            if not net:
                net = netinfo.AddNet(name)

            # Set net class if provided
            if net_class:
                net_classes = self.board.GetNetClasses()
                if net_classes.Find(net_class):
                    net.SetClass(net_classes.Find(net_class))

            return {
                "success": True,
                "message": f"Added net: {name}",
                "net": {
                    "name": name,
                    "class": net_class if net_class else "Default",
                    "netcode": net.GetNetCode()
                }
            }

        except Exception as e:
            logger.error(f"Error adding net: {str(e)}")
            return {
                "success": False,
                "message": "Failed to add net",
                "errorDetails": str(e)
            }

    def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Route a trace between two points or pads"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            start = params.get("start")
            end = params.get("end")
            layer = params.get("layer", "F.Cu")
            width = params.get("width")
            net = params.get("net")
            via = params.get("via", False)

            if not start or not end:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "start and end points are required"
                }

            # Get layer ID
            layer_id = self.board.GetLayerID(layer)
            if layer_id < 0:
                return {
                    "success": False,
                    "message": "Invalid layer",
                    "errorDetails": f"Layer '{layer}' does not exist"
                }

            # Get start point
            start_point = self._get_point(start)
            end_point = self._get_point(end)

            # Create track segment
            track = pcbnew.PCB_TRACK(self.board)
            track.SetStart(start_point)
            track.SetEnd(end_point)
            track.SetLayer(layer_id)

            # Set width (default to board's current track width)
            if width:
                track.SetWidth(int(width * 1000000))  # Convert mm to nm
            else:
                track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())

            # Set net if provided
            if net:
                netinfo = self.board.GetNetInfo()
                net_obj = netinfo.FindNet(net)
                if net_obj:
                    track.SetNet(net_obj)

            # Add track to board
            self.board.Add(track)

            # Add via if requested and net is specified
            if via and net:
                via_point = end_point
                self.add_via({
                    "position": {
                        "x": via_point.x / 1000000,
                        "y": via_point.y / 1000000,
                        "unit": "mm"
                    },
                    "net": net
                })

            return {
                "success": True,
                "message": "Added trace",
                "trace": {
                    "start": {
                        "x": start_point.x / 1000000,
                        "y": start_point.y / 1000000,
                        "unit": "mm"
                    },
                    "end": {
                        "x": end_point.x / 1000000,
                        "y": end_point.y / 1000000,
                        "unit": "mm"
                    },
                    "layer": layer,
                    "width": track.GetWidth() / 1000000,
                    "net": net
                }
            }

        except Exception as e:
            logger.error(f"Error routing trace: {str(e)}")
            return {
                "success": False,
                "message": "Failed to route trace",
                "errorDetails": str(e)
            }

    def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Add a via at the specified location"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            position = params.get("position")
            size = params.get("size")
            drill = params.get("drill")
            net = params.get("net")
            from_layer = params.get("from_layer", "F.Cu")
            to_layer = params.get("to_layer", "B.Cu")

            if not position:
                return {
                    "success": False,
                    "message": "Missing position",
                    "errorDetails": "position parameter is required"
                }

            # Create via
            via = pcbnew.PCB_VIA(self.board)
            
            # Set position
            scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
            x_nm = int(position["x"] * scale)
            y_nm = int(position["y"] * scale)
            via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))

            # Set size and drill (default to board's current via settings)
            design_settings = self.board.GetDesignSettings()
            via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
            via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())

            # Set layers
            from_id = self.board.GetLayerID(from_layer)
            to_id = self.board.GetLayerID(to_layer)
            if from_id < 0 or to_id < 0:
                return {
                    "success": False,
                    "message": "Invalid layer",
                    "errorDetails": "Specified layers do not exist"
                }
            via.SetLayerPair(from_id, to_id)

            # Set net if provided
            if net:
                netinfo = self.board.GetNetInfo()
                net_obj = netinfo.FindNet(net)
                if net_obj:
                    via.SetNet(net_obj)

            # Add via to board
            self.board.Add(via)

            return {
                "success": True,
                "message": "Added via",
                "via": {
                    "position": {
                        "x": position["x"],
                        "y": position["y"],
                        "unit": position["unit"]
                    },
                    "size": via.GetWidth() / 1000000,
                    "drill": via.GetDrill() / 1000000,
                    "from_layer": from_layer,
                    "to_layer": to_layer,
                    "net": net
                }
            }

        except Exception as e:
            logger.error(f"Error adding via: {str(e)}")
            return {
                "success": False,
                "message": "Failed to add via",
                "errorDetails": str(e)
            }

    def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Delete a trace from the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            trace_uuid = params.get("traceUuid")
            position = params.get("position")

            if not trace_uuid and not position:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "Either traceUuid or position must be provided"
                }

            # Find track by UUID
            if trace_uuid:
                track = None
                for item in self.board.Tracks():
                    if str(item.m_Uuid) == trace_uuid:
                        track = item
                        break

                if not track:
                    return {
                        "success": False,
                        "message": "Track not found",
                        "errorDetails": f"Could not find track with UUID: {trace_uuid}"
                    }

                self.board.Remove(track)
                return {
                    "success": True,
                    "message": f"Deleted track: {trace_uuid}"
                }

            # Find track by position
            if position:
                scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
                x_nm = int(position["x"] * scale)
                y_nm = int(position["y"] * scale)
                point = pcbnew.VECTOR2I(x_nm, y_nm)

                # Find closest track
                closest_track = None
                min_distance = float('inf')
                for track in self.board.Tracks():
                    dist = self._point_to_track_distance(point, track)
                    if dist < min_distance:
                        min_distance = dist
                        closest_track = track

                if closest_track and min_distance < 1000000:  # Within 1mm
                    self.board.Remove(closest_track)
                    return {
                        "success": True,
                        "message": "Deleted track at specified position"
                    }
                else:
                    return {
                        "success": False,
                        "message": "No track found",
                        "errorDetails": "No track found near specified position"
                    }

        except Exception as e:
            logger.error(f"Error deleting trace: {str(e)}")
            return {
                "success": False,
                "message": "Failed to delete trace",
                "errorDetails": str(e)
            }

    def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Get a list of all nets in the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            nets = []
            netinfo = self.board.GetNetInfo()
            for net_code in range(netinfo.GetNetCount()):
                net = netinfo.GetNetItem(net_code)
                if net:
                    nets.append({
                        "name": net.GetNetname(),
                        "code": net.GetNetCode(),
                        "class": net.GetClassName()
                    })

            return {
                "success": True,
                "nets": nets
            }

        except Exception as e:
            logger.error(f"Error getting nets list: {str(e)}")
            return {
                "success": False,
                "message": "Failed to get nets list",
                "errorDetails": str(e)
            }
            
    def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Create a new net class with specified properties"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            name = params.get("name")
            clearance = params.get("clearance")
            track_width = params.get("trackWidth")
            via_diameter = params.get("viaDiameter")
            via_drill = params.get("viaDrill")
            uvia_diameter = params.get("uviaDiameter")
            uvia_drill = params.get("uviaDrill")
            diff_pair_width = params.get("diffPairWidth")
            diff_pair_gap = params.get("diffPairGap")
            nets = params.get("nets", [])

            if not name:
                return {
                    "success": False,
                    "message": "Missing netclass name",
                    "errorDetails": "name parameter is required"
                }

            # Get net classes
            net_classes = self.board.GetNetClasses()
            
            # Create new net class if it doesn't exist
            if not net_classes.Find(name):
                netclass = pcbnew.NETCLASS(name)
                net_classes.Add(netclass)
            else:
                netclass = net_classes.Find(name)

            # Set properties
            scale = 1000000  # mm to nm
            if clearance is not None:
                netclass.SetClearance(int(clearance * scale))
            if track_width is not None:
                netclass.SetTrackWidth(int(track_width * scale))
            if via_diameter is not None:
                netclass.SetViaDiameter(int(via_diameter * scale))
            if via_drill is not None:
                netclass.SetViaDrill(int(via_drill * scale))
            if uvia_diameter is not None:
                netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
            if uvia_drill is not None:
                netclass.SetMicroViaDrill(int(uvia_drill * scale))
            if diff_pair_width is not None:
                netclass.SetDiffPairWidth(int(diff_pair_width * scale))
            if diff_pair_gap is not None:
                netclass.SetDiffPairGap(int(diff_pair_gap * scale))

            # Add nets to net class
            netinfo = self.board.GetNetInfo()
            for net_name in nets:
                net = netinfo.FindNet(net_name)
                if net:
                    net.SetClass(netclass)

            return {
                "success": True,
                "message": f"Created net class: {name}",
                "netClass": {
                    "name": name,
                    "clearance": netclass.GetClearance() / scale,
                    "trackWidth": netclass.GetTrackWidth() / scale,
                    "viaDiameter": netclass.GetViaDiameter() / scale,
                    "viaDrill": netclass.GetViaDrill() / scale,
                    "uviaDiameter": netclass.GetMicroViaDiameter() / scale,
                    "uviaDrill": netclass.GetMicroViaDrill() / scale,
                    "diffPairWidth": netclass.GetDiffPairWidth() / scale,
                    "diffPairGap": netclass.GetDiffPairGap() / scale,
                    "nets": nets
                }
            }

        except Exception as e:
            logger.error(f"Error creating net class: {str(e)}")
            return {
                "success": False,
                "message": "Failed to create net class",
                "errorDetails": str(e)
            }
            
    def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Add a copper pour (zone) to the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            layer = params.get("layer", "F.Cu")
            net = params.get("net")
            clearance = params.get("clearance")
            min_width = params.get("minWidth", 0.2)
            points = params.get("points", [])
            priority = params.get("priority", 0)
            fill_type = params.get("fillType", "solid")  # solid or hatched
            
            if not points or len(points) < 3:
                return {
                    "success": False,
                    "message": "Missing points",
                    "errorDetails": "At least 3 points are required for copper pour outline"
                }

            # Get layer ID
            layer_id = self.board.GetLayerID(layer)
            if layer_id < 0:
                return {
                    "success": False,
                    "message": "Invalid layer",
                    "errorDetails": f"Layer '{layer}' does not exist"
                }

            # Create zone
            zone = pcbnew.ZONE(self.board)
            zone.SetLayer(layer_id)
            
            # Set net if provided
            if net:
                netinfo = self.board.GetNetInfo()
                net_obj = netinfo.FindNet(net)
                if net_obj:
                    zone.SetNet(net_obj)
            
            # Set zone properties
            scale = 1000000  # mm to nm
            zone.SetPriority(priority)
            
            if clearance is not None:
                zone.SetLocalClearance(int(clearance * scale))
            
            zone.SetMinThickness(int(min_width * scale))
            
            # Set fill type
            if fill_type == "hatched":
                zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
            else:
                zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
            
            # Create outline
            outline = zone.Outline()
            
            # Add points to outline
            for point in points:
                scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
                x_nm = int(point["x"] * scale)
                y_nm = int(point["y"] * scale)
                outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
            
            # Add zone to board
            self.board.Add(zone)
            
            # Fill zone
            filler = pcbnew.ZONE_FILLER(self.board)
            filler.Fill(self.board.Zones())

            return {
                "success": True,
                "message": "Added copper pour",
                "pour": {
                    "layer": layer,
                    "net": net,
                    "clearance": clearance,
                    "minWidth": min_width,
                    "priority": priority,
                    "fillType": fill_type,
                    "pointCount": len(points)
                }
            }

        except Exception as e:
            logger.error(f"Error adding copper pour: {str(e)}")
            return {
                "success": False,
                "message": "Failed to add copper pour",
                "errorDetails": str(e)
            }
            
    def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Route a differential pair between two sets of points or pads"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            start_pos = params.get("startPos")
            end_pos = params.get("endPos")
            net_pos = params.get("netPos")
            net_neg = params.get("netNeg")
            layer = params.get("layer", "F.Cu")
            width = params.get("width")
            gap = params.get("gap")

            if not start_pos or not end_pos or not net_pos or not net_neg:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "startPos, endPos, netPos, and netNeg are required"
                }

            # Get layer ID
            layer_id = self.board.GetLayerID(layer)
            if layer_id < 0:
                return {
                    "success": False,
                    "message": "Invalid layer",
                    "errorDetails": f"Layer '{layer}' does not exist"
                }

            # Get nets
            netinfo = self.board.GetNetInfo()
            net_pos_obj = netinfo.FindNet(net_pos)
            net_neg_obj = netinfo.FindNet(net_neg)
            
            if not net_pos_obj or not net_neg_obj:
                return {
                    "success": False,
                    "message": "Nets not found",
                    "errorDetails": "One or both nets specified for the differential pair do not exist"
                }

            # Get start and end points
            start_point = self._get_point(start_pos)
            end_point = self._get_point(end_pos)
            
            # Calculate offset vectors for the two traces
            # First, get the direction vector from start to end
            dx = end_point.x - start_point.x
            dy = end_point.y - start_point.y
            length = math.sqrt(dx * dx + dy * dy)
            
            if length <= 0:
                return {
                    "success": False,
                    "message": "Invalid points",
                    "errorDetails": "Start and end points must be different"
                }
                
            # Normalize direction vector
            dx /= length
            dy /= length
            
            # Get perpendicular vector
            px = -dy
            py = dx
            
            # Set default gap if not provided
            if gap is None:
                gap = 0.2  # mm
                
            # Convert to nm
            gap_nm = int(gap * 1000000)
            
            # Calculate offsets
            offset_x = int(px * gap_nm / 2)
            offset_y = int(py * gap_nm / 2)
            
            # Create positive and negative trace points
            pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
            pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
            neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
            neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
            
            # Create positive trace
            pos_track = pcbnew.PCB_TRACK(self.board)
            pos_track.SetStart(pos_start)
            pos_track.SetEnd(pos_end)
            pos_track.SetLayer(layer_id)
            pos_track.SetNet(net_pos_obj)
            
            # Create negative trace
            neg_track = pcbnew.PCB_TRACK(self.board)
            neg_track.SetStart(neg_start)
            neg_track.SetEnd(neg_end)
            neg_track.SetLayer(layer_id)
            neg_track.SetNet(net_neg_obj)
            
            # Set width
            if width:
                trace_width_nm = int(width * 1000000)
                pos_track.SetWidth(trace_width_nm)
                neg_track.SetWidth(trace_width_nm)
            else:
                # Get default width from design rules or net class
                trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
                pos_track.SetWidth(trace_width)
                neg_track.SetWidth(trace_width)
            
            # Add tracks to board
            self.board.Add(pos_track)
            self.board.Add(neg_track)

            return {
                "success": True,
                "message": "Added differential pair traces",
                "diffPair": {
                    "posNet": net_pos,
                    "negNet": net_neg,
                    "layer": layer,
                    "width": pos_track.GetWidth() / 1000000,
                    "gap": gap,
                    "length": length / 1000000
                }
            }

        except Exception as e:
            logger.error(f"Error routing differential pair: {str(e)}")
            return {
                "success": False,
                "message": "Failed to route differential pair",
                "errorDetails": str(e)
            }

    def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
        """Convert point specification to KiCAD point"""
        if "x" in point_spec and "y" in point_spec:
            scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
            x_nm = int(point_spec["x"] * scale)
            y_nm = int(point_spec["y"] * scale)
            return pcbnew.VECTOR2I(x_nm, y_nm)
        elif "pad" in point_spec and "componentRef" in point_spec:
            module = self.board.FindFootprintByReference(point_spec["componentRef"])
            if module:
                pad = module.FindPadByName(point_spec["pad"])
                if pad:
                    return pad.GetPosition()
        raise ValueError("Invalid point specification")

    def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
        """Calculate distance from point to track segment"""
        start = track.GetStart()
        end = track.GetEnd()
        
        # Vector from start to end
        v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
        # Vector from start to point
        w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
        
        # Length of track squared
        c1 = v.x * v.x + v.y * v.y
        if c1 == 0:
            return self._point_distance(point, start)
            
        # Projection coefficient
        c2 = float(w.x * v.x + w.y * v.y) / c1
        
        if c2 < 0:
            return self._point_distance(point, start)
        elif c2 > 1:
            return self._point_distance(point, end)
            
        # Point on line
        proj = pcbnew.VECTOR2I(
            int(start.x + c2 * v.x),
            int(start.y + c2 * v.y)
        )
        return self._point_distance(point, proj)

    def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
        """Calculate distance between two points"""
        dx = p1.x - p2.x
        dy = p1.y - p2.y
        return (dx * dx + dy * dy) ** 0.5

```

--------------------------------------------------------------------------------
/python/commands/component.py:
--------------------------------------------------------------------------------

```python
"""
Component-related command implementations for KiCAD interface
"""

import os
import pcbnew
import logging
import math
from typing import Dict, Any, Optional, List, Tuple
import base64

logger = logging.getLogger('kicad_interface')

class ComponentCommands:
    """Handles component-related KiCAD operations"""

    def __init__(self, board: Optional[pcbnew.BOARD] = None):
        """Initialize with optional board instance"""
        self.board = board

    def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Place a component on the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            # Get parameters
            component_id = params.get("componentId")
            position = params.get("position")
            reference = params.get("reference")
            value = params.get("value")
            footprint = params.get("footprint")
            rotation = params.get("rotation", 0)
            layer = params.get("layer", "F.Cu")

            if not component_id or not position:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "componentId and position are required"
                }

            # Create new module (footprint)
            module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {component_id}"
                }

            # Set position
            scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
            x_nm = int(position["x"] * scale)
            y_nm = int(position["y"] * scale)
            module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))

            # Set reference if provided
            if reference:
                module.SetReference(reference)

            # Set value if provided
            if value:
                module.SetValue(value)

            # Set footprint if provided
            if footprint:
                module.SetFootprintName(footprint)

            # Set rotation
            module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees

            # Set layer
            layer_id = self.board.GetLayerID(layer)
            if layer_id >= 0:
                module.SetLayer(layer_id)

            # Add to board
            self.board.Add(module)

            return {
                "success": True,
                "message": f"Placed component: {component_id}",
                "component": {
                    "reference": module.GetReference(),
                    "value": module.GetValue(),
                    "position": {
                        "x": position["x"],
                        "y": position["y"],
                        "unit": position["unit"]
                    },
                    "rotation": rotation,
                    "layer": layer
                }
            }

        except Exception as e:
            logger.error(f"Error placing component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to place component",
                "errorDetails": str(e)
            }

    def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Move an existing component to a new position"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            position = params.get("position")
            rotation = params.get("rotation")

            if not reference or not position:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "reference and position are required"
                }

            # Find the component
            module = self.board.FindFootprintByReference(reference)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }

            # Set new position
            scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
            x_nm = int(position["x"] * scale)
            y_nm = int(position["y"] * scale)
            module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))

            # Set new rotation if provided
            if rotation is not None:
                module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees

            return {
                "success": True,
                "message": f"Moved component: {reference}",
                "component": {
                    "reference": reference,
                    "position": {
                        "x": position["x"],
                        "y": position["y"],
                        "unit": position["unit"]
                    },
                    "rotation": rotation if rotation is not None else module.GetOrientation() / 10
                }
            }

        except Exception as e:
            logger.error(f"Error moving component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to move component",
                "errorDetails": str(e)
            }

    def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Rotate an existing component"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            angle = params.get("angle")

            if not reference or angle is None:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "reference and angle are required"
                }

            # Find the component
            module = self.board.FindFootprintByReference(reference)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }

            # Set rotation
            module.SetOrientation(angle * 10)  # KiCAD uses decidegrees

            return {
                "success": True,
                "message": f"Rotated component: {reference}",
                "component": {
                    "reference": reference,
                    "rotation": angle
                }
            }

        except Exception as e:
            logger.error(f"Error rotating component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to rotate component",
                "errorDetails": str(e)
            }

    def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Delete a component from the PCB"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            if not reference:
                return {
                    "success": False,
                    "message": "Missing reference",
                    "errorDetails": "reference parameter is required"
                }

            # Find the component
            module = self.board.FindFootprintByReference(reference)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }

            # Remove from board
            self.board.Remove(module)

            return {
                "success": True,
                "message": f"Deleted component: {reference}"
            }

        except Exception as e:
            logger.error(f"Error deleting component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to delete component",
                "errorDetails": str(e)
            }

    def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Edit the properties of an existing component"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            new_reference = params.get("newReference")
            value = params.get("value")
            footprint = params.get("footprint")

            if not reference:
                return {
                    "success": False,
                    "message": "Missing reference",
                    "errorDetails": "reference parameter is required"
                }

            # Find the component
            module = self.board.FindFootprintByReference(reference)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }

            # Update properties
            if new_reference:
                module.SetReference(new_reference)
            if value:
                module.SetValue(value)
            if footprint:
                module.SetFootprintName(footprint)

            return {
                "success": True,
                "message": f"Updated component: {reference}",
                "component": {
                    "reference": new_reference or reference,
                    "value": value or module.GetValue(),
                    "footprint": footprint or module.GetFootprintName()
                }
            }

        except Exception as e:
            logger.error(f"Error editing component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to edit component",
                "errorDetails": str(e)
            }

    def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Get detailed properties of a component"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            if not reference:
                return {
                    "success": False,
                    "message": "Missing reference",
                    "errorDetails": "reference parameter is required"
                }

            # Find the component
            module = self.board.FindFootprintByReference(reference)
            if not module:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }

            # Get position in mm
            pos = module.GetPosition()
            x_mm = pos.x / 1000000
            y_mm = pos.y / 1000000

            return {
                "success": True,
                "component": {
                    "reference": module.GetReference(),
                    "value": module.GetValue(),
                    "footprint": module.GetFootprintName(),
                    "position": {
                        "x": x_mm,
                        "y": y_mm,
                        "unit": "mm"
                    },
                    "rotation": module.GetOrientation() / 10,
                    "layer": self.board.GetLayerName(module.GetLayer()),
                    "attributes": {
                        "smd": module.GetAttributes() & pcbnew.FP_SMD,
                        "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
                        "virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL
                    }
                }
            }

        except Exception as e:
            logger.error(f"Error getting component properties: {str(e)}")
            return {
                "success": False,
                "message": "Failed to get component properties",
                "errorDetails": str(e)
            }

    def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Get a list of all components on the board"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            components = []
            for module in self.board.GetFootprints():
                pos = module.GetPosition()
                x_mm = pos.x / 1000000
                y_mm = pos.y / 1000000

                components.append({
                    "reference": module.GetReference(),
                    "value": module.GetValue(),
                    "footprint": module.GetFootprintName(),
                    "position": {
                        "x": x_mm,
                        "y": y_mm,
                        "unit": "mm"
                    },
                    "rotation": module.GetOrientation() / 10,
                    "layer": self.board.GetLayerName(module.GetLayer())
                })

            return {
                "success": True,
                "components": components
            }

        except Exception as e:
            logger.error(f"Error getting component list: {str(e)}")
            return {
                "success": False,
                "message": "Failed to get component list",
                "errorDetails": str(e)
            }
            
    def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Place an array of components in a grid or circular pattern"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            component_id = params.get("componentId")
            pattern = params.get("pattern", "grid")  # grid or circular
            count = params.get("count")
            reference_prefix = params.get("referencePrefix", "U")
            value = params.get("value")
            
            if not component_id or not count:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "componentId and count are required"
                }
                
            if pattern == "grid":
                start_position = params.get("startPosition")
                rows = params.get("rows")
                columns = params.get("columns")
                spacing_x = params.get("spacingX")
                spacing_y = params.get("spacingY")
                rotation = params.get("rotation", 0)
                layer = params.get("layer", "F.Cu")
                
                if not start_position or not rows or not columns or not spacing_x or not spacing_y:
                    return {
                        "success": False,
                        "message": "Missing grid parameters",
                        "errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
                    }
                    
                if rows * columns != count:
                    return {
                        "success": False,
                        "message": "Invalid grid parameters",
                        "errorDetails": "rows * columns must equal count"
                    }
                    
                placed_components = self._place_grid_array(
                    component_id,
                    start_position,
                    rows,
                    columns,
                    spacing_x,
                    spacing_y,
                    reference_prefix,
                    value,
                    rotation,
                    layer
                )
                
            elif pattern == "circular":
                center = params.get("center")
                radius = params.get("radius")
                angle_start = params.get("angleStart", 0)
                angle_step = params.get("angleStep")
                rotation_offset = params.get("rotationOffset", 0)
                layer = params.get("layer", "F.Cu")
                
                if not center or not radius or not angle_step:
                    return {
                        "success": False,
                        "message": "Missing circular parameters",
                        "errorDetails": "For circular pattern, center, radius, and angleStep are required"
                    }
                    
                placed_components = self._place_circular_array(
                    component_id,
                    center,
                    radius,
                    count,
                    angle_start,
                    angle_step,
                    reference_prefix,
                    value,
                    rotation_offset,
                    layer
                )
                
            else:
                return {
                    "success": False,
                    "message": "Invalid pattern",
                    "errorDetails": "Pattern must be 'grid' or 'circular'"
                }

            return {
                "success": True,
                "message": f"Placed {count} components in {pattern} pattern",
                "components": placed_components
            }

        except Exception as e:
            logger.error(f"Error placing component array: {str(e)}")
            return {
                "success": False,
                "message": "Failed to place component array",
                "errorDetails": str(e)
            }
            
    def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Align multiple components along a line or distribute them evenly"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            references = params.get("references", [])
            alignment = params.get("alignment", "horizontal")  # horizontal, vertical, or edge
            distribution = params.get("distribution", "none")  # none, equal, or spacing
            spacing = params.get("spacing")
            
            if not references or len(references) < 2:
                return {
                    "success": False,
                    "message": "Missing references",
                    "errorDetails": "At least two component references are required"
                }
                
            # Find all referenced components
            components = []
            for ref in references:
                module = self.board.FindFootprintByReference(ref)
                if not module:
                    return {
                        "success": False,
                        "message": "Component not found",
                        "errorDetails": f"Could not find component: {ref}"
                    }
                components.append(module)
            
            # Perform alignment based on selected option
            if alignment == "horizontal":
                self._align_components_horizontally(components, distribution, spacing)
            elif alignment == "vertical":
                self._align_components_vertically(components, distribution, spacing)
            elif alignment == "edge":
                edge = params.get("edge")
                if not edge:
                    return {
                        "success": False,
                        "message": "Missing edge parameter",
                        "errorDetails": "Edge parameter is required for edge alignment"
                    }
                self._align_components_to_edge(components, edge)
            else:
                return {
                    "success": False,
                    "message": "Invalid alignment option",
                    "errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
                }

            # Prepare result data
            aligned_components = []
            for module in components:
                pos = module.GetPosition()
                aligned_components.append({
                    "reference": module.GetReference(),
                    "position": {
                        "x": pos.x / 1000000,
                        "y": pos.y / 1000000,
                        "unit": "mm"
                    },
                    "rotation": module.GetOrientation() / 10
                })

            return {
                "success": True,
                "message": f"Aligned {len(components)} components",
                "alignment": alignment,
                "distribution": distribution,
                "components": aligned_components
            }

        except Exception as e:
            logger.error(f"Error aligning components: {str(e)}")
            return {
                "success": False,
                "message": "Failed to align components",
                "errorDetails": str(e)
            }
            
    def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
        """Duplicate an existing component"""
        try:
            if not self.board:
                return {
                    "success": False,
                    "message": "No board is loaded",
                    "errorDetails": "Load or create a board first"
                }

            reference = params.get("reference")
            new_reference = params.get("newReference")
            position = params.get("position")
            rotation = params.get("rotation")
            
            if not reference or not new_reference:
                return {
                    "success": False,
                    "message": "Missing parameters",
                    "errorDetails": "reference and newReference are required"
                }
                
            # Find the source component
            source = self.board.FindFootprintByReference(reference)
            if not source:
                return {
                    "success": False,
                    "message": "Component not found",
                    "errorDetails": f"Could not find component: {reference}"
                }
                
            # Check if new reference already exists
            if self.board.FindFootprintByReference(new_reference):
                return {
                    "success": False,
                    "message": "Reference already exists",
                    "errorDetails": f"A component with reference {new_reference} already exists"
                }
                
            # Create new footprint with the same properties
            new_module = pcbnew.FOOTPRINT(self.board)
            new_module.SetFootprintName(source.GetFootprintName())
            new_module.SetValue(source.GetValue())
            new_module.SetReference(new_reference)
            new_module.SetLayer(source.GetLayer())
            
            # Copy pads and other items
            for pad in source.Pads():
                new_pad = pcbnew.PAD(new_module)
                new_pad.Copy(pad)
                new_module.Add(new_pad)
                
            # Set position if provided, otherwise use offset from original
            if position:
                scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
                x_nm = int(position["x"] * scale)
                y_nm = int(position["y"] * scale)
                new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
            else:
                # Offset by 5mm
                source_pos = source.GetPosition()
                new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
                
            # Set rotation if provided, otherwise use same as original
            if rotation is not None:
                new_module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees
            else:
                new_module.SetOrientation(source.GetOrientation())
                
            # Add to board
            self.board.Add(new_module)
            
            # Get final position in mm
            pos = new_module.GetPosition()

            return {
                "success": True,
                "message": f"Duplicated component {reference} to {new_reference}",
                "component": {
                    "reference": new_reference,
                    "value": new_module.GetValue(),
                    "footprint": new_module.GetFootprintName(),
                    "position": {
                        "x": pos.x / 1000000,
                        "y": pos.y / 1000000,
                        "unit": "mm"
                    },
                    "rotation": new_module.GetOrientation() / 10,
                    "layer": self.board.GetLayerName(new_module.GetLayer())
                }
            }

        except Exception as e:
            logger.error(f"Error duplicating component: {str(e)}")
            return {
                "success": False,
                "message": "Failed to duplicate component",
                "errorDetails": str(e)
            }
            
    def _place_grid_array(self, component_id: str, start_position: Dict[str, Any], 
                       rows: int, columns: int, spacing_x: float, spacing_y: float,
                       reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
        """Place components in a grid pattern and return the list of placed components"""
        placed = []
        
        # Convert spacing to nm
        unit = start_position.get("unit", "mm")
        scale = 1000000 if unit == "mm" else 25400000  # mm or inch to nm
        spacing_x_nm = int(spacing_x * scale)
        spacing_y_nm = int(spacing_y * scale)
        
        # Get layer ID
        layer_id = self.board.GetLayerID(layer)
        
        for row in range(rows):
            for col in range(columns):
                # Calculate position
                x = start_position["x"] + (col * spacing_x)
                y = start_position["y"] + (row * spacing_y)
                
                # Generate reference
                index = row * columns + col + 1
                component_reference = f"{reference_prefix}{index}"
                
                # Place component
                result = self.place_component({
                    "componentId": component_id,
                    "position": {"x": x, "y": y, "unit": unit},
                    "reference": component_reference,
                    "value": value,
                    "rotation": rotation,
                    "layer": layer
                })
                
                if result["success"]:
                    placed.append(result["component"])
                
        return placed
        
    def _place_circular_array(self, component_id: str, center: Dict[str, Any], 
                          radius: float, count: int, angle_start: float, 
                          angle_step: float, reference_prefix: str, 
                          value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
        """Place components in a circular pattern and return the list of placed components"""
        placed = []
        
        # Get unit
        unit = center.get("unit", "mm")
        
        for i in range(count):
            # Calculate angle for this component
            angle = angle_start + (i * angle_step)
            angle_rad = math.radians(angle)
            
            # Calculate position
            x = center["x"] + (radius * math.cos(angle_rad))
            y = center["y"] + (radius * math.sin(angle_rad))
            
            # Generate reference
            component_reference = f"{reference_prefix}{i+1}"
            
            # Calculate rotation (pointing outward from center)
            component_rotation = angle + rotation_offset
            
            # Place component
            result = self.place_component({
                "componentId": component_id,
                "position": {"x": x, "y": y, "unit": unit},
                "reference": component_reference,
                "value": value,
                "rotation": component_rotation,
                "layer": layer
            })
            
            if result["success"]:
                placed.append(result["component"])
                
        return placed
        
    def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT], 
                                   distribution: str, spacing: Optional[float]) -> None:
        """Align components horizontally and optionally distribute them"""
        if not components:
            return
            
        # Find the average Y coordinate
        y_sum = sum(module.GetPosition().y for module in components)
        y_avg = y_sum // len(components)
        
        # Sort components by X position
        components.sort(key=lambda m: m.GetPosition().x)
        
        # Set Y coordinate for all components
        for module in components:
            pos = module.GetPosition()
            module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
            
        # Handle distribution if requested
        if distribution == "equal" and len(components) > 1:
            # Get leftmost and rightmost X coordinates
            x_min = components[0].GetPosition().x
            x_max = components[-1].GetPosition().x
            
            # Calculate equal spacing
            total_space = x_max - x_min
            spacing_nm = total_space // (len(components) - 1)
            
            # Set X positions with equal spacing
            for i in range(1, len(components) - 1):
                pos = components[i].GetPosition()
                new_x = x_min + (i * spacing_nm)
                components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
                
        elif distribution == "spacing" and spacing is not None:
            # Convert spacing to nanometers
            spacing_nm = int(spacing * 1000000)  # assuming mm
            
            # Set X positions with the specified spacing
            x_current = components[0].GetPosition().x
            for i in range(1, len(components)):
                pos = components[i].GetPosition()
                x_current += spacing_nm
                components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
                
    def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT], 
                                 distribution: str, spacing: Optional[float]) -> None:
        """Align components vertically and optionally distribute them"""
        if not components:
            return
            
        # Find the average X coordinate
        x_sum = sum(module.GetPosition().x for module in components)
        x_avg = x_sum // len(components)
        
        # Sort components by Y position
        components.sort(key=lambda m: m.GetPosition().y)
        
        # Set X coordinate for all components
        for module in components:
            pos = module.GetPosition()
            module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
            
        # Handle distribution if requested
        if distribution == "equal" and len(components) > 1:
            # Get topmost and bottommost Y coordinates
            y_min = components[0].GetPosition().y
            y_max = components[-1].GetPosition().y
            
            # Calculate equal spacing
            total_space = y_max - y_min
            spacing_nm = total_space // (len(components) - 1)
            
            # Set Y positions with equal spacing
            for i in range(1, len(components) - 1):
                pos = components[i].GetPosition()
                new_y = y_min + (i * spacing_nm)
                components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
                
        elif distribution == "spacing" and spacing is not None:
            # Convert spacing to nanometers
            spacing_nm = int(spacing * 1000000)  # assuming mm
            
            # Set Y positions with the specified spacing
            y_current = components[0].GetPosition().y
            for i in range(1, len(components)):
                pos = components[i].GetPosition()
                y_current += spacing_nm
                components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
                
    def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
        """Align components to the specified edge of the board"""
        if not components:
            return
            
        # Get board bounds
        board_box = self.board.GetBoardEdgesBoundingBox()
        left = board_box.GetLeft()
        right = board_box.GetRight()
        top = board_box.GetTop()
        bottom = board_box.GetBottom()
        
        # Align based on specified edge
        if edge == "left":
            for module in components:
                pos = module.GetPosition()
                module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y))  # 2mm offset from edge
        elif edge == "right":
            for module in components:
                pos = module.GetPosition()
                module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y))  # 2mm offset from edge
        elif edge == "top":
            for module in components:
                pos = module.GetPosition()
                module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000))  # 2mm offset from edge
        elif edge == "bottom":
            for module in components:
                pos = module.GetPosition()
                module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000))  # 2mm offset from edge
        else:
            logger.warning(f"Unknown edge alignment: {edge}")

```
Page 3/3FirstPrevNextLast