""" Component-related command implementations for KiCAD interface """ import pcbnew import logging import math from typing import Dict, Any, Optional, List, Tuple import base64 import io from commands.library import LibraryManager from PIL import Image, ImageDraw logger = logging.getLogger('kicad_interface') class ComponentCommands: """Handles component-related KiCAD operations""" def __init__(self, board: Optional[pcbnew.BOARD] = None, library_manager: Optional[LibraryManager] = None): """Initialize with optional board instance and library manager""" self.board = board self.library_manager = library_manager or LibraryManager() 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" } # Find footprint using library manager # component_id can be "Library:Footprint" or just "Footprint" footprint_result = self.library_manager.find_footprint(component_id) if not footprint_result: # Try to suggest similar footprints suggestions = self.library_manager.search_footprints(f"*{component_id}*", limit=5) suggestion_text = "" if suggestions: suggestion_text = "\n\nDid you mean one of these?\n" + \ "\n".join([f" - {s['full_name']}" for s in suggestions]) return { "success": False, "message": "Footprint not found", "errorDetails": f"Could not find footprint: {component_id}{suggestion_text}" } library_path, footprint_name = footprint_result # Load footprint from library # Extract library nickname from path library_nickname = None for nick, path in self.library_manager.libraries.items(): if path == library_path: library_nickname = nick break if not library_nickname: return { "success": False, "message": "Internal error", "errorDetails": "Could not determine library nickname" } # Load the footprint module = pcbnew.FootprintLoad(library_path, footprint_name) if not module: return { "success": False, "message": "Failed to load footprint", "errorDetails": f"Could not load footprint from {library_path}/{footprint_name}" } # 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 (use existing library_nickname and footprint_name) # For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName if footprint: # Parse footprint string if it's in "Library:Footprint" format if ":" in footprint: lib_name, fp_name = footprint.split(":", 1) else: # Use the library_nickname we already have from loading lib_name = library_nickname fp_name = footprint fpid = pcbnew.LIB_ID(lib_name, fp_name) module.SetFPID(fpid) else: # Use the footprint we just loaded fpid = pcbnew.LIB_ID(library_nickname, footprint_name) module.SetFPID(fpid) # Set rotation (KiCAD 9.0 uses EDA_ANGLE) angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T) module.SetOrientation(angle) # 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: angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T) module.SetOrientation(angle) 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().AsDegrees() } } 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 rotation_angle = pcbnew.EDA_ANGLE(angle, pcbnew.DEGREES_T) module.SetOrientation(rotation_angle) 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: # For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName # Parse footprint string (format: "Library:Footprint") if ":" in footprint: lib_name, fp_name = footprint.split(":", 1) fpid = pcbnew.LIB_ID(lib_name, fp_name) module.SetFPID(fpid) else: # If no library specified, keep existing library current_fpid = module.GetFPID() lib_name = current_fpid.GetLibNickname().GetUTF8() fpid = pcbnew.LIB_ID(lib_name, footprint) module.SetFPID(fpid) return { "success": True, "message": f"Updated component: {reference}", "component": { "reference": new_reference or reference, "value": value or module.GetValue(), "footprint": footprint or module.GetFPIDAsString() } } 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.GetFPIDAsString(), "position": { "x": x_mm, "y": y_mm, "unit": "mm" }, "rotation": module.GetOrientation().AsDegrees(), "layer": self.board.GetLayerName(module.GetLayer()), "attributes": { "smd": module.GetAttributes() & pcbnew.FP_SMD, "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE, "board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY } } } 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.GetFPIDAsString(), "position": { "x": x_mm, "y": y_mm, "unit": "mm" }, "rotation": module.GetOrientation().AsDegrees(), "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 find_component(self, params: Dict[str, Any]) -> Dict[str, Any]: """Find components matching search criteria (reference, value, or footprint pattern)""" try: if not self.board: return { "success": False, "message": "No board is loaded", "errorDetails": "Load or create a board first" } # Get search parameters reference_pattern = params.get("reference", "").lower() value_pattern = params.get("value", "").lower() footprint_pattern = params.get("footprint", "").lower() if not reference_pattern and not value_pattern and not footprint_pattern: return { "success": False, "message": "Missing search criteria", "errorDetails": "At least one of reference, value, or footprint pattern is required" } matches = [] for module in self.board.GetFootprints(): ref = module.GetReference().lower() val = module.GetValue().lower() fp = module.GetFPIDAsString().lower() # Check if component matches all provided patterns match = True if reference_pattern and reference_pattern not in ref: match = False if value_pattern and value_pattern not in val: match = False if footprint_pattern and footprint_pattern not in fp: match = False if match: pos = module.GetPosition() matches.append({ "reference": module.GetReference(), "value": module.GetValue(), "footprint": module.GetFPIDAsString(), "position": { "x": pos.x / 1000000, "y": pos.y / 1000000, "unit": "mm" }, "rotation": module.GetOrientation().AsDegrees(), "layer": self.board.GetLayerName(module.GetLayer()) }) return { "success": True, "matchCount": len(matches), "components": matches } except Exception as e: logger.error(f"Error finding components: {str(e)}") return { "success": False, "message": "Failed to find components", "errorDetails": str(e) } def get_component_pads(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get all pads for a component with their positions and net connections""" 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}" } pads = [] for pad in module.Pads(): pos = pad.GetPosition() size = pad.GetSize() # Get pad shape as string shape_map = { pcbnew.PAD_SHAPE_CIRCLE: "circle", pcbnew.PAD_SHAPE_RECT: "rect", pcbnew.PAD_SHAPE_OVAL: "oval", pcbnew.PAD_SHAPE_TRAPEZOID: "trapezoid", pcbnew.PAD_SHAPE_ROUNDRECT: "roundrect", pcbnew.PAD_SHAPE_CHAMFERED_RECT: "chamfered_rect", pcbnew.PAD_SHAPE_CUSTOM: "custom" } shape = shape_map.get(pad.GetShape(), "unknown") # Get pad type type_map = { pcbnew.PAD_ATTRIB_PTH: "through_hole", pcbnew.PAD_ATTRIB_SMD: "smd", pcbnew.PAD_ATTRIB_CONN: "connector", pcbnew.PAD_ATTRIB_NPTH: "npth" } pad_type = type_map.get(pad.GetAttribute(), "unknown") pads.append({ "name": pad.GetName(), "number": pad.GetNumber(), "position": { "x": pos.x / 1000000, "y": pos.y / 1000000, "unit": "mm" }, "net": pad.GetNetname(), "netCode": pad.GetNetCode(), "shape": shape, "type": pad_type, "size": { "x": size.x / 1000000, "y": size.y / 1000000, "unit": "mm" }, "drillSize": pad.GetDrillSize().x / 1000000 if pad.GetDrillSize().x > 0 else None }) # Get component position for reference comp_pos = module.GetPosition() return { "success": True, "reference": reference, "componentPosition": { "x": comp_pos.x / 1000000, "y": comp_pos.y / 1000000, "unit": "mm" }, "padCount": len(pads), "pads": pads } except Exception as e: logger.error(f"Error getting component pads: {str(e)}") return { "success": False, "message": "Failed to get component pads", "errorDetails": str(e) } def get_pad_position(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get the position of a specific pad on 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") pad_name = params.get("padName") or params.get("padNumber") if not reference: return { "success": False, "message": "Missing reference", "errorDetails": "reference parameter is required" } if not pad_name: return { "success": False, "message": "Missing pad identifier", "errorDetails": "padName or padNumber 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}" } # Find the specific pad pad = module.FindPadByNumber(str(pad_name)) if not pad: # List available pads in error message available_pads = [p.GetNumber() for p in module.Pads()] return { "success": False, "message": "Pad not found", "errorDetails": f"Pad '{pad_name}' not found on {reference}. Available pads: {', '.join(available_pads)}" } pos = pad.GetPosition() size = pad.GetSize() return { "success": True, "reference": reference, "padName": pad.GetNumber(), "position": { "x": pos.x / 1000000, "y": pos.y / 1000000, "unit": "mm" }, "net": pad.GetNetname(), "netCode": pad.GetNetCode(), "size": { "x": size.x / 1000000, "y": size.y / 1000000, "unit": "mm" } } except Exception as e: logger.error(f"Error getting pad position: {str(e)}") return { "success": False, "message": "Failed to get pad position", "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().AsDegrees() }) 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) # For KiCAD 9.x compatibility, use SetFPID instead of SetFootprintName new_module.SetFPID(source.GetFPID()) 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: rotation_angle = pcbnew.EDA_ANGLE(rotation, pcbnew.DEGREES_T) new_module.SetOrientation(rotation_angle) 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.GetFPIDAsString(), "position": { "x": pos.x / 1000000, "y": pos.y / 1000000, "unit": "mm" }, "rotation": new_module.GetOrientation().AsDegrees(), "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 add_component_annotation(self, params: Dict[str, Any]) -> Dict[str, Any]: """Add a text annotation close to a component footprint.""" try: if not self.board: return { "success": False, "message": "No board is loaded", "errorDetails": "Load or create a board first", } reference = params.get("reference") annotation = params.get("annotation") visible = params.get("visible", True) if not reference or not annotation: return { "success": False, "message": "Missing parameters", "errorDetails": "reference and annotation are required", } module = self.board.FindFootprintByReference(reference) if not module: return { "success": False, "message": "Component not found", "errorDetails": f"Could not find component: {reference}", } bbox = module.GetBoundingBox() layer_name = "B.SilkS" if self.board.GetLayerName(module.GetLayer()).startswith("B.") else "F.SilkS" layer_id = self.board.GetLayerID(layer_name) text_item = pcbnew.PCB_TEXT(self.board) text_item.SetText(annotation) text_item.SetLayer(layer_id) text_item.SetPosition( pcbnew.VECTOR2I( bbox.GetRight() + 800000, bbox.GetTop() - 800000, ) ) text_item.SetTextSize(pcbnew.VECTOR2I(1000000, 1000000)) text_item.SetTextThickness(150000) if hasattr(text_item, "SetVisible"): text_item.SetVisible(bool(visible)) self.board.Add(text_item) return { "success": True, "message": f"Added annotation to {reference}", "annotation": { "reference": reference, "text": annotation, "layer": layer_name, "visible": bool(visible), }, } except Exception as e: logger.error(f"Error adding component annotation: {str(e)}") return { "success": False, "message": "Failed to add component annotation", "errorDetails": str(e), } def group_components(self, params: Dict[str, Any]) -> Dict[str, Any]: """Create or extend a real KiCad PCB group with the selected components.""" try: if not self.board: return { "success": False, "message": "No board is loaded", "errorDetails": "Load or create a board first", } references = params.get("references", []) group_name = params.get("groupName") if not references or not group_name: return { "success": False, "message": "Missing parameters", "errorDetails": "references and groupName are required", } modules = [] missing = [] for reference in references: module = self.board.FindFootprintByReference(reference) if not module: missing.append(reference) continue modules.append(module) if not modules: return { "success": False, "message": "No valid components found", "errorDetails": f"Missing references: {', '.join(missing)}", } group = None if hasattr(self.board, "Groups"): for existing in self.board.Groups(): name = existing.GetName() if hasattr(existing, "GetName") else "" if name == group_name: group = existing break if group is None: group = pcbnew.PCB_GROUP(self.board) if hasattr(group, "SetName"): group.SetName(group_name) self.board.Add(group) for module in modules: group.AddItem(module) if hasattr(module, "SetParentGroup"): module.SetParentGroup(group) return { "success": True, "message": f"Grouped {len(modules)} component(s) into {group_name}", "group": { "name": group_name, "references": [module.GetReference() for module in modules], "missing": missing, }, } except Exception as e: logger.error(f"Error grouping components: {str(e)}") return { "success": False, "message": "Failed to group components", "errorDetails": str(e), } def replace_component(self, params: Dict[str, Any]) -> Dict[str, Any]: """Replace a component footprint while preserving placement and reference.""" 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_component_id = params.get("newComponentId") new_footprint = params.get("newFootprint") new_value = params.get("newValue") if not reference or not new_component_id: return { "success": False, "message": "Missing parameters", "errorDetails": "reference and newComponentId are required", } module = self.board.FindFootprintByReference(reference) if not module: return { "success": False, "message": "Component not found", "errorDetails": f"Could not find component: {reference}", } pos = module.GetPosition() layer = self.board.GetLayerName(module.GetLayer()) rotation = module.GetOrientation().AsDegrees() value = new_value or module.GetValue() temp_reference = f"{reference}_TMP" replacement_spec = new_footprint or new_component_id place_result = self.place_component( { "componentId": new_component_id, "position": { "x": pos.x / 1000000, "y": pos.y / 1000000, "unit": "mm", }, "reference": temp_reference, "value": value, "footprint": replacement_spec, "rotation": rotation, "layer": layer, } ) if not place_result.get("success"): return place_result delete_result = self.delete_component({"reference": reference}) if not delete_result.get("success"): return delete_result rename_result = self.edit_component( { "reference": temp_reference, "newReference": reference, "value": value, "footprint": replacement_spec, } ) if not rename_result.get("success"): return rename_result return { "success": True, "message": f"Replaced component {reference}", "component": { "reference": reference, "newComponentId": new_component_id, "footprint": replacement_spec, "value": value, }, } except Exception as e: logger.error(f"Error replacing component: {str(e)}") return { "success": False, "message": "Failed to replace component", "errorDetails": str(e), } def get_component_connections(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get pad and net connectivity for 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", } module = self.board.FindFootprintByReference(reference) if not module: return { "success": False, "message": "Component not found", "errorDetails": f"Could not find component: {reference}", } nets = {} pads = [] for pad in module.Pads(): net_name = pad.GetNetname() or "" pad_entry = { "pad": pad.GetNumber(), "net": net_name, "position": { "x": pad.GetPosition().x / 1000000, "y": pad.GetPosition().y / 1000000, "unit": "mm", }, } pads.append(pad_entry) if net_name: nets.setdefault(net_name, []).append(pad.GetNumber()) related_components = {} for other in self.board.GetFootprints(): other_ref = other.GetReference() if other_ref == reference: continue for pad in other.Pads(): net_name = pad.GetNetname() or "" if net_name and net_name in nets: related_components.setdefault(net_name, set()).add(other_ref) return { "success": True, "reference": reference, "pads": pads, "connections": [ { "net": net_name, "pads": pad_numbers, "connectedReferences": sorted(related_components.get(net_name, set())), } for net_name, pad_numbers in sorted(nets.items()) ], } except Exception as e: logger.error(f"Error getting component connections: {str(e)}") return { "success": False, "message": "Failed to get component connections", "errorDetails": str(e), } def get_component_placement(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get placement data for 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", } placements = [] for module in self.board.GetFootprints(): bbox = module.GetBoundingBox() placements.append( { "reference": module.GetReference(), "value": module.GetValue(), "footprint": module.GetFPIDAsString(), "layer": self.board.GetLayerName(module.GetLayer()), "position": { "x": module.GetPosition().x / 1000000, "y": module.GetPosition().y / 1000000, "unit": "mm", }, "rotation": module.GetOrientation().AsDegrees(), "bounds": { "left": bbox.GetLeft() / 1000000, "top": bbox.GetTop() / 1000000, "right": bbox.GetRight() / 1000000, "bottom": bbox.GetBottom() / 1000000, "unit": "mm", }, } ) return { "success": True, "placements": placements, "count": len(placements), } except Exception as e: logger.error(f"Error getting component placement: {str(e)}") return { "success": False, "message": "Failed to get component placement", "errorDetails": str(e), } def get_component_groups(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get explicit KiCad groups, or derived prefix groups when none exist.""" try: if not self.board: return { "success": False, "message": "No board is loaded", "errorDetails": "Load or create a board first", } grouped = {} derived = {} for module in self.board.GetFootprints(): reference = module.GetReference() parent_group = module.GetParentGroup() if hasattr(module, "GetParentGroup") else None if parent_group: name = "" if hasattr(parent_group, "GetName"): name = parent_group.GetName() if not name and hasattr(parent_group, "GetFriendlyName"): name = parent_group.GetFriendlyName() name = name or "unnamed-group" grouped.setdefault(name, {"name": name, "source": "board", "references": []}) grouped[name]["references"].append(reference) else: prefix = "".join(ch for ch in reference if ch.isalpha()) or "misc" derived.setdefault(prefix, {"name": prefix, "source": "derived-prefix", "references": []}) derived[prefix]["references"].append(reference) groups = list(grouped.values()) if grouped else list(derived.values()) for group in groups: group["references"].sort() group["count"] = len(group["references"]) return { "success": True, "groups": sorted(groups, key=lambda group: group["name"]), "count": len(groups), } except Exception as e: logger.error(f"Error getting component groups: {str(e)}") return { "success": False, "message": "Failed to get component groups", "errorDetails": str(e), } def get_component_visualization(self, params: Dict[str, Any]) -> Dict[str, Any]: """Render a lightweight top-down PNG preview for a component footprint.""" 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", } module = self.board.FindFootprintByReference(reference) if not module: return { "success": False, "message": "Component not found", "errorDetails": f"Could not find component: {reference}", } bbox = module.GetBoundingBox() margin_nm = 500000 min_x = bbox.GetLeft() - margin_nm min_y = bbox.GetTop() - margin_nm max_x = bbox.GetRight() + margin_nm max_y = bbox.GetBottom() + margin_nm width_nm = max(max_x - min_x, 1000000) height_nm = max(max_y - min_y, 1000000) canvas_size = 320 padding = 20 scale = min( (canvas_size - 2 * padding) / width_nm, (canvas_size - 2 * padding) / height_nm, ) def to_px(x_nm: int, y_nm: int) -> Tuple[int, int]: x = padding + int((x_nm - min_x) * scale) y = padding + int((y_nm - min_y) * scale) return x, y image = Image.new("RGBA", (canvas_size, canvas_size), (255, 255, 255, 0)) draw = ImageDraw.Draw(image) body_left, body_top = to_px(bbox.GetLeft(), bbox.GetTop()) body_right, body_bottom = to_px(bbox.GetRight(), bbox.GetBottom()) draw.rounded_rectangle( [body_left, body_top, body_right, body_bottom], radius=10, outline=(36, 74, 124, 255), width=3, fill=(230, 238, 250, 180), ) for pad in module.Pads(): pad_box = pad.GetBoundingBox() x0, y0 = to_px(pad_box.GetLeft(), pad_box.GetTop()) x1, y1 = to_px(pad_box.GetRight(), pad_box.GetBottom()) shape = pad.GetShape() circle_shapes = { getattr(pcbnew, "PAD_SHAPE_CIRCLE", object()), getattr(pcbnew, "PAD_SHAPE_OVAL", object()), } if shape in circle_shapes: draw.ellipse([x0, y0, x1, y1], fill=(201, 96, 74, 255), outline=(122, 41, 27, 255)) else: draw.rounded_rectangle( [x0, y0, x1, y1], radius=4, fill=(201, 96, 74, 255), outline=(122, 41, 27, 255), ) draw.text((16, 12), reference, fill=(22, 22, 22, 255)) buffer = io.BytesIO() image.save(buffer, format="PNG") return { "success": True, "imageData": base64.b64encode(buffer.getvalue()).decode("utf-8"), "format": "png", "reference": reference, } except Exception as e: logger.error(f"Error getting component visualization: {str(e)}") return { "success": False, "message": "Failed to get component visualization", "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 int(spacing_x * scale) int(spacing_y * scale) # Get 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}")