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:
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")
|
||||
Reference in New Issue
Block a user