FootprintLoad+Flip() on standalone footprint (not yet in board) causes a 30-second block in KiCAD 9 pcbnew. Solution: add footprint to board first, then call Flip() with board context available. Fixes B.Cu placement timeout that killed the Python process.
1226 lines
48 KiB
Python
1226 lines
48 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")
|
|
|
|
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 _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}")
|