style: apply black formatting to changed files
This commit is contained in:
@@ -7,7 +7,8 @@ import logging
|
|||||||
import math
|
import math
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class BoardOutlineCommands:
|
class BoardOutlineCommands:
|
||||||
"""Handles board outline operations"""
|
"""Handles board outline operations"""
|
||||||
@@ -23,7 +24,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
shape = params.get("shape", "rectangle")
|
shape = params.get("shape", "rectangle")
|
||||||
@@ -40,7 +41,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid shape",
|
"message": "Invalid shape",
|
||||||
"errorDetails": f"Shape '{shape}' not supported"
|
"errorDetails": f"Shape '{shape}' not supported",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
@@ -54,7 +55,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing dimensions",
|
"message": "Missing dimensions",
|
||||||
"errorDetails": "Both width and height are required for rectangle"
|
"errorDetails": "Both width and height are required for rectangle",
|
||||||
}
|
}
|
||||||
|
|
||||||
width_nm = int(width * scale)
|
width_nm = int(width * scale)
|
||||||
@@ -63,10 +64,18 @@ class BoardOutlineCommands:
|
|||||||
center_y_nm = int(center_y * scale)
|
center_y_nm = int(center_y * scale)
|
||||||
|
|
||||||
# Create rectangle
|
# Create rectangle
|
||||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
top_left = pcbnew.VECTOR2I(
|
||||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
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)
|
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
|
# Add lines for rectangle
|
||||||
self._add_edge_line(top_left, top_right, edge_layer)
|
self._add_edge_line(top_left, top_right, edge_layer)
|
||||||
@@ -79,7 +88,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing dimensions",
|
"message": "Missing dimensions",
|
||||||
"errorDetails": "Both width and height are required for rounded rectangle"
|
"errorDetails": "Both width and height are required for rounded rectangle",
|
||||||
}
|
}
|
||||||
|
|
||||||
width_nm = int(width * scale)
|
width_nm = int(width * scale)
|
||||||
@@ -90,9 +99,12 @@ class BoardOutlineCommands:
|
|||||||
|
|
||||||
# Create rounded rectangle
|
# Create rounded rectangle
|
||||||
self._add_rounded_rect(
|
self._add_rounded_rect(
|
||||||
center_x_nm, center_y_nm,
|
center_x_nm,
|
||||||
width_nm, height_nm,
|
center_y_nm,
|
||||||
corner_radius_nm, edge_layer
|
width_nm,
|
||||||
|
height_nm,
|
||||||
|
corner_radius_nm,
|
||||||
|
edge_layer,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif shape == "circle":
|
elif shape == "circle":
|
||||||
@@ -100,7 +112,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing radius",
|
"message": "Missing radius",
|
||||||
"errorDetails": "Radius is required for circle"
|
"errorDetails": "Radius is required for circle",
|
||||||
}
|
}
|
||||||
|
|
||||||
center_x_nm = int(center_x * scale)
|
center_x_nm = int(center_x * scale)
|
||||||
@@ -121,7 +133,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing points",
|
"message": "Missing points",
|
||||||
"errorDetails": "At least 3 points are required for polygon"
|
"errorDetails": "At least 3 points are required for polygon",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert points to nm
|
# Convert points to nm
|
||||||
@@ -136,7 +148,7 @@ class BoardOutlineCommands:
|
|||||||
self._add_edge_line(
|
self._add_edge_line(
|
||||||
polygon_points[i],
|
polygon_points[i],
|
||||||
polygon_points[(i + 1) % len(polygon_points)],
|
polygon_points[(i + 1) % len(polygon_points)],
|
||||||
edge_layer
|
edge_layer,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -149,8 +161,8 @@ class BoardOutlineCommands:
|
|||||||
"center": {"x": center_x, "y": center_y, "unit": unit},
|
"center": {"x": center_x, "y": center_y, "unit": unit},
|
||||||
"radius": radius,
|
"radius": radius,
|
||||||
"cornerRadius": corner_radius,
|
"cornerRadius": corner_radius,
|
||||||
"points": points
|
"points": points,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -158,7 +170,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add board outline",
|
"message": "Failed to add board outline",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def add_mounting_hole(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -168,7 +180,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
position = params.get("position")
|
position = params.get("position")
|
||||||
@@ -180,19 +192,24 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing parameters",
|
"message": "Missing parameters",
|
||||||
"errorDetails": "position and diameter are required"
|
"errorDetails": "position and diameter are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if position.get("unit", "mm") == "mm" else 25400000
|
||||||
|
) # mm or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
diameter_nm = int(diameter * scale)
|
diameter_nm = int(diameter * scale)
|
||||||
pad_diameter_nm = int(pad_diameter * scale) if pad_diameter else diameter_nm + scale # 1mm larger by default
|
pad_diameter_nm = (
|
||||||
|
int(pad_diameter * scale) if pad_diameter else diameter_nm + scale
|
||||||
|
) # 1mm larger by default
|
||||||
|
|
||||||
# Create footprint for mounting hole with unique reference
|
# Create footprint for mounting hole with unique reference
|
||||||
existing_mh = [
|
existing_mh = [
|
||||||
fp.GetReference() for fp in self.board.GetFootprints()
|
fp.GetReference()
|
||||||
|
for fp in self.board.GetFootprints()
|
||||||
if fp.GetReference().startswith("MH")
|
if fp.GetReference().startswith("MH")
|
||||||
]
|
]
|
||||||
next_num = 1
|
next_num = 1
|
||||||
@@ -207,7 +224,9 @@ class BoardOutlineCommands:
|
|||||||
pad = pcbnew.PAD(module)
|
pad = pcbnew.PAD(module)
|
||||||
pad.SetNumber(1)
|
pad.SetNumber(1)
|
||||||
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
|
pad.SetShape(pcbnew.PAD_SHAPE_CIRCLE)
|
||||||
pad.SetAttribute(pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH)
|
pad.SetAttribute(
|
||||||
|
pcbnew.PAD_ATTRIB_PTH if plated else pcbnew.PAD_ATTRIB_NPTH
|
||||||
|
)
|
||||||
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
|
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
|
||||||
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
|
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
|
||||||
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
|
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
|
||||||
@@ -226,8 +245,8 @@ class BoardOutlineCommands:
|
|||||||
"position": position,
|
"position": position,
|
||||||
"diameter": diameter,
|
"diameter": diameter,
|
||||||
"padDiameter": pad_diameter or diameter + 1,
|
"padDiameter": pad_diameter or diameter + 1,
|
||||||
"plated": plated
|
"plated": plated,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -235,7 +254,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add mounting hole",
|
"message": "Failed to add mounting hole",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def add_text(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -245,7 +264,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
text = params.get("text")
|
text = params.get("text")
|
||||||
@@ -260,11 +279,13 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing parameters",
|
"message": "Missing parameters",
|
||||||
"errorDetails": "text and position are required"
|
"errorDetails": "text and position are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert to internal units (nanometers)
|
# Convert to internal units (nanometers)
|
||||||
scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000 # mm or inch to nm
|
scale = (
|
||||||
|
1000000 if position.get("unit", "mm") == "mm" else 25400000
|
||||||
|
) # mm or inch to nm
|
||||||
x_nm = int(position["x"] * scale)
|
x_nm = int(position["x"] * scale)
|
||||||
y_nm = int(position["y"] * scale)
|
y_nm = int(position["y"] * scale)
|
||||||
size_nm = int(size * scale)
|
size_nm = int(size * scale)
|
||||||
@@ -276,7 +297,7 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Invalid layer",
|
"message": "Invalid layer",
|
||||||
"errorDetails": f"Layer '{layer}' does not exist"
|
"errorDetails": f"Layer '{layer}' does not exist",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create text
|
# Create text
|
||||||
@@ -311,8 +332,8 @@ class BoardOutlineCommands:
|
|||||||
"size": size,
|
"size": size,
|
||||||
"thickness": thickness,
|
"thickness": thickness,
|
||||||
"rotation": rotation,
|
"rotation": rotation,
|
||||||
"mirror": mirror
|
"mirror": mirror,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -320,10 +341,12 @@ class BoardOutlineCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to add text",
|
"message": "Failed to add text",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _add_edge_line(self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int) -> None:
|
def _add_edge_line(
|
||||||
|
self, start: pcbnew.VECTOR2I, end: pcbnew.VECTOR2I, layer: int
|
||||||
|
) -> None:
|
||||||
"""Add a line to the edge cuts layer"""
|
"""Add a line to the edge cuts layer"""
|
||||||
line = pcbnew.PCB_SHAPE(self.board)
|
line = pcbnew.PCB_SHAPE(self.board)
|
||||||
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
line.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||||
@@ -333,16 +356,30 @@ class BoardOutlineCommands:
|
|||||||
line.SetWidth(0) # Zero width for edge cuts
|
line.SetWidth(0) # Zero width for edge cuts
|
||||||
self.board.Add(line)
|
self.board.Add(line)
|
||||||
|
|
||||||
def _add_rounded_rect(self, center_x_nm: int, center_y_nm: int,
|
def _add_rounded_rect(
|
||||||
width_nm: int, height_nm: int,
|
self,
|
||||||
radius_nm: int, layer: int) -> None:
|
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"""
|
"""Add a rounded rectangle to the edge cuts layer"""
|
||||||
if radius_nm <= 0:
|
if radius_nm <= 0:
|
||||||
# If no radius, create regular rectangle
|
# If no radius, create regular rectangle
|
||||||
top_left = pcbnew.VECTOR2I(center_x_nm - width_nm // 2, center_y_nm - height_nm // 2)
|
top_left = pcbnew.VECTOR2I(
|
||||||
top_right = pcbnew.VECTOR2I(center_x_nm + width_nm // 2, center_y_nm - height_nm // 2)
|
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)
|
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_left, top_right, layer)
|
||||||
self._add_edge_line(top_right, bottom_right, layer)
|
self._add_edge_line(top_right, bottom_right, layer)
|
||||||
@@ -361,20 +398,16 @@ class BoardOutlineCommands:
|
|||||||
|
|
||||||
# Calculate corner centers
|
# Calculate corner centers
|
||||||
top_left_center = pcbnew.VECTOR2I(
|
top_left_center = pcbnew.VECTOR2I(
|
||||||
center_x_nm - half_width + radius_nm,
|
center_x_nm - half_width + radius_nm, center_y_nm - half_height + radius_nm
|
||||||
center_y_nm - half_height + radius_nm
|
|
||||||
)
|
)
|
||||||
top_right_center = pcbnew.VECTOR2I(
|
top_right_center = pcbnew.VECTOR2I(
|
||||||
center_x_nm + half_width - radius_nm,
|
center_x_nm + half_width - radius_nm, center_y_nm - half_height + radius_nm
|
||||||
center_y_nm - half_height + radius_nm
|
|
||||||
)
|
)
|
||||||
bottom_right_center = pcbnew.VECTOR2I(
|
bottom_right_center = pcbnew.VECTOR2I(
|
||||||
center_x_nm + half_width - radius_nm,
|
center_x_nm + half_width - radius_nm, center_y_nm + half_height - radius_nm
|
||||||
center_y_nm + half_height - radius_nm
|
|
||||||
)
|
)
|
||||||
bottom_left_center = pcbnew.VECTOR2I(
|
bottom_left_center = pcbnew.VECTOR2I(
|
||||||
center_x_nm - half_width + radius_nm,
|
center_x_nm - half_width + radius_nm, center_y_nm + half_height - radius_nm
|
||||||
center_y_nm + half_height - radius_nm
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add arcs for corners
|
# Add arcs for corners
|
||||||
@@ -388,29 +421,35 @@ class BoardOutlineCommands:
|
|||||||
self._add_edge_line(
|
self._add_edge_line(
|
||||||
pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
|
pcbnew.VECTOR2I(top_left_center.x, top_left_center.y - radius_nm),
|
||||||
pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
|
pcbnew.VECTOR2I(top_right_center.x, top_right_center.y - radius_nm),
|
||||||
layer
|
layer,
|
||||||
)
|
)
|
||||||
# Right edge
|
# Right edge
|
||||||
self._add_edge_line(
|
self._add_edge_line(
|
||||||
pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
|
pcbnew.VECTOR2I(top_right_center.x + radius_nm, top_right_center.y),
|
||||||
pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
|
pcbnew.VECTOR2I(bottom_right_center.x + radius_nm, bottom_right_center.y),
|
||||||
layer
|
layer,
|
||||||
)
|
)
|
||||||
# Bottom edge
|
# Bottom edge
|
||||||
self._add_edge_line(
|
self._add_edge_line(
|
||||||
pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
|
pcbnew.VECTOR2I(bottom_right_center.x, bottom_right_center.y + radius_nm),
|
||||||
pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
|
pcbnew.VECTOR2I(bottom_left_center.x, bottom_left_center.y + radius_nm),
|
||||||
layer
|
layer,
|
||||||
)
|
)
|
||||||
# Left edge
|
# Left edge
|
||||||
self._add_edge_line(
|
self._add_edge_line(
|
||||||
pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
|
pcbnew.VECTOR2I(bottom_left_center.x - radius_nm, bottom_left_center.y),
|
||||||
pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
|
pcbnew.VECTOR2I(top_left_center.x - radius_nm, top_left_center.y),
|
||||||
layer
|
layer,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _add_corner_arc(self, center: pcbnew.VECTOR2I, radius: int,
|
def _add_corner_arc(
|
||||||
start_angle: float, end_angle: float, layer: int) -> None:
|
self,
|
||||||
|
center: pcbnew.VECTOR2I,
|
||||||
|
radius: int,
|
||||||
|
start_angle: float,
|
||||||
|
end_angle: float,
|
||||||
|
layer: int,
|
||||||
|
) -> None:
|
||||||
"""Add an arc for a rounded corner"""
|
"""Add an arc for a rounded corner"""
|
||||||
# Create arc for corner
|
# Create arc for corner
|
||||||
arc = pcbnew.PCB_SHAPE(self.board)
|
arc = pcbnew.PCB_SHAPE(self.board)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import logging
|
|||||||
from typing import Dict, Any, Optional, List, Tuple
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
logger = logging.getLogger('kicad_interface')
|
logger = logging.getLogger("kicad_interface")
|
||||||
|
|
||||||
|
|
||||||
class ExportCommands:
|
class ExportCommands:
|
||||||
"""Handles export-related KiCAD operations"""
|
"""Handles export-related KiCAD operations"""
|
||||||
@@ -24,7 +25,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
output_dir = params.get("outputDir")
|
output_dir = params.get("outputDir")
|
||||||
@@ -38,7 +39,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing output directory",
|
"message": "Missing output directory",
|
||||||
"errorDetails": "outputDir parameter is required"
|
"errorDetails": "outputDir parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
@@ -84,28 +85,40 @@ class ExportCommands:
|
|||||||
|
|
||||||
if kicad_cli and board_file and os.path.exists(board_file):
|
if kicad_cli and board_file and os.path.exists(board_file):
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
# Generate drill files using kicad-cli
|
# Generate drill files using kicad-cli
|
||||||
cmd = [
|
cmd = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb', 'export', 'drill',
|
"pcb",
|
||||||
'--output', output_dir,
|
"export",
|
||||||
'--format', 'excellon',
|
"drill",
|
||||||
'--drill-origin', 'absolute',
|
"--output",
|
||||||
'--excellon-separate-th', # Separate plated/non-plated
|
output_dir,
|
||||||
board_file
|
"--format",
|
||||||
|
"excellon",
|
||||||
|
"--drill-origin",
|
||||||
|
"absolute",
|
||||||
|
"--excellon-separate-th", # Separate plated/non-plated
|
||||||
|
board_file,
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
result = subprocess.run(
|
||||||
|
cmd, capture_output=True, text=True, timeout=60
|
||||||
|
)
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
# Get list of generated drill files
|
# Get list of generated drill files
|
||||||
for file in os.listdir(output_dir):
|
for file in os.listdir(output_dir):
|
||||||
if file.endswith((".drl", ".cnc")):
|
if file.endswith((".drl", ".cnc")):
|
||||||
drill_files.append(file)
|
drill_files.append(file)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Drill file generation failed: {result.stderr}")
|
logger.warning(
|
||||||
|
f"Drill file generation failed: {result.stderr}"
|
||||||
|
)
|
||||||
except Exception as drill_error:
|
except Exception as drill_error:
|
||||||
logger.warning(f"Could not generate drill files: {str(drill_error)}")
|
logger.warning(
|
||||||
|
f"Could not generate drill files: {str(drill_error)}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning("kicad-cli not available for drill file generation")
|
logger.warning("kicad-cli not available for drill file generation")
|
||||||
|
|
||||||
@@ -115,9 +128,9 @@ class ExportCommands:
|
|||||||
"files": {
|
"files": {
|
||||||
"gerber": plotted_layers,
|
"gerber": plotted_layers,
|
||||||
"drill": drill_files,
|
"drill": drill_files,
|
||||||
"map": ["job.gbrjob"] if generate_map_file else []
|
"map": ["job.gbrjob"] if generate_map_file else [],
|
||||||
},
|
},
|
||||||
"outputDir": output_dir
|
"outputDir": output_dir,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -125,7 +138,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to export Gerber files",
|
"message": "Failed to export Gerber files",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def export_pdf(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -135,7 +148,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
output_path = params.get("outputPath")
|
output_path = params.get("outputPath")
|
||||||
@@ -148,7 +161,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing output path",
|
"message": "Missing output path",
|
||||||
"errorDetails": "outputPath parameter is required"
|
"errorDetails": "outputPath parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
@@ -180,13 +193,15 @@ class ExportCommands:
|
|||||||
plot_opts.SetAutoScale(True)
|
plot_opts.SetAutoScale(True)
|
||||||
# Note: KiCAD 9.0 doesn't support explicit page size selection
|
# Note: KiCAD 9.0 doesn't support explicit page size selection
|
||||||
# for formats other than A4. The PDF will auto-scale to fit.
|
# for formats other than A4. The PDF will auto-scale to fit.
|
||||||
logger.warning(f"Page size '{page_size}' requested, but KiCAD 9.0 only supports A4 explicitly. Using auto-scale instead.")
|
logger.warning(
|
||||||
|
f"Page size '{page_size}' requested, but KiCAD 9.0 only supports A4 explicitly. Using auto-scale instead."
|
||||||
|
)
|
||||||
|
|
||||||
# Open plot for writing
|
# Open plot for writing
|
||||||
# Note: For PDF, all layers are combined into a single file
|
# Note: For PDF, all layers are combined into a single file
|
||||||
# KiCAD prepends the board filename to the plot file name
|
# KiCAD prepends the board filename to the plot file name
|
||||||
base_name = os.path.basename(output_path).replace('.pdf', '')
|
base_name = os.path.basename(output_path).replace(".pdf", "")
|
||||||
plotter.OpenPlotfile(base_name, pcbnew.PLOT_FORMAT_PDF, '')
|
plotter.OpenPlotfile(base_name, pcbnew.PLOT_FORMAT_PDF, "")
|
||||||
|
|
||||||
# Plot specified layers or all enabled layers
|
# Plot specified layers or all enabled layers
|
||||||
plotted_layers = []
|
plotted_layers = []
|
||||||
@@ -212,7 +227,9 @@ class ExportCommands:
|
|||||||
# Get the actual output filename that was created
|
# Get the actual output filename that was created
|
||||||
board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0]
|
board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0]
|
||||||
actual_filename = f"{board_name}-{base_name}.pdf"
|
actual_filename = f"{board_name}-{base_name}.pdf"
|
||||||
actual_output_path = os.path.join(os.path.dirname(output_path), actual_filename)
|
actual_output_path = os.path.join(
|
||||||
|
os.path.dirname(output_path), actual_filename
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -221,8 +238,8 @@ class ExportCommands:
|
|||||||
"path": actual_output_path,
|
"path": actual_output_path,
|
||||||
"requestedPath": output_path,
|
"requestedPath": output_path,
|
||||||
"layers": plotted_layers,
|
"layers": plotted_layers,
|
||||||
"pageSize": page_size if page_size == "A4" else "auto-scaled"
|
"pageSize": page_size if page_size == "A4" else "auto-scaled",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -230,7 +247,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to export PDF file",
|
"message": "Failed to export PDF file",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def export_svg(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -240,7 +257,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
output_path = params.get("outputPath")
|
output_path = params.get("outputPath")
|
||||||
@@ -252,7 +269,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing output path",
|
"message": "Missing output path",
|
||||||
"errorDetails": "outputPath parameter is required"
|
"errorDetails": "outputPath parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
@@ -290,10 +307,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "Exported SVG file",
|
"message": "Exported SVG file",
|
||||||
"file": {
|
"file": {"path": output_path, "layers": plotted_layers},
|
||||||
"path": output_path,
|
|
||||||
"layers": plotted_layers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -301,7 +315,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to export SVG file",
|
"message": "Failed to export SVG file",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def export_3d(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -315,7 +329,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
output_path = params.get("outputPath")
|
output_path = params.get("outputPath")
|
||||||
@@ -329,7 +343,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing output path",
|
"message": "Missing output path",
|
||||||
"errorDetails": "outputPath parameter is required"
|
"errorDetails": "outputPath parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get board file path
|
# Get board file path
|
||||||
@@ -338,7 +352,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Board file not found",
|
"message": "Board file not found",
|
||||||
"errorDetails": "Board must be saved before exporting 3D models"
|
"errorDetails": "Board must be saved before exporting 3D models",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
@@ -351,7 +365,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "kicad-cli not found",
|
"message": "kicad-cli not found",
|
||||||
"errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH."
|
"errorDetails": "KiCAD CLI tool not found. Install KiCAD 8.0+ or set PATH.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Build command based on format
|
# Build command based on format
|
||||||
@@ -360,30 +374,39 @@ class ExportCommands:
|
|||||||
if format_upper == "STEP":
|
if format_upper == "STEP":
|
||||||
cmd = [
|
cmd = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb', 'export', 'step',
|
"pcb",
|
||||||
'--output', output_path,
|
"export",
|
||||||
'--force' # Overwrite existing file
|
"step",
|
||||||
|
"--output",
|
||||||
|
output_path,
|
||||||
|
"--force", # Overwrite existing file
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add options based on parameters
|
# Add options based on parameters
|
||||||
if not include_components:
|
if not include_components:
|
||||||
cmd.append('--no-components')
|
cmd.append("--no-components")
|
||||||
if include_copper:
|
if include_copper:
|
||||||
cmd.extend(['--include-tracks', '--include-pads', '--include-zones'])
|
cmd.extend(
|
||||||
|
["--include-tracks", "--include-pads", "--include-zones"]
|
||||||
|
)
|
||||||
if include_silkscreen:
|
if include_silkscreen:
|
||||||
cmd.append('--include-silkscreen')
|
cmd.append("--include-silkscreen")
|
||||||
if include_solder_mask:
|
if include_solder_mask:
|
||||||
cmd.append('--include-soldermask')
|
cmd.append("--include-soldermask")
|
||||||
|
|
||||||
cmd.append(board_file)
|
cmd.append(board_file)
|
||||||
|
|
||||||
elif format_upper == "VRML":
|
elif format_upper == "VRML":
|
||||||
cmd = [
|
cmd = [
|
||||||
kicad_cli,
|
kicad_cli,
|
||||||
'pcb', 'export', 'vrml',
|
"pcb",
|
||||||
'--output', output_path,
|
"export",
|
||||||
'--units', 'mm', # Use mm for consistency
|
"vrml",
|
||||||
'--force'
|
"--output",
|
||||||
|
output_path,
|
||||||
|
"--units",
|
||||||
|
"mm", # Use mm for consistency
|
||||||
|
"--force",
|
||||||
]
|
]
|
||||||
|
|
||||||
if not include_components:
|
if not include_components:
|
||||||
@@ -397,7 +420,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Unsupported format",
|
"message": "Unsupported format",
|
||||||
"errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'."
|
"errorDetails": f"Format {format} is not supported. Use 'STEP' or 'VRML'.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Execute kicad-cli command
|
# Execute kicad-cli command
|
||||||
@@ -407,7 +430,7 @@ class ExportCommands:
|
|||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300 # 5 minute timeout for 3D export
|
timeout=300, # 5 minute timeout for 3D export
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
@@ -415,16 +438,13 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "3D export command failed",
|
"message": "3D export command failed",
|
||||||
"errorDetails": result.stderr
|
"errorDetails": result.stderr,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Exported {format_upper} file",
|
"message": f"Exported {format_upper} file",
|
||||||
"file": {
|
"file": {"path": output_path, "format": format_upper},
|
||||||
"path": output_path,
|
|
||||||
"format": format_upper
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
@@ -432,14 +452,14 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "3D export timed out",
|
"message": "3D export timed out",
|
||||||
"errorDetails": "Export took longer than 5 minutes"
|
"errorDetails": "Export took longer than 5 minutes",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error exporting 3D model: {str(e)}")
|
logger.error(f"Error exporting 3D model: {str(e)}")
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to export 3D model",
|
"message": "Failed to export 3D model",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def export_bom(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -449,7 +469,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "No board is loaded",
|
"message": "No board is loaded",
|
||||||
"errorDetails": "Load or create a board first"
|
"errorDetails": "Load or create a board first",
|
||||||
}
|
}
|
||||||
|
|
||||||
output_path = params.get("outputPath")
|
output_path = params.get("outputPath")
|
||||||
@@ -461,7 +481,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Missing output path",
|
"message": "Missing output path",
|
||||||
"errorDetails": "outputPath parameter is required"
|
"errorDetails": "outputPath parameter is required",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create output directory if it doesn't exist
|
# Create output directory if it doesn't exist
|
||||||
@@ -475,7 +495,7 @@ class ExportCommands:
|
|||||||
"reference": module.GetReference(),
|
"reference": module.GetReference(),
|
||||||
"value": module.GetValue(),
|
"value": module.GetValue(),
|
||||||
"footprint": module.GetFPID().GetUniStringLibId(),
|
"footprint": module.GetFPID().GetUniStringLibId(),
|
||||||
"layer": self.board.GetLayerName(module.GetLayer())
|
"layer": self.board.GetLayerName(module.GetLayer()),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add requested attributes
|
# Add requested attributes
|
||||||
@@ -495,7 +515,7 @@ class ExportCommands:
|
|||||||
"value": comp["value"],
|
"value": comp["value"],
|
||||||
"footprint": comp["footprint"],
|
"footprint": comp["footprint"],
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"references": [comp["reference"]]
|
"references": [comp["reference"]],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
grouped[key]["quantity"] += 1
|
grouped[key]["quantity"] += 1
|
||||||
@@ -515,7 +535,7 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Unsupported format",
|
"message": "Unsupported format",
|
||||||
"errorDetails": f"Format {format} is not supported"
|
"errorDetails": f"Format {format} is not supported",
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -524,8 +544,8 @@ class ExportCommands:
|
|||||||
"file": {
|
"file": {
|
||||||
"path": output_path,
|
"path": output_path,
|
||||||
"format": format,
|
"format": format,
|
||||||
"componentCount": len(components)
|
"componentCount": len(components),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -533,13 +553,14 @@ class ExportCommands:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Failed to export BOM",
|
"message": "Failed to export BOM",
|
||||||
"errorDetails": str(e)
|
"errorDetails": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
|
def _export_bom_csv(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||||
"""Export BOM to CSV format"""
|
"""Export BOM to CSV format"""
|
||||||
import csv
|
import csv
|
||||||
with open(path, 'w', newline='') as f:
|
|
||||||
|
with open(path, "w", newline="") as f:
|
||||||
writer = csv.DictWriter(f, fieldnames=components[0].keys())
|
writer = csv.DictWriter(f, fieldnames=components[0].keys())
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(components)
|
writer.writerows(components)
|
||||||
@@ -547,6 +568,7 @@ class ExportCommands:
|
|||||||
def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
|
def _export_bom_xml(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||||
"""Export BOM to XML format"""
|
"""Export BOM to XML format"""
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
root = ET.Element("bom")
|
root = ET.Element("bom")
|
||||||
for comp in components:
|
for comp in components:
|
||||||
comp_elem = ET.SubElement(root, "component")
|
comp_elem = ET.SubElement(root, "component")
|
||||||
@@ -554,7 +576,7 @@ class ExportCommands:
|
|||||||
elem = ET.SubElement(comp_elem, key)
|
elem = ET.SubElement(comp_elem, key)
|
||||||
elem.text = str(value)
|
elem.text = str(value)
|
||||||
tree = ET.ElementTree(root)
|
tree = ET.ElementTree(root)
|
||||||
tree.write(path, encoding='utf-8', xml_declaration=True)
|
tree.write(path, encoding="utf-8", xml_declaration=True)
|
||||||
|
|
||||||
def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
|
def _export_bom_html(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||||
"""Export BOM to HTML format"""
|
"""Export BOM to HTML format"""
|
||||||
@@ -571,13 +593,14 @@ class ExportCommands:
|
|||||||
html.append(f"<td>{value}</td>")
|
html.append(f"<td>{value}</td>")
|
||||||
html.append("</tr>")
|
html.append("</tr>")
|
||||||
html.append("</table></body></html>")
|
html.append("</table></body></html>")
|
||||||
with open(path, 'w') as f:
|
with open(path, "w") as f:
|
||||||
f.write("\n".join(html))
|
f.write("\n".join(html))
|
||||||
|
|
||||||
def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
|
def _export_bom_json(self, path: str, components: List[Dict[str, Any]]) -> None:
|
||||||
"""Export BOM to JSON format"""
|
"""Export BOM to JSON format"""
|
||||||
import json
|
import json
|
||||||
with open(path, 'w') as f:
|
|
||||||
|
with open(path, "w") as f:
|
||||||
json.dump({"components": components}, f, indent=2)
|
json.dump({"components": components}, f, indent=2)
|
||||||
|
|
||||||
def _find_kicad_cli(self) -> Optional[str]:
|
def _find_kicad_cli(self) -> Optional[str]:
|
||||||
|
|||||||
@@ -145,18 +145,40 @@ class RoutingCommands:
|
|||||||
net = start_pad.GetNetname() or end_pad.GetNetname() or ""
|
net = start_pad.GetNetname() or end_pad.GetNetname() or ""
|
||||||
|
|
||||||
# Delegate to route_trace
|
# Delegate to route_trace
|
||||||
result = self.route_trace({
|
result = self.route_trace(
|
||||||
"start": {"x": start_pos.x / scale, "y": start_pos.y / scale, "unit": "mm"},
|
{
|
||||||
"end": {"x": end_pos.x / scale, "y": end_pos.y / scale, "unit": "mm"},
|
"start": {
|
||||||
|
"x": start_pos.x / scale,
|
||||||
|
"y": start_pos.y / scale,
|
||||||
|
"unit": "mm",
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"x": end_pos.x / scale,
|
||||||
|
"y": end_pos.y / scale,
|
||||||
|
"unit": "mm",
|
||||||
|
},
|
||||||
"layer": layer,
|
"layer": layer,
|
||||||
"width": width,
|
"width": width,
|
||||||
"net": net,
|
"net": net,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
result["message"] = f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})"
|
result["message"] = (
|
||||||
result["fromPad"] = {"ref": from_ref, "pad": from_pad, "x": start_pos.x / scale, "y": start_pos.y / scale}
|
f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})"
|
||||||
result["toPad"] = {"ref": to_ref, "pad": to_pad, "x": end_pos.x / scale, "y": end_pos.y / scale}
|
)
|
||||||
|
result["fromPad"] = {
|
||||||
|
"ref": from_ref,
|
||||||
|
"pad": from_pad,
|
||||||
|
"x": start_pos.x / scale,
|
||||||
|
"y": start_pos.y / scale,
|
||||||
|
}
|
||||||
|
result["toPad"] = {
|
||||||
|
"ref": to_ref,
|
||||||
|
"pad": to_pad,
|
||||||
|
"x": end_pos.x / scale,
|
||||||
|
"y": end_pos.y / scale,
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -886,7 +908,9 @@ class RoutingCommands:
|
|||||||
else:
|
else:
|
||||||
traces_to_copy.append(track)
|
traces_to_copy.append(track)
|
||||||
|
|
||||||
filter_method = "net-based" if use_net_filter else "geometric (pads have no nets)"
|
filter_method = (
|
||||||
|
"net-based" if use_net_filter else "geometric (pads have no nets)"
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"copy_routing_pattern: {len(traces_to_copy)} traces, "
|
f"copy_routing_pattern: {len(traces_to_copy)} traces, "
|
||||||
f"{len(vias_to_copy)} vias selected via {filter_method}"
|
f"{len(vias_to_copy)} vias selected via {filter_method}"
|
||||||
|
|||||||
@@ -592,7 +592,9 @@ class KiCADInterface:
|
|||||||
self._current_project_path = project_path
|
self._current_project_path = project_path
|
||||||
local_lib = FootprintLibraryManager(project_path=project_path)
|
local_lib = FootprintLibraryManager(project_path=project_path)
|
||||||
self.component_commands = ComponentCommands(self.board, local_lib)
|
self.component_commands = ComponentCommands(self.board, local_lib)
|
||||||
logger.info(f"Reloaded FootprintLibraryManager with project_path={project_path}")
|
logger.info(
|
||||||
|
f"Reloaded FootprintLibraryManager with project_path={project_path}"
|
||||||
|
)
|
||||||
|
|
||||||
return self.component_commands.place_component(params)
|
return self.component_commands.place_component(params)
|
||||||
|
|
||||||
@@ -665,7 +667,10 @@ class KiCADInterface:
|
|||||||
|
|
||||||
sch_file = Path(schematic_path)
|
sch_file = Path(schematic_path)
|
||||||
if not sch_file.exists():
|
if not sch_file.exists():
|
||||||
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Schematic not found: {schematic_path}",
|
||||||
|
}
|
||||||
|
|
||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
lines = f.read().split("\n")
|
lines = f.read().split("\n")
|
||||||
@@ -676,9 +681,13 @@ class KiCADInterface:
|
|||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if "(lib_symbols" in line and lib_sym_start is None:
|
if "(lib_symbols" in line and lib_sym_start is None:
|
||||||
lib_sym_start = i
|
lib_sym_start = i
|
||||||
depth = sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
depth = sum(1 for c in line if c == "(") - sum(
|
||||||
|
1 for c in line if c == ")"
|
||||||
|
)
|
||||||
elif lib_sym_start is not None and lib_sym_end is None:
|
elif lib_sym_start is not None and lib_sym_end is None:
|
||||||
depth += sum(1 for c in line if c == "(") - sum(1 for c in line if c == ")")
|
depth += sum(1 for c in line if c == "(") - sum(
|
||||||
|
1 for c in line if c == ")"
|
||||||
|
)
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
lib_sym_end = i
|
lib_sym_end = i
|
||||||
break
|
break
|
||||||
@@ -695,15 +704,22 @@ class KiCADInterface:
|
|||||||
|
|
||||||
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]):
|
if re.match(r"\s*\(symbol\s+\(lib_id\s+\"", lines[i]):
|
||||||
b_start = i
|
b_start = i
|
||||||
b_depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")")
|
b_depth = sum(1 for c in lines[i] if c == "(") - sum(
|
||||||
|
1 for c in lines[i] if c == ")"
|
||||||
|
)
|
||||||
j = i + 1
|
j = i + 1
|
||||||
while j < len(lines) and b_depth > 0:
|
while j < len(lines) and b_depth > 0:
|
||||||
b_depth += sum(1 for c in lines[j] if c == "(") - sum(1 for c in lines[j] if c == ")")
|
b_depth += sum(1 for c in lines[j] if c == "(") - sum(
|
||||||
|
1 for c in lines[j] if c == ")"
|
||||||
|
)
|
||||||
j += 1
|
j += 1
|
||||||
b_end = j - 1
|
b_end = j - 1
|
||||||
|
|
||||||
block_text = "\n".join(lines[b_start : b_end + 1])
|
block_text = "\n".join(lines[b_start : b_end + 1])
|
||||||
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
if re.search(
|
||||||
|
r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"',
|
||||||
|
block_text,
|
||||||
|
):
|
||||||
blocks_to_delete.append((b_start, b_end))
|
blocks_to_delete.append((b_start, b_end))
|
||||||
|
|
||||||
i = b_end + 1
|
i = b_end + 1
|
||||||
@@ -727,18 +743,27 @@ class KiCADInterface:
|
|||||||
f.write("\n".join(lines))
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
deleted_count = len(blocks_to_delete)
|
deleted_count = len(blocks_to_delete)
|
||||||
logger.info(f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}")
|
logger.info(
|
||||||
return {"success": True, "reference": reference, "deleted_count": deleted_count, "schematic": str(sch_file)}
|
f"Deleted {deleted_count} instance(s) of {reference} from {sch_file.name}"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"reference": reference,
|
||||||
|
"deleted_count": deleted_count,
|
||||||
|
"schematic": str(sch_file),
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error deleting schematic component: {e}")
|
logger.error(f"Error deleting schematic component: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_edit_schematic_component(self, params):
|
def _handle_edit_schematic_component(self, params):
|
||||||
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
|
"""Update properties of a placed symbol in a schematic (footprint, value, reference).
|
||||||
Uses text-based in-place editing – preserves position, UUID and all other fields."""
|
Uses text-based in-place editing – preserves position, UUID and all other fields.
|
||||||
|
"""
|
||||||
logger.info("Editing schematic component")
|
logger.info("Editing schematic component")
|
||||||
try:
|
try:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -754,12 +779,24 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": "schematicPath is required"}
|
return {"success": False, "message": "schematicPath is required"}
|
||||||
if not reference:
|
if not reference:
|
||||||
return {"success": False, "message": "reference is required"}
|
return {"success": False, "message": "reference is required"}
|
||||||
if not any([new_footprint is not None, new_value is not None, new_reference is not None]):
|
if not any(
|
||||||
return {"success": False, "message": "At least one of footprint, value, or newReference must be provided"}
|
[
|
||||||
|
new_footprint is not None,
|
||||||
|
new_value is not None,
|
||||||
|
new_reference is not None,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "At least one of footprint, value, or newReference must be provided",
|
||||||
|
}
|
||||||
|
|
||||||
sch_file = Path(schematic_path)
|
sch_file = Path(schematic_path)
|
||||||
if not sch_file.exists():
|
if not sch_file.exists():
|
||||||
return {"success": False, "message": f"Schematic not found: {schematic_path}"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Schematic not found: {schematic_path}",
|
||||||
|
}
|
||||||
|
|
||||||
with open(sch_file, "r", encoding="utf-8") as f:
|
with open(sch_file, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
@@ -769,9 +806,9 @@ class KiCADInterface:
|
|||||||
depth = 0
|
depth = 0
|
||||||
i = start
|
i = start
|
||||||
while i < len(s):
|
while i < len(s):
|
||||||
if s[i] == '(':
|
if s[i] == "(":
|
||||||
depth += 1
|
depth += 1
|
||||||
elif s[i] == ')':
|
elif s[i] == ")":
|
||||||
depth -= 1
|
depth -= 1
|
||||||
if depth == 0:
|
if depth == 0:
|
||||||
return i
|
return i
|
||||||
@@ -780,7 +817,9 @@ class KiCADInterface:
|
|||||||
|
|
||||||
# Skip lib_symbols section
|
# Skip lib_symbols section
|
||||||
lib_sym_pos = content.find("(lib_symbols")
|
lib_sym_pos = content.find("(lib_symbols")
|
||||||
lib_sym_end = find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1
|
lib_sym_end = (
|
||||||
|
find_matching_paren(content, lib_sym_pos) if lib_sym_pos >= 0 else -1
|
||||||
|
)
|
||||||
|
|
||||||
# Find placed symbol blocks that match the reference
|
# Find placed symbol blocks that match the reference
|
||||||
# Search for (symbol (lib_id "...") ... (property "Reference" "<ref>" ...) ...)
|
# Search for (symbol (lib_id "...") ... (property "Reference" "<ref>" ...) ...)
|
||||||
@@ -801,35 +840,60 @@ class KiCADInterface:
|
|||||||
search_start = pos + 1
|
search_start = pos + 1
|
||||||
continue
|
continue
|
||||||
block_text = content[pos : end + 1]
|
block_text = content[pos : end + 1]
|
||||||
if re.search(r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"', block_text):
|
if re.search(
|
||||||
|
r'\(property\s+"Reference"\s+"' + re.escape(reference) + r'"',
|
||||||
|
block_text,
|
||||||
|
):
|
||||||
block_start, block_end = pos, end
|
block_start, block_end = pos, end
|
||||||
break
|
break
|
||||||
search_start = end + 1
|
search_start = end + 1
|
||||||
|
|
||||||
if block_start is None:
|
if block_start is None:
|
||||||
return {"success": False, "message": f"Component '{reference}' not found in schematic"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Component '{reference}' not found in schematic",
|
||||||
|
}
|
||||||
|
|
||||||
# Apply property replacements within the found block
|
# Apply property replacements within the found block
|
||||||
block_text = content[block_start : block_end + 1]
|
block_text = content[block_start : block_end + 1]
|
||||||
if new_footprint is not None:
|
if new_footprint is not None:
|
||||||
block_text = re.sub(r'(\(property\s+"Footprint"\s+)"[^"]*"', rf'\1"{new_footprint}"', block_text)
|
block_text = re.sub(
|
||||||
|
r'(\(property\s+"Footprint"\s+)"[^"]*"',
|
||||||
|
rf'\1"{new_footprint}"',
|
||||||
|
block_text,
|
||||||
|
)
|
||||||
if new_value is not None:
|
if new_value is not None:
|
||||||
block_text = re.sub(r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', block_text)
|
block_text = re.sub(
|
||||||
|
r'(\(property\s+"Value"\s+)"[^"]*"', rf'\1"{new_value}"', block_text
|
||||||
|
)
|
||||||
if new_reference is not None:
|
if new_reference is not None:
|
||||||
block_text = re.sub(r'(\(property\s+"Reference"\s+)"[^"]*"', rf'\1"{new_reference}"', block_text)
|
block_text = re.sub(
|
||||||
|
r'(\(property\s+"Reference"\s+)"[^"]*"',
|
||||||
|
rf'\1"{new_reference}"',
|
||||||
|
block_text,
|
||||||
|
)
|
||||||
|
|
||||||
content = content[:block_start] + block_text + content[block_end + 1 :]
|
content = content[:block_start] + block_text + content[block_end + 1 :]
|
||||||
|
|
||||||
with open(sch_file, "w", encoding="utf-8") as f:
|
with open(sch_file, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
changes = {k: v for k, v in {"footprint": new_footprint, "value": new_value, "reference": new_reference}.items() if v is not None}
|
changes = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"footprint": new_footprint,
|
||||||
|
"value": new_value,
|
||||||
|
"reference": new_reference,
|
||||||
|
}.items()
|
||||||
|
if v is not None
|
||||||
|
}
|
||||||
logger.info(f"Edited schematic component {reference}: {changes}")
|
logger.info(f"Edited schematic component {reference}: {changes}")
|
||||||
return {"success": True, "reference": reference, "updated": changes}
|
return {"success": True, "reference": reference, "updated": changes}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error editing schematic component: {e}")
|
logger.error(f"Error editing schematic component: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
@@ -899,7 +963,9 @@ class KiCADInterface:
|
|||||||
|
|
||||||
def _handle_create_footprint(self, params):
|
def _handle_create_footprint(self, params):
|
||||||
"""Create a new .kicad_mod footprint file in a .pretty library."""
|
"""Create a new .kicad_mod footprint file in a .pretty library."""
|
||||||
logger.info(f"create_footprint: {params.get('name')} in {params.get('libraryPath')}")
|
logger.info(
|
||||||
|
f"create_footprint: {params.get('name')} in {params.get('libraryPath')}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
creator = FootprintCreator()
|
creator = FootprintCreator()
|
||||||
return creator.create_footprint(
|
return creator.create_footprint(
|
||||||
@@ -921,7 +987,9 @@ class KiCADInterface:
|
|||||||
|
|
||||||
def _handle_edit_footprint_pad(self, params):
|
def _handle_edit_footprint_pad(self, params):
|
||||||
"""Edit an existing pad in a .kicad_mod file."""
|
"""Edit an existing pad in a .kicad_mod file."""
|
||||||
logger.info(f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}")
|
logger.info(
|
||||||
|
f"edit_footprint_pad: pad {params.get('padNumber')} in {params.get('footprintPath')}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
creator = FootprintCreator()
|
creator = FootprintCreator()
|
||||||
return creator.edit_footprint_pad(
|
return creator.edit_footprint_pad(
|
||||||
@@ -970,7 +1038,9 @@ class KiCADInterface:
|
|||||||
|
|
||||||
def _handle_create_symbol(self, params):
|
def _handle_create_symbol(self, params):
|
||||||
"""Create a new symbol in a .kicad_sym library."""
|
"""Create a new symbol in a .kicad_sym library."""
|
||||||
logger.info(f"create_symbol: {params.get('name')} in {params.get('libraryPath')}")
|
logger.info(
|
||||||
|
f"create_symbol: {params.get('name')} in {params.get('libraryPath')}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
creator = SymbolCreator()
|
creator = SymbolCreator()
|
||||||
return creator.create_symbol(
|
return creator.create_symbol(
|
||||||
@@ -994,7 +1064,9 @@ class KiCADInterface:
|
|||||||
|
|
||||||
def _handle_delete_symbol(self, params):
|
def _handle_delete_symbol(self, params):
|
||||||
"""Delete a symbol from a .kicad_sym library."""
|
"""Delete a symbol from a .kicad_sym library."""
|
||||||
logger.info(f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}")
|
logger.info(
|
||||||
|
f"delete_symbol: {params.get('name')} from {params.get('libraryPath')}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
creator = SymbolCreator()
|
creator = SymbolCreator()
|
||||||
return creator.delete_symbol(
|
return creator.delete_symbol(
|
||||||
@@ -2210,7 +2282,10 @@ class KiCADInterface:
|
|||||||
return manager.enrich_schematic(Path(schematic_path), dry_run=dry_run)
|
return manager.enrich_schematic(Path(schematic_path), dry_run=dry_run)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error enriching datasheets: {e}", exc_info=True)
|
logger.error(f"Error enriching datasheets: {e}", exc_info=True)
|
||||||
return {"success": False, "message": f"Failed to enrich datasheets: {str(e)}"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Failed to enrich datasheets: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
def _handle_get_datasheet_url(self, params):
|
def _handle_get_datasheet_url(self, params):
|
||||||
"""Return LCSC datasheet and product URLs for a part number"""
|
"""Return LCSC datasheet and product URLs for a part number"""
|
||||||
@@ -2232,7 +2307,10 @@ class KiCADInterface:
|
|||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting datasheet URL: {e}", exc_info=True)
|
logger.error(f"Error getting datasheet URL: {e}", exc_info=True)
|
||||||
return {"success": False, "message": f"Failed to get datasheet URL: {str(e)}"}
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"Failed to get datasheet URL: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user