Adds an optional `layer` parameter (e.g., 'F.Cu', 'B.Cu') to the move_component tool. When specified, the component is flipped to the target layer if it's not already on it. The response now also includes the component's layer after the move. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1236 lines
49 KiB
Python
1236 lines
49 KiB
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
|
|
from commands.library import LibraryManager
|
|
|
|
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 for F.Cu (or non-B.Cu) before adding to board
|
|
if layer != "B.Cu":
|
|
layer_id = self.board.GetLayerID(layer)
|
|
if layer_id >= 0:
|
|
module.SetLayer(layer_id)
|
|
|
|
# Add to board first — Flip() requires board context in KiCAD 9
|
|
self.board.Add(module)
|
|
|
|
# Flip to B.Cu after add (board context needed, otherwise hangs 30s)
|
|
if layer == "B.Cu":
|
|
if not module.IsFlipped():
|
|
module.Flip(module.GetPosition(), False)
|
|
|
|
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")
|
|
layer = params.get("layer")
|
|
|
|
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)
|
|
|
|
# Flip to target layer if specified
|
|
if layer:
|
|
current_layer = self.board.GetLayerName(module.GetLayer())
|
|
if layer == "B.Cu" and current_layer != "B.Cu":
|
|
module.Flip(module.GetPosition(), False)
|
|
elif layer == "F.Cu" and current_layer != "F.Cu":
|
|
module.Flip(module.GetPosition(), False)
|
|
|
|
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(),
|
|
"layer": self.board.GetLayerName(module.GetLayer())
|
|
}
|
|
}
|
|
|
|
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 _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}")
|