feat: Extend IPC backend with 21 commands and hybrid footprint placement
- Add IPC handlers for zone operations (add_copper_pour, refill_zones) - Add IPC handlers for board operations (add_board_outline, add_mounting_hole, get_layer_list) - Add IPC handlers for component operations (rotate_component, get_component_properties) - Add IPC handlers for net operations (delete_trace, get_nets_list) - Implement hybrid footprint placement (SWIG library loading + IPC placement) - Extend create_schematic to handle filename, title, projectName params - Update documentation for IPC backend status and known issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -464,6 +464,207 @@ class IPCBoardAPI(BoardAPI):
|
||||
Place a component on the board.
|
||||
|
||||
The component appears immediately in the KiCAD UI.
|
||||
|
||||
This method uses a hybrid approach:
|
||||
1. Load the footprint definition from the library using pcbnew (SWIG)
|
||||
2. Place it on the board via IPC for real-time UI updates
|
||||
|
||||
Args:
|
||||
reference: Component reference designator (e.g., "R1", "U1")
|
||||
footprint: Footprint path in format "Library:FootprintName" or just "FootprintName"
|
||||
x: X position in mm
|
||||
y: Y position in mm
|
||||
rotation: Rotation angle in degrees
|
||||
layer: Layer name ("F.Cu" for top, "B.Cu" for bottom)
|
||||
value: Component value (optional)
|
||||
"""
|
||||
try:
|
||||
# First, try to load the footprint from library using pcbnew SWIG
|
||||
loaded_fp = self._load_footprint_from_library(footprint)
|
||||
|
||||
if loaded_fp:
|
||||
# We have the footprint from the library - place it via SWIG
|
||||
# then sync to IPC for UI update
|
||||
return self._place_loaded_footprint(
|
||||
loaded_fp, reference, x, y, rotation, layer, value
|
||||
)
|
||||
else:
|
||||
# Fallback: Create a basic placeholder footprint via IPC
|
||||
logger.warning(f"Could not load footprint '{footprint}' from library, creating placeholder")
|
||||
return self._place_placeholder_footprint(
|
||||
reference, footprint, x, y, rotation, layer, value
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place component: {e}")
|
||||
return False
|
||||
|
||||
def _load_footprint_from_library(self, footprint_path: str):
|
||||
"""
|
||||
Load a footprint from the library using pcbnew SWIG API.
|
||||
|
||||
Args:
|
||||
footprint_path: Either "Library:FootprintName" or just "FootprintName"
|
||||
|
||||
Returns:
|
||||
pcbnew.FOOTPRINT object or None if not found
|
||||
"""
|
||||
try:
|
||||
import pcbnew
|
||||
|
||||
# Parse library and footprint name
|
||||
if ':' in footprint_path:
|
||||
lib_name, fp_name = footprint_path.split(':', 1)
|
||||
else:
|
||||
# Try to find the footprint in all libraries
|
||||
lib_name = None
|
||||
fp_name = footprint_path
|
||||
|
||||
# Get the footprint library table
|
||||
fp_lib_table = pcbnew.GetGlobalFootprintLib()
|
||||
|
||||
if lib_name:
|
||||
# Load from specific library
|
||||
try:
|
||||
loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib_name, fp_name)
|
||||
if loaded_fp:
|
||||
logger.info(f"Loaded footprint '{fp_name}' from library '{lib_name}'")
|
||||
return loaded_fp
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load from {lib_name}: {e}")
|
||||
else:
|
||||
# Search all libraries for the footprint
|
||||
lib_names = fp_lib_table.GetLogicalLibs()
|
||||
for lib in lib_names:
|
||||
try:
|
||||
loaded_fp = pcbnew.FootprintLoad(fp_lib_table, lib, fp_name)
|
||||
if loaded_fp:
|
||||
logger.info(f"Found footprint '{fp_name}' in library '{lib}'")
|
||||
return loaded_fp
|
||||
except:
|
||||
continue
|
||||
|
||||
logger.warning(f"Footprint '{footprint_path}' not found in any library")
|
||||
return None
|
||||
|
||||
except ImportError:
|
||||
logger.warning("pcbnew not available - cannot load footprints from library")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading footprint from library: {e}")
|
||||
return None
|
||||
|
||||
def _place_loaded_footprint(
|
||||
self,
|
||||
loaded_fp,
|
||||
reference: str,
|
||||
x: float,
|
||||
y: float,
|
||||
rotation: float,
|
||||
layer: str,
|
||||
value: str
|
||||
) -> bool:
|
||||
"""
|
||||
Place a loaded pcbnew footprint onto the board.
|
||||
|
||||
Uses SWIG to add the footprint, then notifies for IPC sync.
|
||||
"""
|
||||
try:
|
||||
import pcbnew
|
||||
|
||||
# Get the board file path from IPC to load via pcbnew
|
||||
board = self._get_board()
|
||||
|
||||
# Get the pcbnew board instance
|
||||
# We need to get the actual board file path
|
||||
project = board.get_project()
|
||||
board_path = None
|
||||
|
||||
# Try to get the board path from kipy
|
||||
try:
|
||||
docs = self._kicad.get_open_documents()
|
||||
for doc in docs:
|
||||
if hasattr(doc, 'path') and str(doc.path).endswith('.kicad_pcb'):
|
||||
board_path = str(doc.path)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get board path from IPC: {e}")
|
||||
|
||||
if board_path and os.path.exists(board_path):
|
||||
# Load board via pcbnew
|
||||
pcb_board = pcbnew.LoadBoard(board_path)
|
||||
else:
|
||||
# Try to get from pcbnew directly
|
||||
pcb_board = pcbnew.GetBoard()
|
||||
|
||||
if not pcb_board:
|
||||
logger.error("Could not get pcbnew board instance")
|
||||
return self._place_placeholder_footprint(
|
||||
reference, "", x, y, rotation, layer, value
|
||||
)
|
||||
|
||||
# Set footprint position and properties
|
||||
scale = MM_TO_NM
|
||||
loaded_fp.SetPosition(pcbnew.VECTOR2I(int(x * scale), int(y * scale)))
|
||||
loaded_fp.SetOrientationDegrees(rotation)
|
||||
|
||||
# Set reference
|
||||
loaded_fp.SetReference(reference)
|
||||
|
||||
# Set value if provided
|
||||
if value:
|
||||
loaded_fp.SetValue(value)
|
||||
|
||||
# Set layer (flip if bottom)
|
||||
if layer == "B.Cu":
|
||||
if not loaded_fp.IsFlipped():
|
||||
loaded_fp.Flip(loaded_fp.GetPosition(), False)
|
||||
|
||||
# Add to board
|
||||
pcb_board.Add(loaded_fp)
|
||||
|
||||
# Save the board so IPC can see the changes
|
||||
pcbnew.SaveBoard(board_path, pcb_board)
|
||||
|
||||
# Refresh IPC view
|
||||
try:
|
||||
board.revert() # Reload from disk to sync IPC
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not refresh IPC board: {e}")
|
||||
|
||||
self._notify("component_placed", {
|
||||
"reference": reference,
|
||||
"footprint": loaded_fp.GetFPIDAsString(),
|
||||
"position": {"x": x, "y": y},
|
||||
"rotation": rotation,
|
||||
"layer": layer,
|
||||
"loaded_from_library": True
|
||||
})
|
||||
|
||||
logger.info(f"Placed component {reference} ({loaded_fp.GetFPIDAsString()}) at ({x}, {y}) mm")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error placing loaded footprint: {e}")
|
||||
# Fall back to placeholder
|
||||
return self._place_placeholder_footprint(
|
||||
reference, "", x, y, rotation, layer, value
|
||||
)
|
||||
|
||||
def _place_placeholder_footprint(
|
||||
self,
|
||||
reference: str,
|
||||
footprint: str,
|
||||
x: float,
|
||||
y: float,
|
||||
rotation: float,
|
||||
layer: str,
|
||||
value: str
|
||||
) -> bool:
|
||||
"""
|
||||
Place a placeholder footprint when library loading fails.
|
||||
|
||||
Creates a basic footprint via IPC with just reference/value fields.
|
||||
"""
|
||||
try:
|
||||
from kipy.board_types import Footprint
|
||||
@@ -487,12 +688,8 @@ class IPCBoardAPI(BoardAPI):
|
||||
# Set reference and value
|
||||
if fp.reference_field:
|
||||
fp.reference_field.text.value = reference
|
||||
if fp.value_field and value:
|
||||
fp.value_field.text.value = value
|
||||
|
||||
# Note: Loading footprint from library requires additional handling
|
||||
# The IPC API may need the footprint definition to be set
|
||||
# For now, we create a basic footprint placeholder
|
||||
if fp.value_field:
|
||||
fp.value_field.text.value = value if value else footprint
|
||||
|
||||
# Begin transaction
|
||||
commit = board.begin_commit()
|
||||
@@ -504,14 +701,16 @@ class IPCBoardAPI(BoardAPI):
|
||||
"footprint": footprint,
|
||||
"position": {"x": x, "y": y},
|
||||
"rotation": rotation,
|
||||
"layer": layer
|
||||
"layer": layer,
|
||||
"loaded_from_library": False,
|
||||
"is_placeholder": True
|
||||
})
|
||||
|
||||
logger.info(f"Placed component {reference} at ({x}, {y}) mm")
|
||||
logger.info(f"Placed placeholder component {reference} at ({x}, {y}) mm")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to place component: {e}")
|
||||
logger.error(f"Failed to place placeholder component: {e}")
|
||||
return False
|
||||
|
||||
def move_component(self, reference: str, x: float, y: float, rotation: Optional[float] = None) -> bool:
|
||||
@@ -857,6 +1056,145 @@ class IPCBoardAPI(BoardAPI):
|
||||
logger.error(f"Failed to get nets: {e}")
|
||||
return []
|
||||
|
||||
def add_zone(
|
||||
self,
|
||||
points: List[Dict[str, float]],
|
||||
layer: str = "F.Cu",
|
||||
net_name: Optional[str] = None,
|
||||
clearance: float = 0.5,
|
||||
min_thickness: float = 0.25,
|
||||
priority: int = 0,
|
||||
fill_mode: str = "solid",
|
||||
name: str = ""
|
||||
) -> bool:
|
||||
"""
|
||||
Add a copper pour zone to the board.
|
||||
|
||||
The zone appears immediately in the KiCAD UI.
|
||||
|
||||
Args:
|
||||
points: List of points defining the zone outline, e.g. [{"x": 0, "y": 0}, ...]
|
||||
layer: Layer name (F.Cu, B.Cu, etc.)
|
||||
net_name: Net to connect the zone to (e.g., "GND")
|
||||
clearance: Clearance from other copper in mm
|
||||
min_thickness: Minimum copper thickness in mm
|
||||
priority: Zone priority (higher = fills first)
|
||||
fill_mode: "solid" or "hatched"
|
||||
name: Optional zone name
|
||||
"""
|
||||
try:
|
||||
from kipy.board_types import Zone, ZoneFillMode, ZoneType
|
||||
from kipy.geometry import PolyLine, PolyLineNode, Vector2
|
||||
from kipy.util.units import from_mm
|
||||
from kipy.proto.board.board_types_pb2 import BoardLayer
|
||||
|
||||
board = self._get_board()
|
||||
|
||||
if len(points) < 3:
|
||||
logger.error("Zone requires at least 3 points")
|
||||
return False
|
||||
|
||||
# Create zone
|
||||
zone = Zone()
|
||||
zone.type = ZoneType.ZT_COPPER
|
||||
|
||||
# Set layer
|
||||
layer_map = {
|
||||
"F.Cu": BoardLayer.BL_F_Cu,
|
||||
"B.Cu": BoardLayer.BL_B_Cu,
|
||||
"In1.Cu": BoardLayer.BL_In1_Cu,
|
||||
"In2.Cu": BoardLayer.BL_In2_Cu,
|
||||
"In3.Cu": BoardLayer.BL_In3_Cu,
|
||||
"In4.Cu": BoardLayer.BL_In4_Cu,
|
||||
}
|
||||
zone.layers = [layer_map.get(layer, BoardLayer.BL_F_Cu)]
|
||||
|
||||
# Set net if specified
|
||||
if net_name:
|
||||
nets = board.get_nets()
|
||||
for net in nets:
|
||||
if net.name == net_name:
|
||||
zone.net = net
|
||||
break
|
||||
|
||||
# Set zone properties
|
||||
zone.clearance = from_mm(clearance)
|
||||
zone.min_thickness = from_mm(min_thickness)
|
||||
zone.priority = priority
|
||||
|
||||
if name:
|
||||
zone.name = name
|
||||
|
||||
# Set fill mode
|
||||
if fill_mode == "hatched":
|
||||
zone.fill_mode = ZoneFillMode.ZFM_HATCHED
|
||||
else:
|
||||
zone.fill_mode = ZoneFillMode.ZFM_SOLID
|
||||
|
||||
# Create outline polyline
|
||||
outline = PolyLine()
|
||||
outline.closed = True
|
||||
|
||||
for point in points:
|
||||
x = point.get("x", 0)
|
||||
y = point.get("y", 0)
|
||||
node = PolyLineNode.from_xy(from_mm(x), from_mm(y))
|
||||
outline.append(node)
|
||||
|
||||
# Set the outline on the zone
|
||||
# Note: Zone outline is set via the proto directly since kipy
|
||||
# doesn't expose a direct setter for creating new zones
|
||||
zone._proto.outline.polygons.add()
|
||||
zone._proto.outline.polygons[0].outline.CopyFrom(outline._proto)
|
||||
|
||||
# Add zone with transaction
|
||||
commit = board.begin_commit()
|
||||
board.create_items(zone)
|
||||
board.push_commit(commit, f"Added copper zone on {layer}")
|
||||
|
||||
self._notify("zone_added", {
|
||||
"layer": layer,
|
||||
"net": net_name,
|
||||
"points": len(points),
|
||||
"priority": priority
|
||||
})
|
||||
|
||||
logger.info(f"Added zone on {layer} with {len(points)} points")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add zone: {e}")
|
||||
return False
|
||||
|
||||
def get_zones(self) -> List[Dict[str, Any]]:
|
||||
"""Get all zones on the board."""
|
||||
try:
|
||||
from kipy.util.units import to_mm
|
||||
|
||||
board = self._get_board()
|
||||
zones = board.get_zones()
|
||||
|
||||
result = []
|
||||
for zone in zones:
|
||||
try:
|
||||
result.append({
|
||||
"name": zone.name if hasattr(zone, 'name') else "",
|
||||
"net": zone.net.name if zone.net else "",
|
||||
"priority": zone.priority if hasattr(zone, 'priority') else 0,
|
||||
"layers": [str(l) for l in zone.layers] if hasattr(zone, 'layers') else [],
|
||||
"filled": zone.filled if hasattr(zone, 'filled') else False,
|
||||
"id": str(zone.id) if hasattr(zone, 'id') else ""
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing zone: {e}")
|
||||
continue
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get zones: {e}")
|
||||
return []
|
||||
|
||||
def refill_zones(self) -> bool:
|
||||
"""Refill all copper pour zones."""
|
||||
try:
|
||||
|
||||
+308
-7
@@ -302,7 +302,8 @@ class KiCADInterface:
|
||||
"create_netclass": self.routing_commands.create_netclass,
|
||||
"add_copper_pour": self.routing_commands.add_copper_pour,
|
||||
"route_differential_pair": self.routing_commands.route_differential_pair,
|
||||
|
||||
"refill_zones": self._handle_refill_zones,
|
||||
|
||||
# Design rule commands
|
||||
"set_design_rules": self.design_rule_commands.set_design_rules,
|
||||
"get_design_rules": self.design_rule_commands.get_design_rules,
|
||||
@@ -358,16 +359,26 @@ class KiCADInterface:
|
||||
"route_trace": "_ipc_route_trace",
|
||||
"add_via": "_ipc_add_via",
|
||||
"add_net": "_ipc_add_net",
|
||||
"delete_trace": "_ipc_delete_trace",
|
||||
"get_nets_list": "_ipc_get_nets_list",
|
||||
# Zone commands
|
||||
"add_copper_pour": "_ipc_add_copper_pour",
|
||||
"refill_zones": "_ipc_refill_zones",
|
||||
# Board commands
|
||||
"add_text": "_ipc_add_text",
|
||||
"add_board_text": "_ipc_add_text",
|
||||
"set_board_size": "_ipc_set_board_size",
|
||||
"get_board_info": "_ipc_get_board_info",
|
||||
"add_board_outline": "_ipc_add_board_outline",
|
||||
"add_mounting_hole": "_ipc_add_mounting_hole",
|
||||
"get_layer_list": "_ipc_get_layer_list",
|
||||
# Component commands
|
||||
"place_component": "_ipc_place_component",
|
||||
"move_component": "_ipc_move_component",
|
||||
"rotate_component": "_ipc_rotate_component",
|
||||
"delete_component": "_ipc_delete_component",
|
||||
"get_component_list": "_ipc_get_component_list",
|
||||
"get_component_properties": "_ipc_get_component_properties",
|
||||
# Save command
|
||||
"save_project": "_ipc_save_project",
|
||||
}
|
||||
@@ -454,18 +465,38 @@ class KiCADInterface:
|
||||
"""Create a new schematic"""
|
||||
logger.info("Creating schematic")
|
||||
try:
|
||||
# Accept both 'name' (from MCP tool) and 'projectName' (legacy)
|
||||
project_name = params.get("name") or params.get("projectName")
|
||||
path = params.get("path", ".")
|
||||
# Support multiple parameter naming conventions for compatibility:
|
||||
# - TypeScript tools use: name, path
|
||||
# - Python schema uses: filename, title
|
||||
# - Legacy uses: projectName, path, metadata
|
||||
project_name = (
|
||||
params.get("projectName") or
|
||||
params.get("name") or
|
||||
params.get("title")
|
||||
)
|
||||
|
||||
# Handle filename parameter - it may contain full path
|
||||
filename = params.get("filename")
|
||||
if filename:
|
||||
# If filename provided, extract name and path from it
|
||||
if filename.endswith('.kicad_sch'):
|
||||
filename = filename[:-10] # Remove .kicad_sch extension
|
||||
path = os.path.dirname(filename) or "."
|
||||
project_name = project_name or os.path.basename(filename)
|
||||
else:
|
||||
path = params.get("path", ".")
|
||||
metadata = params.get("metadata", {})
|
||||
|
||||
if not project_name:
|
||||
return {"success": False, "message": "Project name is required"}
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Schematic name is required. Provide 'name', 'projectName', or 'filename' parameter."
|
||||
}
|
||||
|
||||
schematic = SchematicManager.create_schematic(project_name, metadata)
|
||||
file_path = f"{path}/{project_name}.kicad_sch"
|
||||
success = SchematicManager.save_schematic(schematic, file_path)
|
||||
|
||||
|
||||
return {"success": success, "file_path": file_path}
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating schematic: {str(e)}")
|
||||
@@ -747,6 +778,31 @@ class KiCADInterface:
|
||||
logger.error(f"Error launching KiCAD UI: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _handle_refill_zones(self, params):
|
||||
"""Refill all copper pour zones on the board"""
|
||||
logger.info("Refilling zones")
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first"
|
||||
}
|
||||
|
||||
# Use pcbnew's zone filler for SWIG backend
|
||||
filler = pcbnew.ZONE_FILLER(self.board)
|
||||
zones = self.board.Zones()
|
||||
filler.Fill(zones)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Zones refilled successfully",
|
||||
"zoneCount": zones.size() if hasattr(zones, 'size') else len(list(zones))
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error refilling zones: {str(e)}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
# =========================================================================
|
||||
# IPC Backend handlers - these provide real-time UI synchronization
|
||||
# These methods are called automatically when IPC is available
|
||||
@@ -843,6 +899,73 @@ class KiCADInterface:
|
||||
"net": {"name": name}
|
||||
}
|
||||
|
||||
def _ipc_add_copper_pour(self, params):
|
||||
"""IPC handler for add_copper_pour - adds zone with real-time UI update"""
|
||||
try:
|
||||
layer = params.get("layer", "F.Cu")
|
||||
net = params.get("net")
|
||||
clearance = params.get("clearance", 0.5)
|
||||
min_width = params.get("minWidth", 0.25)
|
||||
points = params.get("points", [])
|
||||
priority = params.get("priority", 0)
|
||||
fill_type = params.get("fillType", "solid")
|
||||
name = params.get("name", "")
|
||||
|
||||
if not points or len(points) < 3:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "At least 3 points are required for copper pour outline"
|
||||
}
|
||||
|
||||
# Convert points format if needed (handle both {x, y} and {x, y, unit})
|
||||
formatted_points = []
|
||||
for point in points:
|
||||
formatted_points.append({
|
||||
"x": point.get("x", 0),
|
||||
"y": point.get("y", 0)
|
||||
})
|
||||
|
||||
success = self.ipc_board_api.add_zone(
|
||||
points=formatted_points,
|
||||
layer=layer,
|
||||
net_name=net,
|
||||
clearance=clearance,
|
||||
min_thickness=min_width,
|
||||
priority=priority,
|
||||
fill_mode=fill_type,
|
||||
name=name
|
||||
)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Added copper pour (visible in KiCAD UI)" if success else "Failed to add 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"IPC add_copper_pour error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_refill_zones(self, params):
|
||||
"""IPC handler for refill_zones - refills all zones with real-time UI update"""
|
||||
try:
|
||||
success = self.ipc_board_api.refill_zones()
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": "Zones refilled (visible in KiCAD UI)" if success else "Failed to refill zones"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC refill_zones error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_add_text(self, params):
|
||||
"""IPC handler for add_text/add_board_text - adds text with real-time UI update"""
|
||||
try:
|
||||
@@ -1017,6 +1140,184 @@ class KiCADInterface:
|
||||
logger.error(f"IPC save_project error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_delete_trace(self, params):
|
||||
"""IPC handler for delete_trace - Note: IPC doesn't support direct trace deletion yet"""
|
||||
# IPC API doesn't have a direct delete track method
|
||||
# Fall back to SWIG for this operation
|
||||
logger.info("delete_trace: Falling back to SWIG (IPC doesn't support trace deletion)")
|
||||
return self.routing_commands.delete_trace(params)
|
||||
|
||||
def _ipc_get_nets_list(self, params):
|
||||
"""IPC handler for get_nets_list - gets nets with real-time data"""
|
||||
try:
|
||||
nets = self.ipc_board_api.get_nets()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"nets": nets,
|
||||
"count": len(nets)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC get_nets_list error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_add_board_outline(self, params):
|
||||
"""IPC handler for add_board_outline - adds board edge with real-time UI update"""
|
||||
try:
|
||||
from kipy.board_types import BoardSegment
|
||||
from kipy.geometry import Vector2
|
||||
from kipy.util.units import from_mm
|
||||
from kipy.proto.board.board_types_pb2 import BoardLayer
|
||||
|
||||
board = self.ipc_board_api._get_board()
|
||||
|
||||
points = params.get("points", [])
|
||||
width = params.get("width", 0.1)
|
||||
|
||||
if len(points) < 2:
|
||||
return {"success": False, "message": "At least 2 points required for board outline"}
|
||||
|
||||
commit = board.begin_commit()
|
||||
lines_created = 0
|
||||
|
||||
# Create line segments connecting the points
|
||||
for i in range(len(points)):
|
||||
start = points[i]
|
||||
end = points[(i + 1) % len(points)] # Wrap around to close the outline
|
||||
|
||||
segment = BoardSegment()
|
||||
segment.start = Vector2.from_xy(from_mm(start.get("x", 0)), from_mm(start.get("y", 0)))
|
||||
segment.end = Vector2.from_xy(from_mm(end.get("x", 0)), from_mm(end.get("y", 0)))
|
||||
segment.layer = BoardLayer.BL_Edge_Cuts
|
||||
segment.attributes.stroke.width = from_mm(width)
|
||||
|
||||
board.create_items(segment)
|
||||
lines_created += 1
|
||||
|
||||
board.push_commit(commit, "Added board outline")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added board outline with {lines_created} segments (visible in KiCAD UI)",
|
||||
"segments": lines_created
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC add_board_outline error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_add_mounting_hole(self, params):
|
||||
"""IPC handler for add_mounting_hole - adds mounting hole with real-time UI update"""
|
||||
try:
|
||||
from kipy.board_types import BoardCircle
|
||||
from kipy.geometry import Vector2
|
||||
from kipy.util.units import from_mm
|
||||
from kipy.proto.board.board_types_pb2 import BoardLayer
|
||||
|
||||
board = self.ipc_board_api._get_board()
|
||||
|
||||
x = params.get("x", 0)
|
||||
y = params.get("y", 0)
|
||||
diameter = params.get("diameter", 3.2) # M3 hole default
|
||||
|
||||
commit = board.begin_commit()
|
||||
|
||||
# Create circle on Edge.Cuts layer for the hole
|
||||
circle = BoardCircle()
|
||||
circle.center = Vector2.from_xy(from_mm(x), from_mm(y))
|
||||
circle.radius = from_mm(diameter / 2)
|
||||
circle.layer = BoardLayer.BL_Edge_Cuts
|
||||
circle.attributes.stroke.width = from_mm(0.1)
|
||||
|
||||
board.create_items(circle)
|
||||
board.push_commit(commit, f"Added mounting hole at ({x}, {y})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Added mounting hole at ({x}, {y}) mm (visible in KiCAD UI)",
|
||||
"hole": {
|
||||
"position": {"x": x, "y": y},
|
||||
"diameter": diameter
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC add_mounting_hole error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_get_layer_list(self, params):
|
||||
"""IPC handler for get_layer_list - gets enabled layers"""
|
||||
try:
|
||||
layers = self.ipc_board_api.get_enabled_layers()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"layers": layers,
|
||||
"count": len(layers)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC get_layer_list error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_rotate_component(self, params):
|
||||
"""IPC handler for rotate_component - rotates component with real-time UI update"""
|
||||
try:
|
||||
reference = params.get("reference", params.get("componentId", ""))
|
||||
angle = params.get("angle", params.get("rotation", 90))
|
||||
|
||||
# Get current component to find its position
|
||||
components = self.ipc_board_api.list_components()
|
||||
target = None
|
||||
for comp in components:
|
||||
if comp.get("reference") == reference:
|
||||
target = comp
|
||||
break
|
||||
|
||||
if not target:
|
||||
return {"success": False, "message": f"Component {reference} not found"}
|
||||
|
||||
# Calculate new rotation
|
||||
current_rotation = target.get("rotation", 0)
|
||||
new_rotation = (current_rotation + angle) % 360
|
||||
|
||||
# Use move_component with new rotation (position stays the same)
|
||||
success = self.ipc_board_api.move_component(
|
||||
reference=reference,
|
||||
x=target.get("position", {}).get("x", 0),
|
||||
y=target.get("position", {}).get("y", 0),
|
||||
rotation=new_rotation
|
||||
)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"message": f"Rotated component {reference} by {angle}° (visible in KiCAD UI)" if success else "Failed to rotate component",
|
||||
"newRotation": new_rotation
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC rotate_component error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _ipc_get_component_properties(self, params):
|
||||
"""IPC handler for get_component_properties - gets detailed component info"""
|
||||
try:
|
||||
reference = params.get("reference", params.get("componentId", ""))
|
||||
|
||||
components = self.ipc_board_api.list_components()
|
||||
target = None
|
||||
for comp in components:
|
||||
if comp.get("reference") == reference:
|
||||
target = comp
|
||||
break
|
||||
|
||||
if not target:
|
||||
return {"success": False, "message": f"Component {reference} not found"}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"component": target
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"IPC get_component_properties error: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
# =========================================================================
|
||||
# Legacy IPC command handlers (explicit ipc_* commands)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user