feat: Week 1 complete - Linux support + IPC API prep
🎉 Major v2.0 rebuild kickoff - Week 1 accomplished! ## Highlights ### Cross-Platform Support 🌍 - ✅ Linux primary platform (Ubuntu/Debian tested) - ✅ Windows fully supported - ✅ macOS experimental support - ✅ Platform-agnostic path handling (XDG spec) - ✅ Auto-detection of KiCAD installation ### Infrastructure 🏗️ - ✅ GitHub Actions CI/CD pipeline - ✅ Pytest framework with 20+ tests - ✅ Pre-commit hooks (Black, MyPy, ESLint) - ✅ Automated Linux installation script - ✅ Enhanced npm scripts ### IPC API Migration Prep 🚀 - ✅ Comprehensive migration plan (30 pages) - ✅ Backend abstraction layer (800+ lines) - ✅ Factory pattern with auto-detection - ✅ SWIG backward compatibility wrapper - ✅ IPC backend skeleton ready ### Documentation 📚 - ✅ Updated README (Linux installation) - ✅ CONTRIBUTING.md guide - ✅ Linux compatibility audit - ✅ IPC API migration plan - ✅ Session summaries - ✅ Platform-specific config templates ## Files Changed - 27 files created - ~3,000 lines of code/docs - 8 comprehensive documentation pages - 20+ unit tests - 5 abstraction layer modules ## Next Steps - Week 2: IPC API migration (project.py → component.py → routing.py) - Migrate from deprecated SWIG to official IPC API - JLCPCB/Digikey integration prep 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
19
python/commands/__init__.py
Normal file
19
python/commands/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
KiCAD command implementations package
|
||||
"""
|
||||
|
||||
from .project import ProjectCommands
|
||||
from .board import BoardCommands
|
||||
from .component import ComponentCommands
|
||||
from .routing import RoutingCommands
|
||||
from .design_rules import DesignRuleCommands
|
||||
from .export import ExportCommands
|
||||
|
||||
__all__ = [
|
||||
'ProjectCommands',
|
||||
'BoardCommands',
|
||||
'ComponentCommands',
|
||||
'RoutingCommands',
|
||||
'DesignRuleCommands',
|
||||
'ExportCommands'
|
||||
]
|
||||
11
python/commands/board.py
Normal file
11
python/commands/board.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Board-related command implementations for KiCAD interface
|
||||
|
||||
This file is maintained for backward compatibility.
|
||||
It imports and re-exports the BoardCommands class from the board package.
|
||||
"""
|
||||
|
||||
from commands.board import BoardCommands
|
||||
|
||||
# Re-export the BoardCommands class for backward compatibility
|
||||
__all__ = ['BoardCommands']
|
||||
77
python/commands/board/__init__.py
Normal file
77
python/commands/board/__init__.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Board-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Import specialized modules
|
||||
from .size import BoardSizeCommands
|
||||
from .layers import BoardLayerCommands
|
||||
from .outline import BoardOutlineCommands
|
||||
from .view import BoardViewCommands
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardCommands:
|
||||
"""Handles board-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
# Initialize specialized command classes
|
||||
self.size_commands = BoardSizeCommands(board)
|
||||
self.layer_commands = BoardLayerCommands(board)
|
||||
self.outline_commands = BoardOutlineCommands(board)
|
||||
self.view_commands = BoardViewCommands(board)
|
||||
|
||||
# Delegate board size commands
|
||||
def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the size of the PCB board"""
|
||||
self.size_commands.board = self.board
|
||||
return self.size_commands.set_board_size(params)
|
||||
|
||||
# Delegate layer commands
|
||||
def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new layer to the PCB"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.add_layer(params)
|
||||
|
||||
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the active layer for PCB operations"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.set_active_layer(params)
|
||||
|
||||
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all layers in the PCB"""
|
||||
self.layer_commands.board = self.board
|
||||
return self.layer_commands.get_layer_list(params)
|
||||
|
||||
# Delegate board outline commands
|
||||
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a board outline to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_board_outline(params)
|
||||
|
||||
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a mounting hole to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_mounting_hole(params)
|
||||
|
||||
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add text annotation to the PCB"""
|
||||
self.outline_commands.board = self.board
|
||||
return self.outline_commands.add_text(params)
|
||||
|
||||
# Delegate view commands
|
||||
def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current board"""
|
||||
self.view_commands.board = self.board
|
||||
return self.view_commands.get_board_info(params)
|
||||
|
||||
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a 2D image of the PCB"""
|
||||
self.view_commands.board = self.board
|
||||
return self.view_commands.get_board_2d_view(params)
|
||||
190
python/commands/board/layers.py
Normal file
190
python/commands/board/layers.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Board layer command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardLayerCommands:
|
||||
"""Handles board layer operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new layer to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
layer_type = params.get("type")
|
||||
position = params.get("position")
|
||||
number = params.get("number")
|
||||
|
||||
if not name or not layer_type or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "name, type, and position are required"
|
||||
}
|
||||
|
||||
# Get layer stack
|
||||
layer_stack = self.board.GetLayerStack()
|
||||
|
||||
# Determine layer ID based on position and number
|
||||
layer_id = None
|
||||
if position == "inner":
|
||||
if number is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing layer number",
|
||||
"errorDetails": "number is required for inner layers"
|
||||
}
|
||||
layer_id = pcbnew.In1_Cu + (number - 1)
|
||||
elif position == "top":
|
||||
layer_id = pcbnew.F_Cu
|
||||
elif position == "bottom":
|
||||
layer_id = pcbnew.B_Cu
|
||||
|
||||
if layer_id is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer position",
|
||||
"errorDetails": "position must be 'top', 'bottom', or 'inner'"
|
||||
}
|
||||
|
||||
# Set layer properties
|
||||
layer_stack.SetLayerName(layer_id, name)
|
||||
layer_stack.SetLayerType(layer_id, self._get_layer_type(layer_type))
|
||||
|
||||
# Enable the layer
|
||||
self.board.SetLayerEnabled(layer_id, True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added layer: {name}",
|
||||
"layer": {
|
||||
"name": name,
|
||||
"type": layer_type,
|
||||
"position": position,
|
||||
"number": number
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding layer: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add layer",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def set_active_layer(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the active layer for PCB operations"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layer = params.get("layer")
|
||||
if not layer:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No layer specified",
|
||||
"errorDetails": "layer parameter is required"
|
||||
}
|
||||
|
||||
# Find layer ID by name
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Layer not found",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Set active layer
|
||||
self.board.SetActiveLayer(layer_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Set active layer to: {layer}",
|
||||
"layer": {
|
||||
"name": layer,
|
||||
"id": layer_id
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting active layer: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set active layer",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_layer_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all layers in the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layers = []
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layers.append({
|
||||
"name": self.board.GetLayerName(layer_id),
|
||||
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
|
||||
"id": layer_id,
|
||||
"isActive": layer_id == self.board.GetActiveLayer()
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"layers": layers
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting layer list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get layer list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_layer_type(self, type_name: str) -> int:
|
||||
"""Convert layer type name to KiCAD layer type constant"""
|
||||
type_map = {
|
||||
"copper": pcbnew.LT_SIGNAL,
|
||||
"technical": pcbnew.LT_SIGNAL,
|
||||
"user": pcbnew.LT_USER,
|
||||
"signal": pcbnew.LT_SIGNAL
|
||||
}
|
||||
return type_map.get(type_name.lower(), pcbnew.LT_SIGNAL)
|
||||
|
||||
def _get_layer_type_name(self, type_id: int) -> str:
|
||||
"""Convert KiCAD layer type constant to name"""
|
||||
type_map = {
|
||||
pcbnew.LT_SIGNAL: "signal",
|
||||
pcbnew.LT_POWER: "power",
|
||||
pcbnew.LT_MIXED: "mixed",
|
||||
pcbnew.LT_JUMPER: "jumper",
|
||||
pcbnew.LT_USER: "user"
|
||||
}
|
||||
return type_map.get(type_id, "unknown")
|
||||
413
python/commands/board/outline.py
Normal file
413
python/commands/board/outline.py
Normal file
@@ -0,0 +1,413 @@
|
||||
"""
|
||||
Board outline command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardOutlineCommands:
|
||||
"""Handles board outline operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_board_outline(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a board outline to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
shape = params.get("shape", "rectangle")
|
||||
width = params.get("width")
|
||||
height = params.get("height")
|
||||
center_x = params.get("centerX", 0)
|
||||
center_y = params.get("centerY", 0)
|
||||
radius = params.get("radius")
|
||||
corner_radius = params.get("cornerRadius", 0)
|
||||
points = params.get("points", [])
|
||||
unit = params.get("unit", "mm")
|
||||
|
||||
if shape not in ["rectangle", "circle", "polygon", "rounded_rectangle"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid shape",
|
||||
"errorDetails": f"Shape '{shape}' not supported"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
|
||||
# Create drawing for edge cuts
|
||||
edge_layer = self.board.GetLayerID("Edge.Cuts")
|
||||
|
||||
if shape == "rectangle":
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required for rectangle"
|
||||
}
|
||||
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
|
||||
# Create rectangle
|
||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
||||
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
|
||||
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
|
||||
|
||||
# Add lines for rectangle
|
||||
self._add_edge_line(top_left, top_right, edge_layer)
|
||||
self._add_edge_line(top_right, bottom_right, edge_layer)
|
||||
self._add_edge_line(bottom_right, bottom_left, edge_layer)
|
||||
self._add_edge_line(bottom_left, top_left, edge_layer)
|
||||
|
||||
elif shape == "rounded_rectangle":
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required for rounded rectangle"
|
||||
}
|
||||
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
corner_radius_nm = int(corner_radius * scale)
|
||||
|
||||
# Create rounded rectangle
|
||||
self._add_rounded_rect(
|
||||
center_x_nm, center_y_nm,
|
||||
width_nm, height_nm,
|
||||
corner_radius_nm, edge_layer
|
||||
)
|
||||
|
||||
elif shape == "circle":
|
||||
if radius is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing radius",
|
||||
"errorDetails": "Radius is required for circle"
|
||||
}
|
||||
|
||||
center_x_nm = int(center_x * scale)
|
||||
center_y_nm = int(center_y * scale)
|
||||
radius_nm = int(radius * scale)
|
||||
|
||||
# Create circle
|
||||
circle = pcbnew.PCB_SHAPE(self.board)
|
||||
circle.SetShape(pcbnew.SHAPE_T_CIRCLE)
|
||||
circle.SetCenter(pcbnew.VECTOR2I(center_x_nm, center_y_nm))
|
||||
circle.SetEnd(pcbnew.VECTOR2I(center_x_nm + radius_nm, center_y_nm))
|
||||
circle.SetLayer(edge_layer)
|
||||
circle.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(circle)
|
||||
|
||||
elif shape == "polygon":
|
||||
if not points or len(points) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing points",
|
||||
"errorDetails": "At least 3 points are required for polygon"
|
||||
}
|
||||
|
||||
# Convert points to nm
|
||||
polygon_points = []
|
||||
for point in points:
|
||||
x_nm = int(point["x"] * scale)
|
||||
y_nm = int(point["y"] * scale)
|
||||
polygon_points.append(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add lines for polygon
|
||||
for i in range(len(polygon_points)):
|
||||
self._add_edge_line(
|
||||
polygon_points[i],
|
||||
polygon_points[(i + 1) % len(polygon_points)],
|
||||
edge_layer
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added board outline: {shape}",
|
||||
"outline": {
|
||||
"shape": shape,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"center": {"x": center_x, "y": center_y, "unit": unit},
|
||||
"radius": radius,
|
||||
"cornerRadius": corner_radius,
|
||||
"points": points
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding board outline: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add board outline",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a mounting hole to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
position = params.get("position")
|
||||
diameter = params.get("diameter")
|
||||
pad_diameter = params.get("padDiameter")
|
||||
plated = params.get("plated", False)
|
||||
|
||||
if not position or not diameter:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "position and diameter are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
diameter_nm = int(diameter * scale)
|
||||
pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale # 1mm larger by default
|
||||
|
||||
# Create footprint for mounting hole
|
||||
module = pcbnew.FOOTPRINT(self.board)
|
||||
module.SetReference(f"MH")
|
||||
module.SetValue(f"MountingHole_{diameter}mm")
|
||||
|
||||
# Create the pad for the hole
|
||||
pad = pcbnew.PAD(module)
|
||||
pad.SetNumber(1)
|
||||
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
|
||||
pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
|
||||
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
|
||||
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
|
||||
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
|
||||
module.Add(pad)
|
||||
|
||||
# Position the mounting hole
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add to board
|
||||
self.board.Add(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added mounting hole",
|
||||
"mountingHole": {
|
||||
"position": position,
|
||||
"diameter": diameter,
|
||||
"padDiameter": pad_diameter or diameter + 1,
|
||||
"plated": plated
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding mounting hole: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add mounting hole",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add text annotation to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
text = params.get("text")
|
||||
position = params.get("position")
|
||||
layer = params.get("layer", "F.SilkS")
|
||||
size = params.get("size", 1.0)
|
||||
thickness = params.get("thickness", 0.15)
|
||||
rotation = params.get("rotation", 0)
|
||||
mirror = params.get("mirror", False)
|
||||
|
||||
if not text or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "text and position are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
size_nm = int(size * scale)
|
||||
thickness_nm = int(thickness * scale)
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Create text
|
||||
pcb_text = pcbnew.PCB_TEXT(self.board)
|
||||
pcb_text.SetText(text)
|
||||
pcb_text.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
pcb_text.SetLayer(layer_id)
|
||||
pcb_text.SetTextSize(pcbnew.VECTOR2I(size_nm, size_nm))
|
||||
pcb_text.SetTextThickness(thickness_nm)
|
||||
pcb_text.SetTextAngle(rotation * 10) # KiCAD uses decidegrees
|
||||
pcb_text.SetMirrored(mirror)
|
||||
|
||||
# Add to board
|
||||
self.board.Add(pcb_text)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added text annotation",
|
||||
"text": {
|
||||
"text": text,
|
||||
"position": position,
|
||||
"layer": layer,
|
||||
"size": size,
|
||||
"thickness": thickness,
|
||||
"rotation": rotation,
|
||||
"mirror": mirror
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding text: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add text",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
|
||||
"""Add a line to the edge cuts layer"""
|
||||
line = pcbnew.PCB_SHAPE(self.board)
|
||||
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||
line.SetStart(start)
|
||||
line.SetEnd(end)
|
||||
line.SetLayer(layer)
|
||||
line.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(line)
|
||||
|
||||
def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int,
|
||||
width_nm: int, height_nm: int,
|
||||
radius_nm: int, layer: int) -> None:
|
||||
"""Add a rounded rectangle to the edge cuts layer"""
|
||||
if radius_nm <= 0:
|
||||
# If no radius, create regular rectangle
|
||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
||||
bottom_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm + height_nm // 2)
|
||||
bottom_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm + height_nm // 2)
|
||||
|
||||
self._add_edge_line(top_left, top_right, layer)
|
||||
self._add_edge_line(top_right, bottom_right, layer)
|
||||
self._add_edge_line(bottom_right, bottom_left, layer)
|
||||
self._add_edge_line(bottom_left, top_left, layer)
|
||||
return
|
||||
|
||||
# Calculate corner centers
|
||||
half_width = width_nm // 2
|
||||
half_height = height_nm // 2
|
||||
|
||||
# Ensure radius is not larger than half the smallest dimension
|
||||
max_radius = min(half_width, half_height)
|
||||
if radius_nm > max_radius:
|
||||
radius_nm = max_radius
|
||||
|
||||
# Calculate corner centers
|
||||
top_left_center = pcbnew.VECTOR2I(
|
||||
center_x_nm - half_width + radius_nm,
|
||||
center_y_nm - half_height + radius_nm
|
||||
)
|
||||
top_right_center = pcbnew.VECTOR2I(
|
||||
center_x_nm + half_width - radius_nm,
|
||||
center_y_nm - half_height + radius_nm
|
||||
)
|
||||
bottom_right_center = pcbnew.VECTOR2I(
|
||||
center_x_nm + half_width - radius_nm,
|
||||
center_y_nm + half_height - radius_nm
|
||||
)
|
||||
bottom_left_center = pcbnew.VECTOR2I(
|
||||
center_x_nm - half_width + radius_nm,
|
||||
center_y_nm + half_height - radius_nm
|
||||
)
|
||||
|
||||
# Add arcs for corners
|
||||
self._add_corner_arc(top_left_center, radius_nm, 180, 270, layer)
|
||||
self._add_corner_arc(top_right_center, radius_nm, 270, 0, layer)
|
||||
self._add_corner_arc(bottom_right_center, radius_nm, 0, 90, layer)
|
||||
self._add_corner_arc(bottom_left_center, radius_nm, 90, 180, layer)
|
||||
|
||||
# Add lines for straight edges
|
||||
# Top edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
|
||||
pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
|
||||
layer
|
||||
)
|
||||
# Right edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
|
||||
pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
|
||||
layer
|
||||
)
|
||||
# Bottom edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
|
||||
pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
|
||||
layer
|
||||
)
|
||||
# Left edge
|
||||
self._add_edge_line(
|
||||
pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
|
||||
pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
|
||||
layer
|
||||
)
|
||||
|
||||
def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int,
|
||||
start_angle: float, end_angle: float, layer: int) -> None:
|
||||
"""Add an arc for a rounded corner"""
|
||||
# Create arc for corner
|
||||
arc = pcbnew.PCB_SHAPE(self.board)
|
||||
arc.SetShape(pcbnew.SHAPE_T_ARC)
|
||||
arc.SetCenter(center)
|
||||
|
||||
# Calculate start and end points
|
||||
start_x = center.x + int(radius * math.cos(math.radians(start_angle)))
|
||||
start_y = center.y + int(radius * math.sin(math.radians(start_angle)))
|
||||
end_x = center.x + int(radius * math.cos(math.radians(end_angle)))
|
||||
end_y = center.y + int(radius * math.sin(math.radians(end_angle)))
|
||||
|
||||
arc.SetStart(pcbnew.VECTOR2I(start_x, start_y))
|
||||
arc.SetEnd(pcbnew.VECTOR2I(end_x, end_y))
|
||||
arc.SetLayer(layer)
|
||||
arc.SetWidth(0) # Zero width for edge cuts
|
||||
self.board.Add(arc)
|
||||
67
python/commands/board/size.py
Normal file
67
python/commands/board/size.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Board size command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardSizeCommands:
|
||||
"""Handles board size operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def set_board_size(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set the size of the PCB board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
width = params.get("width")
|
||||
height = params.get("height")
|
||||
unit = params.get("unit", "mm")
|
||||
|
||||
if width is None or height is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing dimensions",
|
||||
"errorDetails": "Both width and height are required"
|
||||
}
|
||||
|
||||
# Convert to internal units (nanometers)
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
width_nm = int(width * scale)
|
||||
height_nm = int(height * scale)
|
||||
|
||||
# Set board size
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
board_box.SetSize(pcbnew.VECTOR2I(width_nm, height_nm))
|
||||
|
||||
# Update board outline
|
||||
self.board.SetBoardEdgesBoundingBox(board_box)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Set board size to {width}x{height} {unit}",
|
||||
"size": {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"unit": unit
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting board size: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set board size",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
171
python/commands/board/view.py
Normal file
171
python/commands/board/view.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Board view command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
from PIL import Image
|
||||
import io
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class BoardViewCommands:
|
||||
"""Handles board viewing operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get board dimensions
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
width_nm = board_box.GetWidth()
|
||||
height_nm = board_box.GetHeight()
|
||||
|
||||
# Convert to mm
|
||||
width_mm = width_nm / 1000000
|
||||
height_mm = height_nm / 1000000
|
||||
|
||||
# Get layer information
|
||||
layers = []
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layers.append({
|
||||
"name": self.board.GetLayerName(layer_id),
|
||||
"type": self._get_layer_type_name(self.board.GetLayerType(layer_id)),
|
||||
"id": layer_id
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"board": {
|
||||
"filename": self.board.GetFileName(),
|
||||
"size": {
|
||||
"width": width_mm,
|
||||
"height": height_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"layers": layers,
|
||||
"title": self.board.GetTitleBlock().GetTitle(),
|
||||
"activeLayer": self.board.GetActiveLayer()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting board info: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get board information",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a 2D image of the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get parameters
|
||||
width = params.get("width", 800)
|
||||
height = params.get("height", 600)
|
||||
format = params.get("format", "png")
|
||||
layers = params.get("layers", [])
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(self.board.GetFileName()))
|
||||
plot_opts.SetScale(1)
|
||||
plot_opts.SetMirror(False)
|
||||
plot_opts.SetExcludeEdgeLayer(False)
|
||||
plot_opts.SetPlotFrameRef(False)
|
||||
plot_opts.SetPlotValue(True)
|
||||
plot_opts.SetPlotReference(True)
|
||||
|
||||
# Plot to SVG first (for vector output)
|
||||
temp_svg = os.path.join(os.path.dirname(self.board.GetFileName()), "temp_view.svg")
|
||||
plotter.OpenPlotfile("temp_view", pcbnew.PLOT_FORMAT_SVG, "Temporary View")
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0 and self.board.IsLayerEnabled(layer_id):
|
||||
plotter.PlotLayer(layer_id)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
plotter.PlotLayer(layer_id)
|
||||
|
||||
plotter.ClosePlot()
|
||||
|
||||
# Convert SVG to requested format
|
||||
if format == "svg":
|
||||
with open(temp_svg, 'r') as f:
|
||||
svg_data = f.read()
|
||||
os.remove(temp_svg)
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": svg_data,
|
||||
"format": "svg"
|
||||
}
|
||||
else:
|
||||
# Use PIL to convert SVG to PNG/JPG
|
||||
from cairosvg import svg2png
|
||||
png_data = svg2png(url=temp_svg, output_width=width, output_height=height)
|
||||
os.remove(temp_svg)
|
||||
|
||||
if format == "jpg":
|
||||
# Convert PNG to JPG
|
||||
img = Image.open(io.BytesIO(png_data))
|
||||
jpg_buffer = io.BytesIO()
|
||||
img.convert('RGB').save(jpg_buffer, format='JPEG')
|
||||
jpg_data = jpg_buffer.getvalue()
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(jpg_data).decode('utf-8'),
|
||||
"format": "jpg"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(png_data).decode('utf-8'),
|
||||
"format": "png"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting board 2D view: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get board 2D view",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_layer_type_name(self, type_id: int) -> str:
|
||||
"""Convert KiCAD layer type constant to name"""
|
||||
type_map = {
|
||||
pcbnew.LT_SIGNAL: "signal",
|
||||
pcbnew.LT_POWER: "power",
|
||||
pcbnew.LT_MIXED: "mixed",
|
||||
pcbnew.LT_JUMPER: "jumper",
|
||||
pcbnew.LT_USER: "user"
|
||||
}
|
||||
return type_map.get(type_id, "unknown")
|
||||
916
python/commands/component.py
Normal file
916
python/commands/component.py
Normal file
@@ -0,0 +1,916 @@
|
||||
"""
|
||||
Component-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class ComponentCommands:
|
||||
"""Handles component-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Place a component on the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Get parameters
|
||||
component_id = params.get("componentId")
|
||||
position = params.get("position")
|
||||
reference = params.get("reference")
|
||||
value = params.get("value")
|
||||
footprint = params.get("footprint")
|
||||
rotation = params.get("rotation", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not component_id or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "componentId and position are required"
|
||||
}
|
||||
|
||||
# Create new module (footprint)
|
||||
module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {component_id}"
|
||||
}
|
||||
|
||||
# Set position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set reference if provided
|
||||
if reference:
|
||||
module.SetReference(reference)
|
||||
|
||||
# Set value if provided
|
||||
if value:
|
||||
module.SetValue(value)
|
||||
|
||||
# Set footprint if provided
|
||||
if footprint:
|
||||
module.SetFootprintName(footprint)
|
||||
|
||||
# Set rotation
|
||||
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
|
||||
# Set layer
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id >= 0:
|
||||
module.SetLayer(layer_id)
|
||||
|
||||
# Add to board
|
||||
self.board.Add(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Placed component: {component_id}",
|
||||
"component": {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"rotation": rotation,
|
||||
"layer": layer
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error placing component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to place component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Move an existing component to a new position"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
position = params.get("position")
|
||||
rotation = params.get("rotation")
|
||||
|
||||
if not reference or not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and position are required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Set new position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set new rotation if provided
|
||||
if rotation is not None:
|
||||
module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Moved component: {reference}",
|
||||
"component": {
|
||||
"reference": reference,
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"rotation": rotation if rotation is not None else module.GetOrientation() / 10
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to move component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Rotate an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
angle = params.get("angle")
|
||||
|
||||
if not reference or angle is None:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and angle are required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Set rotation
|
||||
module.SetOrientation(angle * 10) # KiCAD uses decidegrees
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Rotated component: {reference}",
|
||||
"component": {
|
||||
"reference": reference,
|
||||
"rotation": angle
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error rotating component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to rotate component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Delete a component from the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Remove from board
|
||||
self.board.Remove(module)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Deleted component: {reference}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to delete component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Edit the properties of an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
new_reference = params.get("newReference")
|
||||
value = params.get("value")
|
||||
footprint = params.get("footprint")
|
||||
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Update properties
|
||||
if new_reference:
|
||||
module.SetReference(new_reference)
|
||||
if value:
|
||||
module.SetValue(value)
|
||||
if footprint:
|
||||
module.SetFootprintName(footprint)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Updated component: {reference}",
|
||||
"component": {
|
||||
"reference": new_reference or reference,
|
||||
"value": value or module.GetValue(),
|
||||
"footprint": footprint or module.GetFootprintName()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to edit component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get detailed properties of a component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
if not reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing reference",
|
||||
"errorDetails": "reference parameter is required"
|
||||
}
|
||||
|
||||
# Find the component
|
||||
module = self.board.FindFootprintByReference(reference)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Get position in mm
|
||||
pos = module.GetPosition()
|
||||
x_mm = pos.x / 1000000
|
||||
y_mm = pos.y / 1000000
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"component": {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": x_mm,
|
||||
"y": y_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(module.GetLayer()),
|
||||
"attributes": {
|
||||
"smd": module.GetAttributes() & pcbnew.FP_SMD,
|
||||
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
|
||||
"virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting component properties: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get component properties",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all components on the board"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
components = []
|
||||
for module in self.board.GetFootprints():
|
||||
pos = module.GetPosition()
|
||||
x_mm = pos.x / 1000000
|
||||
y_mm = pos.y / 1000000
|
||||
|
||||
components.append({
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": x_mm,
|
||||
"y": y_mm,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(module.GetLayer())
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"components": components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting component list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get component list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Place an array of components in a grid or circular pattern"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
component_id = params.get("componentId")
|
||||
pattern = params.get("pattern", "grid") # grid or circular
|
||||
count = params.get("count")
|
||||
reference_prefix = params.get("referencePrefix", "U")
|
||||
value = params.get("value")
|
||||
|
||||
if not component_id or not count:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "componentId and count are required"
|
||||
}
|
||||
|
||||
if pattern == "grid":
|
||||
start_position = params.get("startPosition")
|
||||
rows = params.get("rows")
|
||||
columns = params.get("columns")
|
||||
spacing_x = params.get("spacingX")
|
||||
spacing_y = params.get("spacingY")
|
||||
rotation = params.get("rotation", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not start_position or not rows or not columns or not spacing_x or not spacing_y:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing grid parameters",
|
||||
"errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
|
||||
}
|
||||
|
||||
if rows * columns != count:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid grid parameters",
|
||||
"errorDetails": "rows * columns must equal count"
|
||||
}
|
||||
|
||||
placed_components = self._place_grid_array(
|
||||
component_id,
|
||||
start_position,
|
||||
rows,
|
||||
columns,
|
||||
spacing_x,
|
||||
spacing_y,
|
||||
reference_prefix,
|
||||
value,
|
||||
rotation,
|
||||
layer
|
||||
)
|
||||
|
||||
elif pattern == "circular":
|
||||
center = params.get("center")
|
||||
radius = params.get("radius")
|
||||
angle_start = params.get("angleStart", 0)
|
||||
angle_step = params.get("angleStep")
|
||||
rotation_offset = params.get("rotationOffset", 0)
|
||||
layer = params.get("layer", "F.Cu")
|
||||
|
||||
if not center or not radius or not angle_step:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing circular parameters",
|
||||
"errorDetails": "For circular pattern, center, radius, and angleStep are required"
|
||||
}
|
||||
|
||||
placed_components = self._place_circular_array(
|
||||
component_id,
|
||||
center,
|
||||
radius,
|
||||
count,
|
||||
angle_start,
|
||||
angle_step,
|
||||
reference_prefix,
|
||||
value,
|
||||
rotation_offset,
|
||||
layer
|
||||
)
|
||||
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid pattern",
|
||||
"errorDetails": "Pattern must be 'grid' or 'circular'"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Placed {count} components in {pattern} pattern",
|
||||
"components": placed_components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error placing component array: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to place component array",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Align multiple components along a line or distribute them evenly"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
references = params.get("references", [])
|
||||
alignment = params.get("alignment", "horizontal") # horizontal, vertical, or edge
|
||||
distribution = params.get("distribution", "none") # none, equal, or spacing
|
||||
spacing = params.get("spacing")
|
||||
|
||||
if not references or len(references) < 2:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing references",
|
||||
"errorDetails": "At least two component references are required"
|
||||
}
|
||||
|
||||
# Find all referenced components
|
||||
components = []
|
||||
for ref in references:
|
||||
module = self.board.FindFootprintByReference(ref)
|
||||
if not module:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {ref}"
|
||||
}
|
||||
components.append(module)
|
||||
|
||||
# Perform alignment based on selected option
|
||||
if alignment == "horizontal":
|
||||
self._align_components_horizontally(components, distribution, spacing)
|
||||
elif alignment == "vertical":
|
||||
self._align_components_vertically(components, distribution, spacing)
|
||||
elif alignment == "edge":
|
||||
edge = params.get("edge")
|
||||
if not edge:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing edge parameter",
|
||||
"errorDetails": "Edge parameter is required for edge alignment"
|
||||
}
|
||||
self._align_components_to_edge(components, edge)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid alignment option",
|
||||
"errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
|
||||
}
|
||||
|
||||
# Prepare result data
|
||||
aligned_components = []
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
aligned_components.append({
|
||||
"reference": module.GetReference(),
|
||||
"position": {
|
||||
"x": pos.x / 1000000,
|
||||
"y": pos.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": module.GetOrientation() / 10
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Aligned {len(components)} components",
|
||||
"alignment": alignment,
|
||||
"distribution": distribution,
|
||||
"components": aligned_components
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error aligning components: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to align components",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Duplicate an existing component"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
reference = params.get("reference")
|
||||
new_reference = params.get("newReference")
|
||||
position = params.get("position")
|
||||
rotation = params.get("rotation")
|
||||
|
||||
if not reference or not new_reference:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "reference and newReference are required"
|
||||
}
|
||||
|
||||
# Find the source component
|
||||
source = self.board.FindFootprintByReference(reference)
|
||||
if not source:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Component not found",
|
||||
"errorDetails": f"Could not find component: {reference}"
|
||||
}
|
||||
|
||||
# Check if new reference already exists
|
||||
if self.board.FindFootprintByReference(new_reference):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Reference already exists",
|
||||
"errorDetails": f"A component with reference {new_reference} already exists"
|
||||
}
|
||||
|
||||
# Create new footprint with the same properties
|
||||
new_module = pcbnew.FOOTPRINT(self.board)
|
||||
new_module.SetFootprintName(source.GetFootprintName())
|
||||
new_module.SetValue(source.GetValue())
|
||||
new_module.SetReference(new_reference)
|
||||
new_module.SetLayer(source.GetLayer())
|
||||
|
||||
# Copy pads and other items
|
||||
for pad in source.Pads():
|
||||
new_pad = pcbnew.PAD(new_module)
|
||||
new_pad.Copy(pad)
|
||||
new_module.Add(new_pad)
|
||||
|
||||
# Set position if provided, otherwise use offset from original
|
||||
if position:
|
||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
else:
|
||||
# Offset by 5mm
|
||||
source_pos = source.GetPosition()
|
||||
new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
|
||||
|
||||
# Set rotation if provided, otherwise use same as original
|
||||
if rotation is not None:
|
||||
new_module.SetOrientation(rotation * 10) # KiCAD uses decidegrees
|
||||
else:
|
||||
new_module.SetOrientation(source.GetOrientation())
|
||||
|
||||
# Add to board
|
||||
self.board.Add(new_module)
|
||||
|
||||
# Get final position in mm
|
||||
pos = new_module.GetPosition()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Duplicated component {reference} to {new_reference}",
|
||||
"component": {
|
||||
"reference": new_reference,
|
||||
"value": new_module.GetValue(),
|
||||
"footprint": new_module.GetFootprintName(),
|
||||
"position": {
|
||||
"x": pos.x / 1000000,
|
||||
"y": pos.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"rotation": new_module.GetOrientation() / 10,
|
||||
"layer": self.board.GetLayerName(new_module.GetLayer())
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error duplicating component: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to duplicate component",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _place_grid_array(self, component_id: str, start_position: Dict[str, Any],
|
||||
rows: int, columns: int, spacing_x: float, spacing_y: float,
|
||||
reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
|
||||
"""Place components in a grid pattern and return the list of placed components"""
|
||||
placed = []
|
||||
|
||||
# Convert spacing to nm
|
||||
unit = start_position.get("unit", "mm")
|
||||
scale = 1000000 if unit == "mm" else 25400000 # mm or inch to nm
|
||||
spacing_x_nm = int(spacing_x * scale)
|
||||
spacing_y_nm = int(spacing_y * scale)
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
|
||||
for row in range(rows):
|
||||
for col in range(columns):
|
||||
# Calculate position
|
||||
x = start_position["x"] + (col * spacing_x)
|
||||
y = start_position["y"] + (row * spacing_y)
|
||||
|
||||
# Generate reference
|
||||
index = row * columns + col + 1
|
||||
component_reference = f"{reference_prefix}{index}"
|
||||
|
||||
# Place component
|
||||
result = self.place_component({
|
||||
"componentId": component_id,
|
||||
"position": {"x": x, "y": y, "unit": unit},
|
||||
"reference": component_reference,
|
||||
"value": value,
|
||||
"rotation": rotation,
|
||||
"layer": layer
|
||||
})
|
||||
|
||||
if result["success"]:
|
||||
placed.append(result["component"])
|
||||
|
||||
return placed
|
||||
|
||||
def _place_circular_array(self, component_id: str, center: Dict[str, Any],
|
||||
radius: float, count: int, angle_start: float,
|
||||
angle_step: float, reference_prefix: str,
|
||||
value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
|
||||
"""Place components in a circular pattern and return the list of placed components"""
|
||||
placed = []
|
||||
|
||||
# Get unit
|
||||
unit = center.get("unit", "mm")
|
||||
|
||||
for i in range(count):
|
||||
# Calculate angle for this component
|
||||
angle = angle_start + (i * angle_step)
|
||||
angle_rad = math.radians(angle)
|
||||
|
||||
# Calculate position
|
||||
x = center["x"] + (radius * math.cos(angle_rad))
|
||||
y = center["y"] + (radius * math.sin(angle_rad))
|
||||
|
||||
# Generate reference
|
||||
component_reference = f"{reference_prefix}{i+1}"
|
||||
|
||||
# Calculate rotation (pointing outward from center)
|
||||
component_rotation = angle + rotation_offset
|
||||
|
||||
# Place component
|
||||
result = self.place_component({
|
||||
"componentId": component_id,
|
||||
"position": {"x": x, "y": y, "unit": unit},
|
||||
"reference": component_reference,
|
||||
"value": value,
|
||||
"rotation": component_rotation,
|
||||
"layer": layer
|
||||
})
|
||||
|
||||
if result["success"]:
|
||||
placed.append(result["component"])
|
||||
|
||||
return placed
|
||||
|
||||
def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT],
|
||||
distribution: str, spacing: Optional[float]) -> None:
|
||||
"""Align components horizontally and optionally distribute them"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Find the average Y coordinate
|
||||
y_sum = sum(module.GetPosition().y for module in components)
|
||||
y_avg = y_sum // len(components)
|
||||
|
||||
# Sort components by X position
|
||||
components.sort(key=lambda m: m.GetPosition().x)
|
||||
|
||||
# Set Y coordinate for all components
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
|
||||
|
||||
# Handle distribution if requested
|
||||
if distribution == "equal" and len(components) > 1:
|
||||
# Get leftmost and rightmost X coordinates
|
||||
x_min = components[0].GetPosition().x
|
||||
x_max = components[-1].GetPosition().x
|
||||
|
||||
# Calculate equal spacing
|
||||
total_space = x_max - x_min
|
||||
spacing_nm = total_space // (len(components) - 1)
|
||||
|
||||
# Set X positions with equal spacing
|
||||
for i in range(1, len(components) - 1):
|
||||
pos = components[i].GetPosition()
|
||||
new_x = x_min + (i * spacing_nm)
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
|
||||
|
||||
elif distribution == "spacing" and spacing is not None:
|
||||
# Convert spacing to nanometers
|
||||
spacing_nm = int(spacing * 1000000) # assuming mm
|
||||
|
||||
# Set X positions with the specified spacing
|
||||
x_current = components[0].GetPosition().x
|
||||
for i in range(1, len(components)):
|
||||
pos = components[i].GetPosition()
|
||||
x_current += spacing_nm
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
|
||||
|
||||
def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT],
|
||||
distribution: str, spacing: Optional[float]) -> None:
|
||||
"""Align components vertically and optionally distribute them"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Find the average X coordinate
|
||||
x_sum = sum(module.GetPosition().x for module in components)
|
||||
x_avg = x_sum // len(components)
|
||||
|
||||
# Sort components by Y position
|
||||
components.sort(key=lambda m: m.GetPosition().y)
|
||||
|
||||
# Set X coordinate for all components
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
|
||||
|
||||
# Handle distribution if requested
|
||||
if distribution == "equal" and len(components) > 1:
|
||||
# Get topmost and bottommost Y coordinates
|
||||
y_min = components[0].GetPosition().y
|
||||
y_max = components[-1].GetPosition().y
|
||||
|
||||
# Calculate equal spacing
|
||||
total_space = y_max - y_min
|
||||
spacing_nm = total_space // (len(components) - 1)
|
||||
|
||||
# Set Y positions with equal spacing
|
||||
for i in range(1, len(components) - 1):
|
||||
pos = components[i].GetPosition()
|
||||
new_y = y_min + (i * spacing_nm)
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
|
||||
|
||||
elif distribution == "spacing" and spacing is not None:
|
||||
# Convert spacing to nanometers
|
||||
spacing_nm = int(spacing * 1000000) # assuming mm
|
||||
|
||||
# Set Y positions with the specified spacing
|
||||
y_current = components[0].GetPosition().y
|
||||
for i in range(1, len(components)):
|
||||
pos = components[i].GetPosition()
|
||||
y_current += spacing_nm
|
||||
components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
|
||||
|
||||
def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
|
||||
"""Align components to the specified edge of the board"""
|
||||
if not components:
|
||||
return
|
||||
|
||||
# Get board bounds
|
||||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||||
left = board_box.GetLeft()
|
||||
right = board_box.GetRight()
|
||||
top = board_box.GetTop()
|
||||
bottom = board_box.GetBottom()
|
||||
|
||||
# Align based on specified edge
|
||||
if edge == "left":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y)) # 2mm offset from edge
|
||||
elif edge == "right":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y)) # 2mm offset from edge
|
||||
elif edge == "top":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000)) # 2mm offset from edge
|
||||
elif edge == "bottom":
|
||||
for module in components:
|
||||
pos = module.GetPosition()
|
||||
module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000)) # 2mm offset from edge
|
||||
else:
|
||||
logger.warning(f"Unknown edge alignment: {edge}")
|
||||
165
python/commands/component_schematic.py
Normal file
165
python/commands/component_schematic.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from skip import Schematic
|
||||
# Symbol class might not be directly importable in the current version
|
||||
import os
|
||||
|
||||
class ComponentManager:
|
||||
"""Manage components in a schematic"""
|
||||
|
||||
@staticmethod
|
||||
def add_component(schematic: Schematic, component_def: dict):
|
||||
"""Add a component to the schematic"""
|
||||
try:
|
||||
# Create a new symbol
|
||||
symbol = schematic.add_symbol(
|
||||
lib=component_def.get('library', 'Device'),
|
||||
name=component_def.get('type', 'R'), # Default to Resistor symbol 'R'
|
||||
reference=component_def.get('reference', 'R?'),
|
||||
at=[component_def.get('x', 0), component_def.get('y', 0)],
|
||||
unit=component_def.get('unit', 1),
|
||||
rotation=component_def.get('rotation', 0)
|
||||
)
|
||||
|
||||
# Set properties
|
||||
if 'value' in component_def:
|
||||
symbol.property.Value.value = component_def['value']
|
||||
if 'footprint' in component_def:
|
||||
symbol.property.Footprint.value = component_def['footprint']
|
||||
if 'datasheet' in component_def:
|
||||
symbol.property.Datasheet.value = component_def['datasheet']
|
||||
|
||||
# Add additional properties
|
||||
for key, value in component_def.get('properties', {}).items():
|
||||
# Avoid overwriting standard properties unless explicitly intended
|
||||
if key not in ['Reference', 'Value', 'Footprint', 'Datasheet']:
|
||||
symbol.property.append(key, value)
|
||||
|
||||
print(f"Added component {symbol.reference} ({symbol.name}) to schematic.")
|
||||
return symbol
|
||||
except Exception as e:
|
||||
print(f"Error adding component: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def remove_component(schematic: Schematic, component_ref: str):
|
||||
"""Remove a component from the schematic by reference designator"""
|
||||
try:
|
||||
# kicad-skip doesn't have a direct remove_symbol method by reference.
|
||||
# We need to find the symbol and then remove it from the symbols list.
|
||||
symbol_to_remove = None
|
||||
for symbol in schematic.symbol:
|
||||
if symbol.reference == component_ref:
|
||||
symbol_to_remove = symbol
|
||||
break
|
||||
|
||||
if symbol_to_remove:
|
||||
schematic.symbol.remove(symbol_to_remove)
|
||||
print(f"Removed component {component_ref} from schematic.")
|
||||
return True
|
||||
else:
|
||||
print(f"Component with reference {component_ref} not found.")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error removing component {component_ref}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@staticmethod
|
||||
def update_component(schematic: Schematic, component_ref: str, new_properties: dict):
|
||||
"""Update component properties by reference designator"""
|
||||
try:
|
||||
symbol_to_update = None
|
||||
for symbol in schematic.symbol:
|
||||
if symbol.reference == component_ref:
|
||||
symbol_to_update = symbol
|
||||
break
|
||||
|
||||
if symbol_to_update:
|
||||
for key, value in new_properties.items():
|
||||
if key in symbol_to_update.property:
|
||||
symbol_to_update.property[key].value = value
|
||||
else:
|
||||
# Add as a new property if it doesn't exist
|
||||
symbol_to_update.property.append(key, value)
|
||||
print(f"Updated properties for component {component_ref}.")
|
||||
return True
|
||||
else:
|
||||
print(f"Component with reference {component_ref} not found.")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error updating component {component_ref}: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_component(schematic: Schematic, component_ref: str):
|
||||
"""Get a component by reference designator"""
|
||||
for symbol in schematic.symbol:
|
||||
if symbol.reference == component_ref:
|
||||
print(f"Found component with reference {component_ref}.")
|
||||
return symbol
|
||||
print(f"Component with reference {component_ref} not found.")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def search_components(schematic: Schematic, query: str):
|
||||
"""Search for components matching criteria (basic implementation)"""
|
||||
# This is a basic search, could be expanded to use regex or more complex logic
|
||||
matching_components = []
|
||||
query_lower = query.lower()
|
||||
for symbol in schematic.symbol:
|
||||
if query_lower in symbol.reference.lower() or \
|
||||
query_lower in symbol.name.lower() or \
|
||||
(hasattr(symbol.property, 'Value') and query_lower in symbol.property.Value.value.lower()):
|
||||
matching_components.append(symbol)
|
||||
print(f"Found {len(matching_components)} components matching query '{query}'.")
|
||||
return matching_components
|
||||
|
||||
@staticmethod
|
||||
def get_all_components(schematic: Schematic):
|
||||
"""Get all components in schematic"""
|
||||
print(f"Retrieving all {len(schematic.symbol)} components.")
|
||||
return list(schematic.symbol)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
from schematic import SchematicManager # Assuming schematic.py is in the same directory
|
||||
|
||||
# Create a new schematic
|
||||
test_sch = SchematicManager.create_schematic("ComponentTestSchematic")
|
||||
|
||||
# Add components
|
||||
comp1_def = {"type": "R", "reference": "R1", "value": "10k", "x": 100, "y": 100}
|
||||
comp2_def = {"type": "C", "reference": "C1", "value": "0.1uF", "x": 200, "y": 100, "library": "Device"}
|
||||
comp3_def = {"type": "LED", "reference": "D1", "x": 300, "y": 100, "library": "Device", "properties": {"Color": "Red"}}
|
||||
|
||||
comp1 = ComponentManager.add_component(test_sch, comp1_def)
|
||||
comp2 = ComponentManager.add_component(test_sch, comp2_def)
|
||||
comp3 = ComponentManager.add_component(test_sch, comp3_def)
|
||||
|
||||
# Get a component
|
||||
retrieved_comp = ComponentManager.get_component(test_sch, "C1")
|
||||
if retrieved_comp:
|
||||
print(f"Retrieved component: {retrieved_comp.reference} ({retrieved_comp.value})")
|
||||
|
||||
# Update a component
|
||||
ComponentManager.update_component(test_sch, "R1", {"value": "20k", "Tolerance": "5%"})
|
||||
|
||||
# Search components
|
||||
matching_comps = ComponentManager.search_components(test_sch, "100") # Search by position
|
||||
print(f"Search results for '100': {[c.reference for c in matching_comps]}")
|
||||
|
||||
# Get all components
|
||||
all_comps = ComponentManager.get_all_components(test_sch)
|
||||
print(f"All components: {[c.reference for c in all_comps]}")
|
||||
|
||||
# Remove a component
|
||||
ComponentManager.remove_component(test_sch, "D1")
|
||||
all_comps_after_remove = ComponentManager.get_all_components(test_sch)
|
||||
print(f"Components after removing D1: {[c.reference for c in all_comps_after_remove]}")
|
||||
|
||||
# Save the schematic (optional)
|
||||
# SchematicManager.save_schematic(test_sch, "component_test.kicad_sch")
|
||||
|
||||
# Clean up (if saved)
|
||||
# if os.path.exists("component_test.kicad_sch"):
|
||||
# os.remove("component_test.kicad_sch")
|
||||
# print("Cleaned up component_test.kicad_sch")
|
||||
91
python/commands/connection_schematic.py
Normal file
91
python/commands/connection_schematic.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from skip import Schematic
|
||||
# Wire and Net classes might not be directly importable in the current version
|
||||
import os
|
||||
|
||||
class ConnectionManager:
|
||||
"""Manage connections between components"""
|
||||
|
||||
@staticmethod
|
||||
def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None):
|
||||
"""Add a wire between two points"""
|
||||
try:
|
||||
wire = schematic.add_wire(start=start_point, end=end_point)
|
||||
# kicad-skip wire properties are limited, but we can potentially
|
||||
# add graphical properties if needed in the future.
|
||||
print(f"Added wire from {start_point} to {end_point}.")
|
||||
return wire
|
||||
except Exception as e:
|
||||
print(f"Error adding wire: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_connection(schematic: Schematic, source_ref: str, source_pin: str, target_ref: str, target_pin: str):
|
||||
"""Add a connection between component pins"""
|
||||
# kicad-skip handles connections implicitly through wires and labels.
|
||||
# This method would typically involve adding wires and potentially net labels
|
||||
# to connect the specified pins.
|
||||
# A direct 'add_connection' between pins isn't a standard kicad-skip operation
|
||||
# in the way it is in some other schematic tools.
|
||||
# We will need to implement this logic by finding the component pins
|
||||
# and adding wires/labels between their locations. This is more complex
|
||||
# and might require pin location information which isn't directly
|
||||
# exposed in a simple way by default in kicad-skip Symbol objects.
|
||||
|
||||
# For now, this method will be a placeholder or require a more advanced
|
||||
# implementation based on how kicad-skip handles net connections.
|
||||
# A common approach is to add wires between graphical points and then
|
||||
# add net labels to define the net name.
|
||||
|
||||
print(f"Attempted to add connection between {source_ref}/{source_pin} and {target_ref}/{target_pin}. This requires advanced implementation.")
|
||||
return False # Indicate not fully implemented yet
|
||||
|
||||
@staticmethod
|
||||
def remove_connection(schematic: Schematic, connection_id: str):
|
||||
"""Remove a connection"""
|
||||
# Removing connections in kicad-skip typically means removing the wires
|
||||
# or net labels that form the connection.
|
||||
# This method would need to identify the relevant graphical elements
|
||||
# based on a connection identifier (which we would need to define).
|
||||
# This is also an advanced implementation task.
|
||||
print(f"Attempted to remove connection with ID {connection_id}. This requires advanced implementation.")
|
||||
return False # Indicate not fully implemented yet
|
||||
|
||||
@staticmethod
|
||||
def get_net_connections(schematic: Schematic, net_name: str):
|
||||
"""Get all connections in a named net"""
|
||||
# kicad-skip represents nets implicitly through connected wires and net labels.
|
||||
# To get connections for a net, we would need to iterate through wires
|
||||
# and net labels to build a list of connected pins/points.
|
||||
# This requires traversing the schematic's graphical elements and understanding
|
||||
# how they form nets. This is an advanced implementation task.
|
||||
print(f"Attempted to get connections for net '{net_name}'. This requires advanced implementation.")
|
||||
return [] # Return empty list for now
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
from schematic import SchematicManager # Assuming schematic.py is in the same directory
|
||||
|
||||
# Create a new schematic
|
||||
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
|
||||
|
||||
# Add some wires
|
||||
wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
|
||||
wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
|
||||
|
||||
# Note: add_connection, remove_connection, get_net_connections are placeholders
|
||||
# and require more complex implementation based on kicad-skip's structure.
|
||||
|
||||
# Example of how you might add a net label (requires finding a point on a wire)
|
||||
# from skip import Label
|
||||
# if wire1:
|
||||
# net_label_pos = wire1.start # Or calculate a point on the wire
|
||||
# net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
|
||||
# print(f"Added net label 'Net_01' at {net_label_pos}")
|
||||
|
||||
# Save the schematic (optional)
|
||||
# SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
|
||||
|
||||
# Clean up (if saved)
|
||||
# if os.path.exists("connection_test.kicad_sch"):
|
||||
# os.remove("connection_test.kicad_sch")
|
||||
# print("Cleaned up connection_test.kicad_sch")
|
||||
239
python/commands/design_rules.py
Normal file
239
python/commands/design_rules.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
Design rules command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class DesignRuleCommands:
|
||||
"""Handles design rule checking and configuration"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def set_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Set design rules for the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
|
||||
# Convert mm to nanometers for KiCAD internal units
|
||||
scale = 1000000 # mm to nm
|
||||
|
||||
# Set clearance
|
||||
if "clearance" in params:
|
||||
design_settings.SetMinClearance(int(params["clearance"] * scale))
|
||||
|
||||
# Set track width
|
||||
if "trackWidth" in params:
|
||||
design_settings.SetCurrentTrackWidth(int(params["trackWidth"] * scale))
|
||||
|
||||
# Set via settings
|
||||
if "viaDiameter" in params:
|
||||
design_settings.SetCurrentViaSize(int(params["viaDiameter"] * scale))
|
||||
if "viaDrill" in params:
|
||||
design_settings.SetCurrentViaDrill(int(params["viaDrill"] * scale))
|
||||
|
||||
# Set micro via settings
|
||||
if "microViaDiameter" in params:
|
||||
design_settings.SetCurrentMicroViaSize(int(params["microViaDiameter"] * scale))
|
||||
if "microViaDrill" in params:
|
||||
design_settings.SetCurrentMicroViaDrill(int(params["microViaDrill"] * scale))
|
||||
|
||||
# Set minimum values
|
||||
if "minTrackWidth" in params:
|
||||
design_settings.m_TrackMinWidth = int(params["minTrackWidth"] * scale)
|
||||
if "minViaDiameter" in params:
|
||||
design_settings.m_ViasMinSize = int(params["minViaDiameter"] * scale)
|
||||
if "minViaDrill" in params:
|
||||
design_settings.m_ViasMinDrill = int(params["minViaDrill"] * scale)
|
||||
if "minMicroViaDiameter" in params:
|
||||
design_settings.m_MicroViasMinSize = int(params["minMicroViaDiameter"] * scale)
|
||||
if "minMicroViaDrill" in params:
|
||||
design_settings.m_MicroViasMinDrill = int(params["minMicroViaDrill"] * scale)
|
||||
|
||||
# Set hole diameter
|
||||
if "minHoleDiameter" in params:
|
||||
design_settings.m_MinHoleDiameter = int(params["minHoleDiameter"] * scale)
|
||||
|
||||
# Set courtyard settings
|
||||
if "requireCourtyard" in params:
|
||||
design_settings.m_RequireCourtyards = params["requireCourtyard"]
|
||||
if "courtyardClearance" in params:
|
||||
design_settings.m_CourtyardMinClearance = int(params["courtyardClearance"] * scale)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Updated design rules",
|
||||
"rules": {
|
||||
"clearance": design_settings.GetMinClearance() / scale,
|
||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
|
||||
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
|
||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||
"minViaDrill": design_settings.m_ViasMinDrill / scale,
|
||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
|
||||
"requireCourtyard": design_settings.m_RequireCourtyards,
|
||||
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting design rules: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to set design rules",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_design_rules(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get current design rules"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
scale = 1000000 # nm to mm
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"rules": {
|
||||
"clearance": design_settings.GetMinClearance() / scale,
|
||||
"trackWidth": design_settings.GetCurrentTrackWidth() / scale,
|
||||
"viaDiameter": design_settings.GetCurrentViaSize() / scale,
|
||||
"viaDrill": design_settings.GetCurrentViaDrill() / scale,
|
||||
"microViaDiameter": design_settings.GetCurrentMicroViaSize() / scale,
|
||||
"microViaDrill": design_settings.GetCurrentMicroViaDrill() / scale,
|
||||
"minTrackWidth": design_settings.m_TrackMinWidth / scale,
|
||||
"minViaDiameter": design_settings.m_ViasMinSize / scale,
|
||||
"minViaDrill": design_settings.m_ViasMinDrill / scale,
|
||||
"minMicroViaDiameter": design_settings.m_MicroViasMinSize / scale,
|
||||
"minMicroViaDrill": design_settings.m_MicroViasMinDrill / scale,
|
||||
"minHoleDiameter": design_settings.m_MinHoleDiameter / scale,
|
||||
"requireCourtyard": design_settings.m_RequireCourtyards,
|
||||
"courtyardClearance": design_settings.m_CourtyardMinClearance / scale
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting design rules: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get design rules",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def run_drc(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Run Design Rule Check"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
report_path = params.get("reportPath")
|
||||
|
||||
# Create DRC runner
|
||||
drc = pcbnew.DRC(self.board)
|
||||
|
||||
# Run DRC
|
||||
drc.Run()
|
||||
|
||||
# Get violations
|
||||
violations = []
|
||||
for marker in drc.GetMarkers():
|
||||
violations.append({
|
||||
"type": marker.GetErrorCode(),
|
||||
"severity": "error",
|
||||
"message": marker.GetDescription(),
|
||||
"location": {
|
||||
"x": marker.GetPos().x / 1000000,
|
||||
"y": marker.GetPos().y / 1000000,
|
||||
"unit": "mm"
|
||||
}
|
||||
})
|
||||
|
||||
# Save report if path provided
|
||||
if report_path:
|
||||
report_path = os.path.abspath(os.path.expanduser(report_path))
|
||||
drc.WriteReport(report_path)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Found {len(violations)} DRC violations",
|
||||
"violations": violations,
|
||||
"reportPath": report_path if report_path else None
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running DRC: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to run DRC",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_drc_violations(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get list of DRC violations"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
severity = params.get("severity", "all")
|
||||
|
||||
# Get DRC markers
|
||||
violations = []
|
||||
for marker in self.board.GetDRCMarkers():
|
||||
violation = {
|
||||
"type": marker.GetErrorCode(),
|
||||
"severity": "error", # KiCAD DRC markers are always errors
|
||||
"message": marker.GetDescription(),
|
||||
"location": {
|
||||
"x": marker.GetPos().x / 1000000,
|
||||
"y": marker.GetPos().y / 1000000,
|
||||
"unit": "mm"
|
||||
}
|
||||
}
|
||||
|
||||
# Filter by severity if specified
|
||||
if severity == "all" or severity == violation["severity"]:
|
||||
violations.append(violation)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"violations": violations
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting DRC violations: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get DRC violations",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
475
python/commands/export.py
Normal file
475
python/commands/export.py
Normal file
@@ -0,0 +1,475 @@
|
||||
"""
|
||||
Export command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
import base64
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class ExportCommands:
|
||||
"""Handles export-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def export_gerber(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export Gerber files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_dir = params.get("outputDir")
|
||||
layers = params.get("layers", [])
|
||||
use_protel_extensions = params.get("useProtelExtensions", False)
|
||||
generate_drill_files = params.get("generateDrillFiles", True)
|
||||
generate_map_file = params.get("generateMapFile", False)
|
||||
use_aux_origin = params.get("useAuxOrigin", False)
|
||||
|
||||
if not output_dir:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output directory",
|
||||
"errorDetails": "outputDir parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_dir = os.path.abspath(os.path.expanduser(output_dir))
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(output_dir)
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
plot_opts.SetUseGerberProtelExtensions(use_protel_extensions)
|
||||
plot_opts.SetUseAuxOrigin(use_aux_origin)
|
||||
plot_opts.SetCreateGerberJobFile(generate_map_file)
|
||||
plot_opts.SetSubtractMaskFromSilk(True)
|
||||
|
||||
# Plot specified layers or all copper layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
# Generate drill files if requested
|
||||
drill_files = []
|
||||
if generate_drill_files:
|
||||
drill_writer = pcbnew.EXCELLON_WRITER(self.board)
|
||||
drill_writer.SetFormat(True)
|
||||
drill_writer.SetMapFileFormat(pcbnew.PLOT_FORMAT_GERBER)
|
||||
|
||||
merge_npth = False # Keep plated/non-plated holes separate
|
||||
drill_writer.SetOptions(merge_npth)
|
||||
|
||||
drill_writer.CreateDrillandMapFilesSet(output_dir, True, generate_map_file)
|
||||
|
||||
# Get list of generated drill files
|
||||
for file in os.listdir(output_dir):
|
||||
if file.endswith(".drl") or file.endswith(".cnc"):
|
||||
drill_files.append(file)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported Gerber files",
|
||||
"files": {
|
||||
"gerber": plotted_layers,
|
||||
"drill": drill_files,
|
||||
"map": ["job.gbrjob"] if generate_map_file else []
|
||||
},
|
||||
"outputDir": output_dir
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting Gerber files: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export Gerber files",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export PDF files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
layers = params.get("layers", [])
|
||||
black_and_white = params.get("blackAndWhite", False)
|
||||
frame_reference = params.get("frameReference", True)
|
||||
page_size = params.get("pageSize", "A4")
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_PDF)
|
||||
plot_opts.SetPlotFrameRef(frame_reference)
|
||||
plot_opts.SetPlotValue(True)
|
||||
plot_opts.SetPlotReference(True)
|
||||
plot_opts.SetMonochrome(black_and_white)
|
||||
|
||||
# Set page size
|
||||
page_sizes = {
|
||||
"A4": (297, 210),
|
||||
"A3": (420, 297),
|
||||
"A2": (594, 420),
|
||||
"A1": (841, 594),
|
||||
"A0": (1189, 841),
|
||||
"Letter": (279.4, 215.9),
|
||||
"Legal": (355.6, 215.9),
|
||||
"Tabloid": (431.8, 279.4)
|
||||
}
|
||||
if page_size in page_sizes:
|
||||
height, width = page_sizes[page_size]
|
||||
plot_opts.SetPageSettings((width, height))
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported PDF file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"layers": plotted_layers,
|
||||
"pageSize": page_size
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting PDF file: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export PDF file",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export SVG files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
layers = params.get("layers", [])
|
||||
black_and_white = params.get("blackAndWhite", False)
|
||||
include_components = params.get("includeComponents", True)
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
|
||||
# Set up plot options
|
||||
plot_opts = plotter.GetPlotOptions()
|
||||
plot_opts.SetOutputDirectory(os.path.dirname(output_path))
|
||||
plot_opts.SetFormat(pcbnew.PLOT_FORMAT_SVG)
|
||||
plot_opts.SetPlotValue(include_components)
|
||||
plot_opts.SetPlotReference(include_components)
|
||||
plot_opts.SetMonochrome(black_and_white)
|
||||
|
||||
# Plot specified layers or all enabled layers
|
||||
plotted_layers = []
|
||||
if layers:
|
||||
for layer_name in layers:
|
||||
layer_id = self.board.GetLayerID(layer_name)
|
||||
if layer_id >= 0:
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
else:
|
||||
for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT):
|
||||
if self.board.IsLayerEnabled(layer_id):
|
||||
layer_name = self.board.GetLayerName(layer_id)
|
||||
plotter.PlotLayer(layer_id)
|
||||
plotted_layers.append(layer_name)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Exported SVG file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"layers": plotted_layers
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting SVG file: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export SVG file",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export 3D model files"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
format = params.get("format", "STEP")
|
||||
include_components = params.get("includeComponents", True)
|
||||
include_copper = params.get("includeCopper", True)
|
||||
include_solder_mask = params.get("includeSolderMask", True)
|
||||
include_silkscreen = params.get("includeSilkscreen", True)
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Get 3D viewer
|
||||
viewer = self.board.Get3DViewer()
|
||||
if not viewer:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "3D viewer not available",
|
||||
"errorDetails": "Could not initialize 3D viewer"
|
||||
}
|
||||
|
||||
# Set export options
|
||||
viewer.SetCopperLayersOn(include_copper)
|
||||
viewer.SetSolderMaskLayersOn(include_solder_mask)
|
||||
viewer.SetSilkScreenLayersOn(include_silkscreen)
|
||||
viewer.Set3DModelsOn(include_components)
|
||||
|
||||
# Export based on format
|
||||
if format == "STEP":
|
||||
viewer.ExportSTEPFile(output_path)
|
||||
elif format == "VRML":
|
||||
viewer.ExportVRMLFile(output_path)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Unsupported format",
|
||||
"errorDetails": f"Format {format} is not supported"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Exported {format} file",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"format": format
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting 3D model: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export 3D model",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Export Bill of Materials"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
output_path = params.get("outputPath")
|
||||
format = params.get("format", "CSV")
|
||||
group_by_value = params.get("groupByValue", True)
|
||||
include_attributes = params.get("includeAttributes", [])
|
||||
|
||||
if not output_path:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing output path",
|
||||
"errorDetails": "outputPath parameter is required"
|
||||
}
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
output_path = os.path.abspath(os.path.expanduser(output_path))
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
|
||||
# Get all components
|
||||
components = []
|
||||
for module in self.board.GetFootprints():
|
||||
component = {
|
||||
"reference": module.GetReference(),
|
||||
"value": module.GetValue(),
|
||||
"footprint": module.GetFootprintName(),
|
||||
"layer": self.board.GetLayerName(module.GetLayer())
|
||||
}
|
||||
|
||||
# Add requested attributes
|
||||
for attr in include_attributes:
|
||||
if hasattr(module, f"Get{attr}"):
|
||||
component[attr] = getattr(module, f"Get{attr}")()
|
||||
|
||||
components.append(component)
|
||||
|
||||
# Group by value if requested
|
||||
if group_by_value:
|
||||
grouped = {}
|
||||
for comp in components:
|
||||
key = f"{comp['value']}_{comp['footprint']}"
|
||||
if key not in grouped:
|
||||
grouped[key] = {
|
||||
"value": comp["value"],
|
||||
"footprint": comp["footprint"],
|
||||
"quantity": 1,
|
||||
"references": [comp["reference"]]
|
||||
}
|
||||
else:
|
||||
grouped[key]["quantity"] += 1
|
||||
grouped[key]["references"].append(comp["reference"])
|
||||
components = list(grouped.values())
|
||||
|
||||
# Export based on format
|
||||
if format == "CSV":
|
||||
self._export_bom_csv(output_path, components)
|
||||
elif format == "XML":
|
||||
self._export_bom_xml(output_path, components)
|
||||
elif format == "HTML":
|
||||
self._export_bom_html(output_path, components)
|
||||
elif format == "JSON":
|
||||
self._export_bom_json(output_path, components)
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Unsupported format",
|
||||
"errorDetails": f"Format {format} is not supported"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Exported BOM to {format}",
|
||||
"file": {
|
||||
"path": output_path,
|
||||
"format": format,
|
||||
"componentCount": len(components)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting BOM: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to export BOM",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to CSV format"""
|
||||
import csv
|
||||
with open(path, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=components[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(components)
|
||||
|
||||
def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to XML format"""
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.Element("bom")
|
||||
for comp in components:
|
||||
comp_elem = ET.SubElement(root, "component")
|
||||
for key, value in comp.items():
|
||||
elem = ET.SubElement(comp_elem, key)
|
||||
elem.text = str(value)
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(path, encoding='utf-8', xml_declaration=True)
|
||||
|
||||
def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to HTML format"""
|
||||
html = ["<html><head><title>Bill of Materials</title></head><body>"]
|
||||
html.append("<table border='1'><tr>")
|
||||
# Headers
|
||||
for key in components[0].keys():
|
||||
html.append(f"<th>{key}</th>")
|
||||
html.append("</tr>")
|
||||
# Data
|
||||
for comp in components:
|
||||
html.append("<tr>")
|
||||
for value in comp.values():
|
||||
html.append(f"<td>{value}</td>")
|
||||
html.append("</tr>")
|
||||
html.append("</table></body></html>")
|
||||
with open(path, 'w') as f:
|
||||
f.write("\n".join(html))
|
||||
|
||||
def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||
"""Export BOM to JSON format"""
|
||||
import json
|
||||
with open(path, 'w') as f:
|
||||
json.dump({"components": components}, f, indent=2)
|
||||
141
python/commands/library_schematic.py
Normal file
141
python/commands/library_schematic.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from skip import Schematic
|
||||
# Symbol class might not be directly importable in the current version
|
||||
import os
|
||||
import glob
|
||||
|
||||
class LibraryManager:
|
||||
"""Manage symbol libraries"""
|
||||
|
||||
@staticmethod
|
||||
def list_available_libraries(search_paths=None):
|
||||
"""List all available symbol libraries"""
|
||||
if search_paths is None:
|
||||
# Default library paths based on common KiCAD installations
|
||||
# This would need to be configured for the specific environment
|
||||
search_paths = [
|
||||
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym", # Windows path pattern
|
||||
"/usr/share/kicad/symbols/*.kicad_sym", # Linux path pattern
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym", # macOS path pattern
|
||||
os.path.expanduser("~/Documents/KiCad/*/symbols/*.kicad_sym") # User libraries pattern
|
||||
]
|
||||
|
||||
libraries = []
|
||||
for path_pattern in search_paths:
|
||||
try:
|
||||
# Use glob to find all matching files
|
||||
matching_libs = glob.glob(path_pattern, recursive=True)
|
||||
libraries.extend(matching_libs)
|
||||
except Exception as e:
|
||||
print(f"Error searching for libraries at {path_pattern}: {e}")
|
||||
|
||||
# Extract library names from paths
|
||||
library_names = [os.path.splitext(os.path.basename(lib))[0] for lib in libraries]
|
||||
print(f"Found {len(library_names)} libraries: {', '.join(library_names[:10])}{'...' if len(library_names) > 10 else ''}")
|
||||
|
||||
# Return both full paths and library names
|
||||
return {"paths": libraries, "names": library_names}
|
||||
|
||||
@staticmethod
|
||||
def list_library_symbols(library_path):
|
||||
"""List all symbols in a library"""
|
||||
try:
|
||||
# kicad-skip doesn't provide a direct way to simply list symbols in a library
|
||||
# without loading each one. We might need to implement this using KiCAD's Python API
|
||||
# directly, or by using a different approach.
|
||||
# For now, this is a placeholder implementation.
|
||||
|
||||
# A potential approach would be to load the library file using KiCAD's Python API
|
||||
# or by parsing the library file format.
|
||||
# KiCAD symbol libraries are .kicad_sym files which are S-expression format
|
||||
print(f"Attempted to list symbols in library {library_path}. This requires advanced implementation.")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Error listing symbols in library {library_path}: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_symbol_details(library_path, symbol_name):
|
||||
"""Get detailed information about a symbol"""
|
||||
try:
|
||||
# Similar to list_library_symbols, this might require a more direct approach
|
||||
# using KiCAD's Python API or by parsing the symbol library.
|
||||
print(f"Attempted to get details for symbol {symbol_name} in library {library_path}. This requires advanced implementation.")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error getting symbol details for {symbol_name} in {library_path}: {e}")
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def search_symbols(query, search_paths=None):
|
||||
"""Search for symbols matching criteria"""
|
||||
try:
|
||||
# This would typically involve:
|
||||
# 1. Getting a list of all libraries using list_available_libraries
|
||||
# 2. For each library, getting a list of all symbols
|
||||
# 3. Filtering symbols based on the query
|
||||
|
||||
# For now, this is a placeholder implementation
|
||||
libraries = LibraryManager.list_available_libraries(search_paths)
|
||||
|
||||
results = []
|
||||
print(f"Searched for symbols matching '{query}'. This requires advanced implementation.")
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"Error searching for symbols matching '{query}': {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_default_symbol_for_component_type(component_type, search_paths=None):
|
||||
"""Get a recommended default symbol for a given component type"""
|
||||
# This method provides a simplified way to get a symbol for common component types
|
||||
# It's useful when the user doesn't specify a particular library/symbol
|
||||
|
||||
# Define common mappings from component type to library/symbol
|
||||
common_mappings = {
|
||||
"resistor": {"library": "Device", "symbol": "R"},
|
||||
"capacitor": {"library": "Device", "symbol": "C"},
|
||||
"inductor": {"library": "Device", "symbol": "L"},
|
||||
"diode": {"library": "Device", "symbol": "D"},
|
||||
"led": {"library": "Device", "symbol": "LED"},
|
||||
"transistor_npn": {"library": "Device", "symbol": "Q_NPN_BCE"},
|
||||
"transistor_pnp": {"library": "Device", "symbol": "Q_PNP_BCE"},
|
||||
"opamp": {"library": "Amplifier_Operational", "symbol": "OpAmp_Dual_Generic"},
|
||||
"microcontroller": {"library": "MCU_Module", "symbol": "Arduino_UNO_R3"},
|
||||
# Add more common components as needed
|
||||
}
|
||||
|
||||
# Normalize input to lowercase
|
||||
component_type_lower = component_type.lower()
|
||||
|
||||
# Try direct match first
|
||||
if component_type_lower in common_mappings:
|
||||
return common_mappings[component_type_lower]
|
||||
|
||||
# Try partial matches
|
||||
for key, value in common_mappings.items():
|
||||
if component_type_lower in key or key in component_type_lower:
|
||||
return value
|
||||
|
||||
# Default fallback
|
||||
return {"library": "Device", "symbol": "R"}
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
# List available libraries
|
||||
libraries = LibraryManager.list_available_libraries()
|
||||
if libraries["paths"]:
|
||||
first_lib = libraries["paths"][0]
|
||||
lib_name = libraries["names"][0]
|
||||
print(f"Testing with first library: {lib_name} ({first_lib})")
|
||||
|
||||
# List symbols in the first library
|
||||
symbols = LibraryManager.list_library_symbols(first_lib)
|
||||
# This will report that it requires advanced implementation
|
||||
|
||||
# Get default symbol for a component type
|
||||
resistor_sym = LibraryManager.get_default_symbol_for_component_type("resistor")
|
||||
print(f"Default symbol for resistor: {resistor_sym['library']}/{resistor_sym['symbol']}")
|
||||
|
||||
# Try a partial match
|
||||
cap_sym = LibraryManager.get_default_symbol_for_component_type("cap")
|
||||
print(f"Default symbol for 'cap': {cap_sym['library']}/{cap_sym['symbol']}")
|
||||
200
python/commands/project.py
Normal file
200
python/commands/project.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
Project-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew # type: ignore
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class ProjectCommands:
|
||||
"""Handles project-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a new KiCAD project"""
|
||||
try:
|
||||
project_name = params.get("projectName", "New_Project")
|
||||
path = params.get("path", os.getcwd())
|
||||
template = params.get("template")
|
||||
|
||||
# Generate the full project path
|
||||
project_path = os.path.join(path, project_name)
|
||||
if not project_path.endswith(".kicad_pro"):
|
||||
project_path += ".kicad_pro"
|
||||
|
||||
# Create project directory if it doesn't exist
|
||||
os.makedirs(os.path.dirname(project_path), exist_ok=True)
|
||||
|
||||
# Create a new board
|
||||
board = pcbnew.BOARD()
|
||||
|
||||
# Set project properties
|
||||
board.GetTitleBlock().SetTitle(project_name)
|
||||
|
||||
# Set current date with proper parameter
|
||||
from datetime import datetime
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
board.GetTitleBlock().SetDate(current_date)
|
||||
|
||||
# If template is specified, try to load it
|
||||
if template:
|
||||
template_path = os.path.expanduser(template)
|
||||
if os.path.exists(template_path):
|
||||
template_board = pcbnew.LoadBoard(template_path)
|
||||
# Copy settings from template
|
||||
board.SetDesignSettings(template_board.GetDesignSettings())
|
||||
board.SetLayerStack(template_board.GetLayerStack())
|
||||
|
||||
# Save the board
|
||||
board_path = project_path.replace(".kicad_pro", ".kicad_pcb")
|
||||
board.SetFileName(board_path)
|
||||
pcbnew.SaveBoard(board_path, board)
|
||||
|
||||
# Create project file
|
||||
with open(project_path, 'w') as f:
|
||||
f.write('{\n')
|
||||
f.write(' "board": {\n')
|
||||
f.write(f' "filename": "{os.path.basename(board_path)}"\n')
|
||||
f.write(' }\n')
|
||||
f.write('}\n')
|
||||
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created project: {project_name}",
|
||||
"project": {
|
||||
"name": project_name,
|
||||
"path": project_path,
|
||||
"boardPath": board_path
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to create project",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def open_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Open an existing KiCAD project"""
|
||||
try:
|
||||
filename = params.get("filename")
|
||||
if not filename:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No filename provided",
|
||||
"errorDetails": "The filename parameter is required"
|
||||
}
|
||||
|
||||
# Expand user path and make absolute
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
|
||||
# If it's a project file, get the board file
|
||||
if filename.endswith(".kicad_pro"):
|
||||
board_path = filename.replace(".kicad_pro", ".kicad_pcb")
|
||||
else:
|
||||
board_path = filename
|
||||
|
||||
# Load the board
|
||||
board = pcbnew.LoadBoard(board_path)
|
||||
self.board = board
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Opened project: {os.path.basename(board_path)}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(board_path))[0],
|
||||
"path": filename,
|
||||
"boardPath": board_path
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to open project",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def save_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Save the current KiCAD project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
filename = params.get("filename")
|
||||
if filename:
|
||||
# Save to new location
|
||||
filename = os.path.abspath(os.path.expanduser(filename))
|
||||
self.board.SetFileName(filename)
|
||||
|
||||
# Save the board
|
||||
pcbnew.SaveBoard(self.board.GetFileName(), self.board)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Saved project to: {self.board.GetFileName()}",
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(self.board.GetFileName()))[0],
|
||||
"path": self.board.GetFileName()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving project: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to save project",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_project_info(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get information about the current project"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
title_block = self.board.GetTitleBlock()
|
||||
filename = self.board.GetFileName()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"project": {
|
||||
"name": os.path.splitext(os.path.basename(filename))[0],
|
||||
"path": filename,
|
||||
"title": title_block.GetTitle(),
|
||||
"date": title_block.GetDate(),
|
||||
"revision": title_block.GetRevision(),
|
||||
"company": title_block.GetCompany(),
|
||||
"comment1": title_block.GetComment(0),
|
||||
"comment2": title_block.GetComment(1),
|
||||
"comment3": title_block.GetComment(2),
|
||||
"comment4": title_block.GetComment(3)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting project info: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get project information",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
740
python/commands/routing.py
Normal file
740
python/commands/routing.py
Normal file
@@ -0,0 +1,740 @@
|
||||
"""
|
||||
Routing-related command implementations for KiCAD interface
|
||||
"""
|
||||
|
||||
import os
|
||||
import pcbnew
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, Any, Optional, List, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
|
||||
class RoutingCommands:
|
||||
"""Handles routing-related KiCAD operations"""
|
||||
|
||||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||||
"""Initialize with optional board instance"""
|
||||
self.board = board
|
||||
|
||||
def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a new net to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
net_class = params.get("class")
|
||||
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing net name",
|
||||
"errorDetails": "name parameter is required"
|
||||
}
|
||||
|
||||
# Create new net
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net = netinfo.FindNet(name)
|
||||
if not net:
|
||||
net = netinfo.AddNet(name)
|
||||
|
||||
# Set net class if provided
|
||||
if net_class:
|
||||
net_classes = self.board.GetNetClasses()
|
||||
if net_classes.Find(net_class):
|
||||
net.SetClass(net_classes.Find(net_class))
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added net: {name}",
|
||||
"net": {
|
||||
"name": name,
|
||||
"class": net_class if net_class else "Default",
|
||||
"netcode": net.GetNetCode()
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding net: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add net",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Route a trace between two points or pads"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
start = params.get("start")
|
||||
end = params.get("end")
|
||||
layer = params.get("layer", "F.Cu")
|
||||
width = params.get("width")
|
||||
net = params.get("net")
|
||||
via = params.get("via", False)
|
||||
|
||||
if not start or not end:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "start and end points are required"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Get start point
|
||||
start_point = self._get_point(start)
|
||||
end_point = self._get_point(end)
|
||||
|
||||
# Create track segment
|
||||
track = pcbnew.PCB_TRACK(self.board)
|
||||
track.SetStart(start_point)
|
||||
track.SetEnd(end_point)
|
||||
track.SetLayer(layer_id)
|
||||
|
||||
# Set width (default to board's current track width)
|
||||
if width:
|
||||
track.SetWidth(int(width * 1000000)) # Convert mm to nm
|
||||
else:
|
||||
track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
track.SetNet(net_obj)
|
||||
|
||||
# Add track to board
|
||||
self.board.Add(track)
|
||||
|
||||
# Add via if requested and net is specified
|
||||
if via and net:
|
||||
via_point = end_point
|
||||
self.add_via({
|
||||
"position": {
|
||||
"x": via_point.x / 1000000,
|
||||
"y": via_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"net": net
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added trace",
|
||||
"trace": {
|
||||
"start": {
|
||||
"x": start_point.x / 1000000,
|
||||
"y": start_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"end": {
|
||||
"x": end_point.x / 1000000,
|
||||
"y": end_point.y / 1000000,
|
||||
"unit": "mm"
|
||||
},
|
||||
"layer": layer,
|
||||
"width": track.GetWidth() / 1000000,
|
||||
"net": net
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error routing trace: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to route trace",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a via at the specified location"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
position = params.get("position")
|
||||
size = params.get("size")
|
||||
drill = params.get("drill")
|
||||
net = params.get("net")
|
||||
from_layer = params.get("from_layer", "F.Cu")
|
||||
to_layer = params.get("to_layer", "B.Cu")
|
||||
|
||||
if not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing position",
|
||||
"errorDetails": "position parameter is required"
|
||||
}
|
||||
|
||||
# Create via
|
||||
via = pcbnew.PCB_VIA(self.board)
|
||||
|
||||
# Set position
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Set size and drill (default to board's current via settings)
|
||||
design_settings = self.board.GetDesignSettings()
|
||||
via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
|
||||
via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
|
||||
|
||||
# Set layers
|
||||
from_id = self.board.GetLayerID(from_layer)
|
||||
to_id = self.board.GetLayerID(to_layer)
|
||||
if from_id < 0 or to_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": "Specified layers do not exist"
|
||||
}
|
||||
via.SetLayerPair(from_id, to_id)
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
via.SetNet(net_obj)
|
||||
|
||||
# Add via to board
|
||||
self.board.Add(via)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added via",
|
||||
"via": {
|
||||
"position": {
|
||||
"x": position["x"],
|
||||
"y": position["y"],
|
||||
"unit": position["unit"]
|
||||
},
|
||||
"size": via.GetWidth() / 1000000,
|
||||
"drill": via.GetDrill() / 1000000,
|
||||
"from_layer": from_layer,
|
||||
"to_layer": to_layer,
|
||||
"net": net
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding via: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add via",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Delete a trace from the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
trace_uuid = params.get("traceUuid")
|
||||
position = params.get("position")
|
||||
|
||||
if not trace_uuid and not position:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "Either traceUuid or position must be provided"
|
||||
}
|
||||
|
||||
# Find track by UUID
|
||||
if trace_uuid:
|
||||
track = None
|
||||
for item in self.board.Tracks():
|
||||
if str(item.m_Uuid) == trace_uuid:
|
||||
track = item
|
||||
break
|
||||
|
||||
if not track:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Track not found",
|
||||
"errorDetails": f"Could not find track with UUID: {trace_uuid}"
|
||||
}
|
||||
|
||||
self.board.Remove(track)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Deleted track: {trace_uuid}"
|
||||
}
|
||||
|
||||
# Find track by position
|
||||
if position:
|
||||
scale = 1000000 if position["unit"] == "mm" else 25400000 # mm or inch to nm
|
||||
x_nm = int(position["x"] * scale)
|
||||
y_nm = int(position["y"] * scale)
|
||||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||||
|
||||
# Find closest track
|
||||
closest_track = None
|
||||
min_distance = float('inf')
|
||||
for track in self.board.Tracks():
|
||||
dist = self._point_to_track_distance(point, track)
|
||||
if dist < min_distance:
|
||||
min_distance = dist
|
||||
closest_track = track
|
||||
|
||||
if closest_track and min_distance < 1000000: # Within 1mm
|
||||
self.board.Remove(closest_track)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Deleted track at specified position"
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No track found",
|
||||
"errorDetails": "No track found near specified position"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting trace: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to delete trace",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a list of all nets in the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
nets = []
|
||||
netinfo = self.board.GetNetInfo()
|
||||
for net_code in range(netinfo.GetNetCount()):
|
||||
net = netinfo.GetNetItem(net_code)
|
||||
if net:
|
||||
nets.append({
|
||||
"name": net.GetNetname(),
|
||||
"code": net.GetNetCode(),
|
||||
"class": net.GetClassName()
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"nets": nets
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting nets list: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to get nets list",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create a new net class with specified properties"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
name = params.get("name")
|
||||
clearance = params.get("clearance")
|
||||
track_width = params.get("trackWidth")
|
||||
via_diameter = params.get("viaDiameter")
|
||||
via_drill = params.get("viaDrill")
|
||||
uvia_diameter = params.get("uviaDiameter")
|
||||
uvia_drill = params.get("uviaDrill")
|
||||
diff_pair_width = params.get("diffPairWidth")
|
||||
diff_pair_gap = params.get("diffPairGap")
|
||||
nets = params.get("nets", [])
|
||||
|
||||
if not name:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing netclass name",
|
||||
"errorDetails": "name parameter is required"
|
||||
}
|
||||
|
||||
# Get net classes
|
||||
net_classes = self.board.GetNetClasses()
|
||||
|
||||
# Create new net class if it doesn't exist
|
||||
if not net_classes.Find(name):
|
||||
netclass = pcbnew.NETCLASS(name)
|
||||
net_classes.Add(netclass)
|
||||
else:
|
||||
netclass = net_classes.Find(name)
|
||||
|
||||
# Set properties
|
||||
scale = 1000000 # mm to nm
|
||||
if clearance is not None:
|
||||
netclass.SetClearance(int(clearance * scale))
|
||||
if track_width is not None:
|
||||
netclass.SetTrackWidth(int(track_width * scale))
|
||||
if via_diameter is not None:
|
||||
netclass.SetViaDiameter(int(via_diameter * scale))
|
||||
if via_drill is not None:
|
||||
netclass.SetViaDrill(int(via_drill * scale))
|
||||
if uvia_diameter is not None:
|
||||
netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
|
||||
if uvia_drill is not None:
|
||||
netclass.SetMicroViaDrill(int(uvia_drill * scale))
|
||||
if diff_pair_width is not None:
|
||||
netclass.SetDiffPairWidth(int(diff_pair_width * scale))
|
||||
if diff_pair_gap is not None:
|
||||
netclass.SetDiffPairGap(int(diff_pair_gap * scale))
|
||||
|
||||
# Add nets to net class
|
||||
netinfo = self.board.GetNetInfo()
|
||||
for net_name in nets:
|
||||
net = netinfo.FindNet(net_name)
|
||||
if net:
|
||||
net.SetClass(netclass)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Created net class: {name}",
|
||||
"netClass": {
|
||||
"name": name,
|
||||
"clearance": netclass.GetClearance() / scale,
|
||||
"trackWidth": netclass.GetTrackWidth() / scale,
|
||||
"viaDiameter": netclass.GetViaDiameter() / scale,
|
||||
"viaDrill": netclass.GetViaDrill() / scale,
|
||||
"uviaDiameter": netclass.GetMicroViaDiameter() / scale,
|
||||
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
||||
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
||||
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
||||
"nets": nets
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating net class: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to create net class",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Add a copper pour (zone) to the PCB"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
layer = params.get("layer", "F.Cu")
|
||||
net = params.get("net")
|
||||
clearance = params.get("clearance")
|
||||
min_width = params.get("minWidth", 0.2)
|
||||
points = params.get("points", [])
|
||||
priority = params.get("priority", 0)
|
||||
fill_type = params.get("fillType", "solid") # solid or hatched
|
||||
|
||||
if not points or len(points) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing points",
|
||||
"errorDetails": "At least 3 points are required for copper pour outline"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Create zone
|
||||
zone = pcbnew.ZONE(self.board)
|
||||
zone.SetLayer(layer_id)
|
||||
|
||||
# Set net if provided
|
||||
if net:
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_obj = netinfo.FindNet(net)
|
||||
if net_obj:
|
||||
zone.SetNet(net_obj)
|
||||
|
||||
# Set zone properties
|
||||
scale = 1000000 # mm to nm
|
||||
zone.SetPriority(priority)
|
||||
|
||||
if clearance is not None:
|
||||
zone.SetLocalClearance(int(clearance * scale))
|
||||
|
||||
zone.SetMinThickness(int(min_width * scale))
|
||||
|
||||
# Set fill type
|
||||
if fill_type == "hatched":
|
||||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
|
||||
else:
|
||||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
|
||||
|
||||
# Create outline
|
||||
outline = zone.Outline()
|
||||
|
||||
# Add points to outline
|
||||
for point in points:
|
||||
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(point["x"] * scale)
|
||||
y_nm = int(point["y"] * scale)
|
||||
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
|
||||
|
||||
# Add zone to board
|
||||
self.board.Add(zone)
|
||||
|
||||
# Fill zone
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
filler.Fill(self.board.Zones())
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added copper pour",
|
||||
"pour": {
|
||||
"layer": layer,
|
||||
"net": net,
|
||||
"clearance": clearance,
|
||||
"minWidth": min_width,
|
||||
"priority": priority,
|
||||
"fillType": fill_type,
|
||||
"pointCount": len(points)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding copper pour: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to add copper pour",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Route a differential pair between two sets of points or pads"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
start_pos = params.get("startPos")
|
||||
end_pos = params.get("endPos")
|
||||
net_pos = params.get("netPos")
|
||||
net_neg = params.get("netNeg")
|
||||
layer = params.get("layer", "F.Cu")
|
||||
width = params.get("width")
|
||||
gap = params.get("gap")
|
||||
|
||||
if not start_pos or not end_pos or not net_pos or not net_neg:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Missing parameters",
|
||||
"errorDetails": "startPos, endPos, netPos, and netNeg are required"
|
||||
}
|
||||
|
||||
# Get layer ID
|
||||
layer_id = self.board.GetLayerID(layer)
|
||||
if layer_id < 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid layer",
|
||||
"errorDetails": f"Layer '{layer}' does not exist"
|
||||
}
|
||||
|
||||
# Get nets
|
||||
netinfo = self.board.GetNetInfo()
|
||||
net_pos_obj = netinfo.FindNet(net_pos)
|
||||
net_neg_obj = netinfo.FindNet(net_neg)
|
||||
|
||||
if not net_pos_obj or not net_neg_obj:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Nets not found",
|
||||
"errorDetails": "One or both nets specified for the differential pair do not exist"
|
||||
}
|
||||
|
||||
# Get start and end points
|
||||
start_point = self._get_point(start_pos)
|
||||
end_point = self._get_point(end_pos)
|
||||
|
||||
# Calculate offset vectors for the two traces
|
||||
# First, get the direction vector from start to end
|
||||
dx = end_point.x - start_point.x
|
||||
dy = end_point.y - start_point.y
|
||||
length = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if length <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Invalid points",
|
||||
"errorDetails": "Start and end points must be different"
|
||||
}
|
||||
|
||||
# Normalize direction vector
|
||||
dx /= length
|
||||
dy /= length
|
||||
|
||||
# Get perpendicular vector
|
||||
px = -dy
|
||||
py = dx
|
||||
|
||||
# Set default gap if not provided
|
||||
if gap is None:
|
||||
gap = 0.2 # mm
|
||||
|
||||
# Convert to nm
|
||||
gap_nm = int(gap * 1000000)
|
||||
|
||||
# Calculate offsets
|
||||
offset_x = int(px * gap_nm / 2)
|
||||
offset_y = int(py * gap_nm / 2)
|
||||
|
||||
# Create positive and negative trace points
|
||||
pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
|
||||
pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
|
||||
neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
|
||||
neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
|
||||
|
||||
# Create positive trace
|
||||
pos_track = pcbnew.PCB_TRACK(self.board)
|
||||
pos_track.SetStart(pos_start)
|
||||
pos_track.SetEnd(pos_end)
|
||||
pos_track.SetLayer(layer_id)
|
||||
pos_track.SetNet(net_pos_obj)
|
||||
|
||||
# Create negative trace
|
||||
neg_track = pcbnew.PCB_TRACK(self.board)
|
||||
neg_track.SetStart(neg_start)
|
||||
neg_track.SetEnd(neg_end)
|
||||
neg_track.SetLayer(layer_id)
|
||||
neg_track.SetNet(net_neg_obj)
|
||||
|
||||
# Set width
|
||||
if width:
|
||||
trace_width_nm = int(width * 1000000)
|
||||
pos_track.SetWidth(trace_width_nm)
|
||||
neg_track.SetWidth(trace_width_nm)
|
||||
else:
|
||||
# Get default width from design rules or net class
|
||||
trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
|
||||
pos_track.SetWidth(trace_width)
|
||||
neg_track.SetWidth(trace_width)
|
||||
|
||||
# Add tracks to board
|
||||
self.board.Add(pos_track)
|
||||
self.board.Add(neg_track)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Added differential pair traces",
|
||||
"diffPair": {
|
||||
"posNet": net_pos,
|
||||
"negNet": net_neg,
|
||||
"layer": layer,
|
||||
"width": pos_track.GetWidth() / 1000000,
|
||||
"gap": gap,
|
||||
"length": length / 1000000
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error routing differential pair: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to route differential pair",
|
||||
"errorDetails": str(e)
|
||||
}
|
||||
|
||||
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
||||
"""Convert point specification to KiCAD point"""
|
||||
if "x" in point_spec and "y" in point_spec:
|
||||
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
|
||||
x_nm = int(point_spec["x"] * scale)
|
||||
y_nm = int(point_spec["y"] * scale)
|
||||
return pcbnew.VECTOR2I(x_nm, y_nm)
|
||||
elif "pad" in point_spec and "componentRef" in point_spec:
|
||||
module = self.board.FindFootprintByReference(point_spec["componentRef"])
|
||||
if module:
|
||||
pad = module.FindPadByName(point_spec["pad"])
|
||||
if pad:
|
||||
return pad.GetPosition()
|
||||
raise ValueError("Invalid point specification")
|
||||
|
||||
def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
|
||||
"""Calculate distance from point to track segment"""
|
||||
start = track.GetStart()
|
||||
end = track.GetEnd()
|
||||
|
||||
# Vector from start to end
|
||||
v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
|
||||
# Vector from start to point
|
||||
w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
|
||||
|
||||
# Length of track squared
|
||||
c1 = v.x * v.x + v.y * v.y
|
||||
if c1 == 0:
|
||||
return self._point_distance(point, start)
|
||||
|
||||
# Projection coefficient
|
||||
c2 = float(w.x * v.x + w.y * v.y) / c1
|
||||
|
||||
if c2 < 0:
|
||||
return self._point_distance(point, start)
|
||||
elif c2 > 1:
|
||||
return self._point_distance(point, end)
|
||||
|
||||
# Point on line
|
||||
proj = pcbnew.VECTOR2I(
|
||||
int(start.x + c2 * v.x),
|
||||
int(start.y + c2 * v.y)
|
||||
)
|
||||
return self._point_distance(point, proj)
|
||||
|
||||
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
||||
"""Calculate distance between two points"""
|
||||
dx = p1.x - p2.x
|
||||
dy = p1.y - p2.y
|
||||
return (dx * dx + dy * dy) ** 0.5
|
||||
96
python/commands/schematic.py
Normal file
96
python/commands/schematic.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from skip import Schematic
|
||||
import os
|
||||
|
||||
class SchematicManager:
|
||||
"""Core schematic operations using kicad-skip"""
|
||||
|
||||
@staticmethod
|
||||
def create_schematic(name, metadata=None):
|
||||
"""Create a new empty schematic"""
|
||||
# kicad-skip requires a filepath to create a schematic
|
||||
# We'll create a blank schematic file by loading an existing file
|
||||
# or we can create a template file first.
|
||||
|
||||
# Create an empty template file first
|
||||
temp_path = f"{name}_template.kicad_sch"
|
||||
with open(temp_path, 'w') as f:
|
||||
# Write minimal schematic file content
|
||||
f.write("(kicad_sch (version 20230121) (generator \"KiCAD-MCP-Server\"))\n")
|
||||
|
||||
# Now load it
|
||||
sch = Schematic(temp_path)
|
||||
sch.version = "20230121" # Set appropriate version
|
||||
sch.generator = "KiCAD-MCP-Server"
|
||||
|
||||
# Clean up the template
|
||||
os.remove(temp_path)
|
||||
# Add metadata if provided
|
||||
if metadata:
|
||||
for key, value in metadata.items():
|
||||
# kicad-skip doesn't have a direct metadata property on Schematic,
|
||||
# but we can add properties to the root sheet if needed, or
|
||||
# include it in the file path/name convention.
|
||||
# For now, we'll just create the schematic.
|
||||
pass # Placeholder for potential metadata handling
|
||||
|
||||
print(f"Created new schematic: {name}")
|
||||
return sch
|
||||
|
||||
@staticmethod
|
||||
def load_schematic(file_path):
|
||||
"""Load an existing schematic"""
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Error: Schematic file not found at {file_path}")
|
||||
return None
|
||||
try:
|
||||
sch = Schematic(file_path)
|
||||
print(f"Loaded schematic from: {file_path}")
|
||||
return sch
|
||||
except Exception as e:
|
||||
print(f"Error loading schematic from {file_path}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def save_schematic(schematic, file_path):
|
||||
"""Save a schematic to file"""
|
||||
try:
|
||||
# kicad-skip uses write method, not save
|
||||
schematic.write(file_path)
|
||||
print(f"Saved schematic to: {file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving schematic to {file_path}: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_schematic_metadata(schematic):
|
||||
"""Extract metadata from schematic"""
|
||||
# kicad-skip doesn't expose a direct metadata object on Schematic.
|
||||
# We can return basic info like version and generator.
|
||||
metadata = {
|
||||
"version": schematic.version,
|
||||
"generator": schematic.generator,
|
||||
# Add other relevant properties if needed
|
||||
}
|
||||
print("Extracted schematic metadata")
|
||||
return metadata
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing)
|
||||
# Create a new schematic
|
||||
new_sch = SchematicManager.create_schematic("MyTestSchematic")
|
||||
|
||||
# Save the schematic
|
||||
test_file = "test_schematic.kicad_sch"
|
||||
SchematicManager.save_schematic(new_sch, test_file)
|
||||
|
||||
# Load the schematic
|
||||
loaded_sch = SchematicManager.load_schematic(test_file)
|
||||
if loaded_sch:
|
||||
metadata = SchematicManager.get_schematic_metadata(loaded_sch)
|
||||
print(f"Loaded schematic metadata: {metadata}")
|
||||
|
||||
# Clean up test file
|
||||
if os.path.exists(test_file):
|
||||
os.remove(test_file)
|
||||
print(f"Cleaned up {test_file}")
|
||||
Reference in New Issue
Block a user