Two issues fixed: 1. TypeScript schema was missing the outline parameter entirely, so MCP clients couldn't send pour boundary points. 2. Python code read "points" key but schema defined "outline" key. Now accepts "outline" (with "points" as fallback for backwards compatibility). When no outline is provided, automatically uses the board edge bounding box as the pour boundary.
1362 lines
52 KiB
Python
1362 lines
52 KiB
Python
"""
|
||
Routing-related command implementations for KiCAD interface
|
||
"""
|
||
|
||
import os
|
||
import pcbnew
|
||
import logging
|
||
import math
|
||
from typing import Dict, Any, Optional, List, Tuple
|
||
|
||
logger = logging.getLogger("kicad_interface")
|
||
|
||
|
||
class RoutingCommands:
|
||
"""Handles routing-related KiCAD operations"""
|
||
|
||
def __init__(self, board: Optional[pcbnew.BOARD] = None):
|
||
"""Initialize with optional board instance"""
|
||
self.board = board
|
||
|
||
def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Add a new net to the PCB"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
name = params.get("name")
|
||
net_class = params.get("class")
|
||
|
||
if not name:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing net name",
|
||
"errorDetails": "name parameter is required",
|
||
}
|
||
|
||
# Create new net
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
if nets_map.has_key(name):
|
||
net = nets_map[name]
|
||
else:
|
||
net = pcbnew.NETINFO_ITEM(self.board, name)
|
||
self.board.Add(net)
|
||
|
||
# Set net class if provided
|
||
if net_class:
|
||
net_classes = self.board.GetNetClasses()
|
||
if net_classes.Find(net_class):
|
||
net.SetClass(net_classes.Find(net_class))
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Added net: {name}",
|
||
"net": {
|
||
"name": name,
|
||
"class": net_class if net_class else "Default",
|
||
"netcode": net.GetNetCode(),
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error adding net: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to add net",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def route_pad_to_pad(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Route a trace directly from one component pad to another.
|
||
|
||
Looks up pad positions automatically, then creates a trace.
|
||
Convenience wrapper around route_trace that eliminates the need
|
||
for separate get_pad_position calls.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
from_ref = params.get("fromRef")
|
||
from_pad = str(params.get("fromPad", ""))
|
||
to_ref = params.get("toRef")
|
||
to_pad = str(params.get("toPad", ""))
|
||
layer = params.get("layer", "F.Cu")
|
||
width = params.get("width")
|
||
net = params.get("net") # optional override
|
||
|
||
if not from_ref or not from_pad or not to_ref or not to_pad:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "fromRef, fromPad, toRef, toPad are all required",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm
|
||
|
||
# Find pads
|
||
footprints = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
||
|
||
for ref in [from_ref, to_ref]:
|
||
if ref not in footprints:
|
||
return {
|
||
"success": False,
|
||
"message": f"Component not found: {ref}",
|
||
"errorDetails": f"'{ref}' does not exist on the board",
|
||
}
|
||
|
||
def find_pad(ref: str, pad_num: str):
|
||
fp = footprints[ref]
|
||
for pad in fp.Pads():
|
||
if pad.GetNumber() == pad_num:
|
||
return pad
|
||
return None
|
||
|
||
start_pad = find_pad(from_ref, from_pad)
|
||
end_pad = find_pad(to_ref, to_pad)
|
||
|
||
if not start_pad:
|
||
return {
|
||
"success": False,
|
||
"message": f"Pad not found: {from_ref} pad {from_pad}",
|
||
"errorDetails": f"Check pad number for {from_ref}",
|
||
}
|
||
if not end_pad:
|
||
return {
|
||
"success": False,
|
||
"message": f"Pad not found: {to_ref} pad {to_pad}",
|
||
"errorDetails": f"Check pad number for {to_ref}",
|
||
}
|
||
|
||
start_pos = start_pad.GetPosition()
|
||
end_pos = end_pad.GetPosition()
|
||
|
||
# Use net from start pad if not overridden
|
||
if not net:
|
||
net = start_pad.GetNetname() or end_pad.GetNetname() or ""
|
||
|
||
# Delegate to 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"},
|
||
"layer": layer,
|
||
"width": width,
|
||
"net": net,
|
||
})
|
||
|
||
if result.get("success"):
|
||
result["message"] = f"Routed {from_ref}.{from_pad} → {to_ref}.{to_pad} (net: {net or 'none'})"
|
||
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
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in route_pad_to_pad: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to route pad to pad",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Route a trace between two points or pads"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
start = params.get("start")
|
||
end = params.get("end")
|
||
layer = params.get("layer", "F.Cu")
|
||
width = params.get("width")
|
||
net = params.get("net")
|
||
via = params.get("via", False)
|
||
|
||
if not start or not end:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "start and end points are required",
|
||
}
|
||
|
||
# Get layer ID
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if layer_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": f"Layer '{layer}' does not exist",
|
||
}
|
||
|
||
# Get start point
|
||
start_point = self._get_point(start)
|
||
end_point = self._get_point(end)
|
||
|
||
# Create track segment
|
||
track = pcbnew.PCB_TRACK(self.board)
|
||
track.SetStart(start_point)
|
||
track.SetEnd(end_point)
|
||
track.SetLayer(layer_id)
|
||
|
||
# Set width (default to board's current track width)
|
||
if width:
|
||
track.SetWidth(int(width * 1000000)) # Convert mm to nm
|
||
else:
|
||
track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
|
||
|
||
# Set net if provided
|
||
if net:
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
if nets_map.has_key(net):
|
||
net_obj = nets_map[net]
|
||
track.SetNet(net_obj)
|
||
|
||
# Add track to board
|
||
self.board.Add(track)
|
||
|
||
# Add via if requested and net is specified
|
||
if via and net:
|
||
via_point = end_point
|
||
self.add_via(
|
||
{
|
||
"position": {
|
||
"x": via_point.x / 1000000,
|
||
"y": via_point.y / 1000000,
|
||
"unit": "mm",
|
||
},
|
||
"net": net,
|
||
}
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Added trace",
|
||
"trace": {
|
||
"start": {
|
||
"x": start_point.x / 1000000,
|
||
"y": start_point.y / 1000000,
|
||
"unit": "mm",
|
||
},
|
||
"end": {
|
||
"x": end_point.x / 1000000,
|
||
"y": end_point.y / 1000000,
|
||
"unit": "mm",
|
||
},
|
||
"layer": layer,
|
||
"width": track.GetWidth() / 1000000,
|
||
"net": net,
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error routing trace: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to route trace",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Add a via at the specified location"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
position = params.get("position")
|
||
size = params.get("size")
|
||
drill = params.get("drill")
|
||
net = params.get("net")
|
||
from_layer = params.get("from_layer", "F.Cu")
|
||
to_layer = params.get("to_layer", "B.Cu")
|
||
|
||
if not position:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing position",
|
||
"errorDetails": "position parameter is required",
|
||
}
|
||
|
||
# Create via
|
||
via = pcbnew.PCB_VIA(self.board)
|
||
|
||
# Set position
|
||
scale = (
|
||
1000000 if position["unit"] == "mm" else 25400000
|
||
) # mm or inch to nm
|
||
x_nm = int(position["x"] * scale)
|
||
y_nm = int(position["y"] * scale)
|
||
via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
|
||
|
||
# Set size and drill (default to board's current via settings)
|
||
design_settings = self.board.GetDesignSettings()
|
||
via.SetWidth(
|
||
int(size * 1000000) if size else design_settings.GetCurrentViaSize()
|
||
)
|
||
via.SetDrill(
|
||
int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill()
|
||
)
|
||
|
||
# Set layers
|
||
from_id = self.board.GetLayerID(from_layer)
|
||
to_id = self.board.GetLayerID(to_layer)
|
||
if from_id < 0 or to_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": "Specified layers do not exist",
|
||
}
|
||
via.SetLayerPair(from_id, to_id)
|
||
|
||
# Set net if provided
|
||
if net:
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
if nets_map.has_key(net):
|
||
net_obj = nets_map[net]
|
||
via.SetNet(net_obj)
|
||
|
||
# Add via to board
|
||
self.board.Add(via)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Added via",
|
||
"via": {
|
||
"position": {
|
||
"x": position["x"],
|
||
"y": position["y"],
|
||
"unit": position["unit"],
|
||
},
|
||
"size": via.GetWidth() / 1000000,
|
||
"drill": via.GetDrill() / 1000000,
|
||
"from_layer": from_layer,
|
||
"to_layer": to_layer,
|
||
"net": net,
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error adding via: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to add via",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Delete a trace from the PCB"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
trace_uuid = params.get("traceUuid")
|
||
position = params.get("position")
|
||
net_name = params.get("net")
|
||
layer = params.get("layer")
|
||
include_vias = params.get("includeVias", False)
|
||
|
||
if not trace_uuid and not position and not net_name:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "One of traceUuid, position, or net must be provided",
|
||
}
|
||
|
||
# Delete by net name (bulk delete)
|
||
if net_name:
|
||
tracks_to_remove = []
|
||
for track in list(self.board.Tracks()):
|
||
if track.GetNetname() != net_name:
|
||
continue
|
||
|
||
# Skip vias if not requested
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
if is_via and not include_vias:
|
||
continue
|
||
|
||
# Filter by layer if specified (only for non-vias)
|
||
if layer and not is_via:
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if track.GetLayer() != layer_id:
|
||
continue
|
||
|
||
tracks_to_remove.append(track)
|
||
|
||
deleted_count = len(tracks_to_remove)
|
||
for track in tracks_to_remove:
|
||
self.board.Remove(track)
|
||
tracks_to_remove.clear()
|
||
self.board.SetModified()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Deleted {deleted_count} traces on net '{net_name}'",
|
||
"deletedCount": deleted_count,
|
||
}
|
||
|
||
# Find track by UUID
|
||
if trace_uuid:
|
||
track = None
|
||
for item in list(self.board.Tracks()):
|
||
if item.m_Uuid.AsString() == trace_uuid:
|
||
track = item
|
||
break
|
||
|
||
if not track:
|
||
return {
|
||
"success": False,
|
||
"message": "Track not found",
|
||
"errorDetails": f"Could not find track with UUID: {trace_uuid}",
|
||
}
|
||
|
||
self.board.Remove(track)
|
||
track = None
|
||
self.board.SetModified()
|
||
return {"success": True, "message": f"Deleted track: {trace_uuid}"}
|
||
|
||
# No valid parameters provided
|
||
if not position:
|
||
return {
|
||
"success": False,
|
||
"message": "No valid search parameter provided",
|
||
"errorDetails": "Provide traceUuid, position, or net parameter",
|
||
}
|
||
|
||
# Find track by position
|
||
if position:
|
||
scale = (
|
||
1000000 if position["unit"] == "mm" else 25400000
|
||
) # mm or inch to nm
|
||
x_nm = int(position["x"] * scale)
|
||
y_nm = int(position["y"] * scale)
|
||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||
|
||
# Find closest track
|
||
closest_track = None
|
||
min_distance = float("inf")
|
||
for track in list(self.board.Tracks()):
|
||
dist = self._point_to_track_distance(point, track)
|
||
if dist < min_distance:
|
||
min_distance = dist
|
||
closest_track = track
|
||
|
||
if closest_track and min_distance < 1000000: # Within 1mm
|
||
self.board.Remove(closest_track)
|
||
closest_track = None
|
||
self.board.SetModified()
|
||
return {
|
||
"success": True,
|
||
"message": "Deleted track at specified position",
|
||
}
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"message": "No track found",
|
||
"errorDetails": "No track found near specified position",
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error deleting trace: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to delete trace",
|
||
"errorDetails": str(e),
|
||
}
|
||
return {
|
||
"success": False,
|
||
"message": "No action taken",
|
||
"errorDetails": "No matching trace found for given parameters",
|
||
}
|
||
|
||
def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Get a list of all nets in the PCB"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
nets = []
|
||
netinfo = self.board.GetNetInfo()
|
||
for net_code in range(netinfo.GetNetCount()):
|
||
net = netinfo.GetNetItem(net_code)
|
||
if net:
|
||
nets.append(
|
||
{
|
||
"name": net.GetNetname(),
|
||
"code": net.GetNetCode(),
|
||
"class": net.GetNetClassName(),
|
||
}
|
||
)
|
||
|
||
return {"success": True, "nets": nets}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting nets list: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to get nets list",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def query_traces(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Query traces by net, layer, or bounding box"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
# Get filter parameters
|
||
net_name = params.get("net")
|
||
layer = params.get("layer")
|
||
bbox = params.get("boundingBox") # {x1, y1, x2, y2, unit}
|
||
include_vias = params.get("includeVias", False)
|
||
|
||
scale = 1000000 # nm to mm conversion factor
|
||
traces = []
|
||
vias = []
|
||
|
||
# Process tracks
|
||
for track in list(self.board.Tracks()):
|
||
try:
|
||
# Check if it's a via
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
|
||
if is_via and not include_vias:
|
||
continue
|
||
|
||
# Filter by net
|
||
if net_name and track.GetNetname() != net_name:
|
||
continue
|
||
|
||
# Filter by layer (only for tracks, not vias)
|
||
if layer and not is_via:
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if track.GetLayer() != layer_id:
|
||
continue
|
||
|
||
# Filter by bounding box
|
||
if bbox:
|
||
bbox_unit = bbox.get("unit", "mm")
|
||
bbox_scale = scale if bbox_unit == "mm" else 25400000
|
||
x1 = int(bbox.get("x1", 0) * bbox_scale)
|
||
y1 = int(bbox.get("y1", 0) * bbox_scale)
|
||
x2 = int(bbox.get("x2", 0) * bbox_scale)
|
||
y2 = int(bbox.get("y2", 0) * bbox_scale)
|
||
|
||
if is_via:
|
||
pos = track.GetPosition()
|
||
if not (x1 <= pos.x <= x2 and y1 <= pos.y <= y2):
|
||
continue
|
||
else:
|
||
start = track.GetStart()
|
||
end = track.GetEnd()
|
||
# Check if either endpoint is within bbox
|
||
start_in = x1 <= start.x <= x2 and y1 <= start.y <= y2
|
||
end_in = x1 <= end.x <= x2 and y1 <= end.y <= y2
|
||
if not (start_in or end_in):
|
||
continue
|
||
|
||
if is_via:
|
||
pos = track.GetPosition()
|
||
vias.append(
|
||
{
|
||
"uuid": track.m_Uuid.AsString(),
|
||
"position": {
|
||
"x": pos.x / scale,
|
||
"y": pos.y / scale,
|
||
"unit": "mm",
|
||
},
|
||
"net": track.GetNetname(),
|
||
"netCode": track.GetNetCode(),
|
||
"diameter": track.GetWidth() / scale,
|
||
"drill": track.GetDrillValue() / scale,
|
||
}
|
||
)
|
||
else:
|
||
start = track.GetStart()
|
||
end = track.GetEnd()
|
||
traces.append(
|
||
{
|
||
"uuid": track.m_Uuid.AsString(),
|
||
"net": track.GetNetname(),
|
||
"netCode": track.GetNetCode(),
|
||
"layer": self.board.GetLayerName(track.GetLayer()),
|
||
"width": track.GetWidth() / scale,
|
||
"start": {
|
||
"x": start.x / scale,
|
||
"y": start.y / scale,
|
||
"unit": "mm",
|
||
},
|
||
"end": {
|
||
"x": end.x / scale,
|
||
"y": end.y / scale,
|
||
"unit": "mm",
|
||
},
|
||
"length": track.GetLength() / scale,
|
||
}
|
||
)
|
||
except Exception as track_err:
|
||
logger.warning(f"Skipping invalid track object: {track_err}")
|
||
continue
|
||
|
||
result = {"success": True, "traceCount": len(traces), "traces": traces}
|
||
|
||
if include_vias:
|
||
result["viaCount"] = len(vias)
|
||
result["vias"] = vias
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error querying traces: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to query traces",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Modify properties of an existing trace
|
||
|
||
Allows changing trace width, layer, and net assignment.
|
||
Find trace by UUID or position.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
# Identification parameters
|
||
trace_uuid = params.get("uuid")
|
||
position = params.get("position") # {x, y, unit}
|
||
|
||
# Modification parameters
|
||
new_width = params.get("width") # in mm
|
||
new_layer = params.get("layer")
|
||
new_net = params.get("net")
|
||
|
||
if not trace_uuid and not position:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing trace identifier",
|
||
"errorDetails": "Provide either 'uuid' or 'position' to identify the trace",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm conversion
|
||
|
||
# Find the track
|
||
track = None
|
||
|
||
if trace_uuid:
|
||
for item in list(self.board.Tracks()):
|
||
if item.m_Uuid.AsString() == trace_uuid:
|
||
track = item
|
||
break
|
||
elif position:
|
||
pos_unit = position.get("unit", "mm")
|
||
pos_scale = scale if pos_unit == "mm" else 25400000
|
||
x_nm = int(position["x"] * pos_scale)
|
||
y_nm = int(position["y"] * pos_scale)
|
||
point = pcbnew.VECTOR2I(x_nm, y_nm)
|
||
|
||
# Find closest track
|
||
min_distance = float("inf")
|
||
for item in list(self.board.Tracks()):
|
||
dist = self._point_to_track_distance(point, item)
|
||
if dist < min_distance:
|
||
min_distance = dist
|
||
track = item
|
||
|
||
# Only accept if within 1mm
|
||
if min_distance >= 1000000:
|
||
track = None
|
||
|
||
if not track:
|
||
return {
|
||
"success": False,
|
||
"message": "Track not found",
|
||
"errorDetails": "Could not find track with specified identifier",
|
||
}
|
||
|
||
# Check if it's a via (some modifications don't apply)
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
modifications = []
|
||
|
||
# Apply modifications
|
||
if new_width is not None:
|
||
width_nm = int(new_width * scale)
|
||
track.SetWidth(width_nm)
|
||
modifications.append(f"width={new_width}mm")
|
||
|
||
if new_layer and not is_via:
|
||
layer_id = self.board.GetLayerID(new_layer)
|
||
if layer_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": f"Layer '{new_layer}' not found",
|
||
}
|
||
track.SetLayer(layer_id)
|
||
modifications.append(f"layer={new_layer}")
|
||
|
||
if new_net:
|
||
netinfo = self.board.GetNetInfo()
|
||
net = netinfo.GetNetItem(new_net)
|
||
if not net:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid net",
|
||
"errorDetails": f"Net '{new_net}' not found",
|
||
}
|
||
track.SetNet(net)
|
||
modifications.append(f"net={new_net}")
|
||
|
||
if not modifications:
|
||
return {
|
||
"success": False,
|
||
"message": "No modifications specified",
|
||
"errorDetails": "Provide at least one of: width, layer, net",
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Modified trace: {', '.join(modifications)}",
|
||
"uuid": track.m_Uuid.AsString(),
|
||
"modifications": modifications,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error modifying trace: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to modify trace",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def copy_routing_pattern(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Copy routing pattern from source components to target components
|
||
|
||
This enables routing replication between identical component groups.
|
||
The pattern is copied with a translation offset calculated from
|
||
the position difference between source and target components.
|
||
"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
source_refs = params.get("sourceRefs", []) # e.g., ["U1", "U2", "U3"]
|
||
target_refs = params.get("targetRefs", []) # e.g., ["U4", "U5", "U6"]
|
||
include_vias = params.get("includeVias", True)
|
||
trace_width = params.get("traceWidth") # Optional override
|
||
|
||
if not source_refs or not target_refs:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing component references",
|
||
"errorDetails": "Provide both 'sourceRefs' and 'targetRefs' arrays",
|
||
}
|
||
|
||
if len(source_refs) != len(target_refs):
|
||
return {
|
||
"success": False,
|
||
"message": "Mismatched component counts",
|
||
"errorDetails": f"sourceRefs has {len(source_refs)} items, targetRefs has {len(target_refs)}",
|
||
}
|
||
|
||
scale = 1000000 # nm to mm conversion
|
||
|
||
# Get footprints
|
||
footprints = {fp.GetReference(): fp for fp in self.board.GetFootprints()}
|
||
|
||
# Validate all references exist
|
||
for ref in source_refs + target_refs:
|
||
if ref not in footprints:
|
||
return {
|
||
"success": False,
|
||
"message": "Component not found",
|
||
"errorDetails": f"Component '{ref}' not found on board",
|
||
}
|
||
|
||
# Calculate offset from first source to first target component
|
||
source_fp = footprints[source_refs[0]]
|
||
target_fp = footprints[target_refs[0]]
|
||
source_pos = source_fp.GetPosition()
|
||
target_pos = target_fp.GetPosition()
|
||
|
||
offset_x = target_pos.x - source_pos.x
|
||
offset_y = target_pos.y - source_pos.y
|
||
|
||
# Build mapping from source refs to target refs
|
||
ref_mapping = dict(zip(source_refs, target_refs))
|
||
|
||
# Collect all nets connected to source components
|
||
source_nets = set()
|
||
source_pad_positions = [] # (x, y) in nm for geometric fallback
|
||
for ref in source_refs:
|
||
fp = footprints[ref]
|
||
for pad in fp.Pads():
|
||
net_name = pad.GetNetname()
|
||
if net_name and net_name != "":
|
||
source_nets.add(net_name)
|
||
pos = pad.GetPosition()
|
||
source_pad_positions.append((pos.x, pos.y))
|
||
|
||
# Build bounding box around source pads (with 5mm tolerance in nm)
|
||
TOLERANCE_NM = int(5 * scale)
|
||
if source_pad_positions:
|
||
xs = [p[0] for p in source_pad_positions]
|
||
ys = [p[1] for p in source_pad_positions]
|
||
bbox_x1 = min(xs) - TOLERANCE_NM
|
||
bbox_x2 = max(xs) + TOLERANCE_NM
|
||
bbox_y1 = min(ys) - TOLERANCE_NM
|
||
bbox_y2 = max(ys) + TOLERANCE_NM
|
||
else:
|
||
# Fall back to component position ± 25mm
|
||
sp = source_fp.GetPosition()
|
||
bbox_x1 = sp.x - int(25 * scale)
|
||
bbox_x2 = sp.x + int(25 * scale)
|
||
bbox_y1 = sp.y - int(25 * scale)
|
||
bbox_y2 = sp.y + int(25 * scale)
|
||
|
||
def point_in_bbox(px: int, py: int) -> bool:
|
||
return bbox_x1 <= px <= bbox_x2 and bbox_y1 <= py <= bbox_y2
|
||
|
||
# Collect traces: by net name (if available) OR by geometric proximity
|
||
use_net_filter = len(source_nets) > 0
|
||
traces_to_copy = []
|
||
vias_to_copy = []
|
||
|
||
for track in list(self.board.Tracks()):
|
||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
||
|
||
if use_net_filter:
|
||
# Primary: net-based filter
|
||
if track.GetNetname() not in source_nets:
|
||
continue
|
||
else:
|
||
# Fallback: geometric filter – trace start OR end inside source bbox
|
||
if is_via:
|
||
pos = track.GetPosition()
|
||
if not point_in_bbox(pos.x, pos.y):
|
||
continue
|
||
else:
|
||
s = track.GetStart()
|
||
e = track.GetEnd()
|
||
if not (point_in_bbox(s.x, s.y) or point_in_bbox(e.x, e.y)):
|
||
continue
|
||
|
||
if is_via:
|
||
if include_vias:
|
||
vias_to_copy.append(track)
|
||
else:
|
||
traces_to_copy.append(track)
|
||
|
||
filter_method = "net-based" if use_net_filter else "geometric (pads have no nets)"
|
||
logger.info(
|
||
f"copy_routing_pattern: {len(traces_to_copy)} traces, "
|
||
f"{len(vias_to_copy)} vias selected via {filter_method}"
|
||
)
|
||
|
||
# Create new traces with offset
|
||
created_traces = 0
|
||
created_vias = 0
|
||
|
||
for track in traces_to_copy:
|
||
start = track.GetStart()
|
||
end = track.GetEnd()
|
||
|
||
# Create new track
|
||
new_track = pcbnew.PCB_TRACK(self.board)
|
||
new_track.SetStart(
|
||
pcbnew.VECTOR2I(start.x + offset_x, start.y + offset_y)
|
||
)
|
||
new_track.SetEnd(pcbnew.VECTOR2I(end.x + offset_x, end.y + offset_y))
|
||
new_track.SetLayer(track.GetLayer())
|
||
|
||
# Set width (use override or original)
|
||
if trace_width:
|
||
new_track.SetWidth(int(trace_width * scale))
|
||
else:
|
||
new_track.SetWidth(track.GetWidth())
|
||
|
||
# Try to find corresponding target net
|
||
# This is a simplification - more sophisticated mapping would be needed
|
||
# for complex designs
|
||
self.board.Add(new_track)
|
||
created_traces += 1
|
||
|
||
for via in vias_to_copy:
|
||
pos = via.GetPosition()
|
||
|
||
# Create new via
|
||
new_via = pcbnew.PCB_VIA(self.board)
|
||
new_via.SetPosition(pcbnew.VECTOR2I(pos.x + offset_x, pos.y + offset_y))
|
||
new_via.SetWidth(via.GetWidth())
|
||
new_via.SetDrill(via.GetDrillValue())
|
||
new_via.SetViaType(via.GetViaType())
|
||
|
||
self.board.Add(new_via)
|
||
created_vias += 1
|
||
|
||
result = {
|
||
"success": True,
|
||
"message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias",
|
||
"filterMethod": filter_method,
|
||
"offset": {"x": offset_x / scale, "y": offset_y / scale, "unit": "mm"},
|
||
"createdTraces": created_traces,
|
||
"createdVias": created_vias,
|
||
"sourceComponents": source_refs,
|
||
"targetComponents": target_refs,
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error copying routing pattern: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to copy routing pattern",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Create a new net class with specified properties"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
name = params.get("name")
|
||
clearance = params.get("clearance")
|
||
track_width = params.get("trackWidth")
|
||
via_diameter = params.get("viaDiameter")
|
||
via_drill = params.get("viaDrill")
|
||
uvia_diameter = params.get("uviaDiameter")
|
||
uvia_drill = params.get("uviaDrill")
|
||
diff_pair_width = params.get("diffPairWidth")
|
||
diff_pair_gap = params.get("diffPairGap")
|
||
nets = params.get("nets", [])
|
||
|
||
if not name:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing netclass name",
|
||
"errorDetails": "name parameter is required",
|
||
}
|
||
|
||
# Get net classes
|
||
net_classes = self.board.GetNetClasses()
|
||
|
||
# Create new net class if it doesn't exist
|
||
if not net_classes.Find(name):
|
||
netclass = pcbnew.NETCLASS(name)
|
||
net_classes.Add(netclass)
|
||
else:
|
||
netclass = net_classes.Find(name)
|
||
|
||
# Set properties
|
||
scale = 1000000 # mm to nm
|
||
if clearance is not None:
|
||
netclass.SetClearance(int(clearance * scale))
|
||
if track_width is not None:
|
||
netclass.SetTrackWidth(int(track_width * scale))
|
||
if via_diameter is not None:
|
||
netclass.SetViaDiameter(int(via_diameter * scale))
|
||
if via_drill is not None:
|
||
netclass.SetViaDrill(int(via_drill * scale))
|
||
if uvia_diameter is not None:
|
||
netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
|
||
if uvia_drill is not None:
|
||
netclass.SetMicroViaDrill(int(uvia_drill * scale))
|
||
if diff_pair_width is not None:
|
||
netclass.SetDiffPairWidth(int(diff_pair_width * scale))
|
||
if diff_pair_gap is not None:
|
||
netclass.SetDiffPairGap(int(diff_pair_gap * scale))
|
||
|
||
# Add nets to net class
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
for net_name in nets:
|
||
if nets_map.has_key(net_name):
|
||
net = nets_map[net_name]
|
||
net.SetClass(netclass)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Created net class: {name}",
|
||
"netClass": {
|
||
"name": name,
|
||
"clearance": netclass.GetClearance() / scale,
|
||
"trackWidth": netclass.GetTrackWidth() / scale,
|
||
"viaDiameter": netclass.GetViaDiameter() / scale,
|
||
"viaDrill": netclass.GetViaDrill() / scale,
|
||
"uviaDiameter": netclass.GetMicroViaDiameter() / scale,
|
||
"uviaDrill": netclass.GetMicroViaDrill() / scale,
|
||
"diffPairWidth": netclass.GetDiffPairWidth() / scale,
|
||
"diffPairGap": netclass.GetDiffPairGap() / scale,
|
||
"nets": nets,
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error creating net class: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to create net class",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Add a copper pour (zone) to the PCB"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
layer = params.get("layer", "F.Cu")
|
||
net = params.get("net")
|
||
clearance = params.get("clearance")
|
||
min_width = params.get("minWidth", 0.2)
|
||
points = params.get("outline", params.get("points", []))
|
||
priority = params.get("priority", 0)
|
||
fill_type = params.get("fillType", "solid") # solid or hatched
|
||
|
||
# If no outline provided, use board outline
|
||
if not points or len(points) < 3:
|
||
board_box = self.board.GetBoardEdgesBoundingBox()
|
||
if board_box.GetWidth() > 0 and board_box.GetHeight() > 0:
|
||
scale = 1000000 # nm to mm
|
||
x1 = board_box.GetX() / scale
|
||
y1 = board_box.GetY() / scale
|
||
x2 = (board_box.GetX() + board_box.GetWidth()) / scale
|
||
y2 = (board_box.GetY() + board_box.GetHeight()) / scale
|
||
points = [
|
||
{"x": x1, "y": y1},
|
||
{"x": x2, "y": y1},
|
||
{"x": x2, "y": y2},
|
||
{"x": x1, "y": y2},
|
||
]
|
||
else:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing outline",
|
||
"errorDetails": "Provide an outline array or add a board outline first",
|
||
}
|
||
|
||
# Get layer ID
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if layer_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": f"Layer '{layer}' does not exist",
|
||
}
|
||
|
||
# Create zone
|
||
zone = pcbnew.ZONE(self.board)
|
||
zone.SetLayer(layer_id)
|
||
|
||
# Set net if provided
|
||
if net:
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
if nets_map.has_key(net):
|
||
net_obj = nets_map[net]
|
||
zone.SetNet(net_obj)
|
||
|
||
# Set zone properties
|
||
scale = 1000000 # mm to nm
|
||
zone.SetAssignedPriority(priority)
|
||
|
||
if clearance is not None:
|
||
zone.SetLocalClearance(int(clearance * scale))
|
||
|
||
zone.SetMinThickness(int(min_width * scale))
|
||
|
||
# Set fill type
|
||
if fill_type == "hatched":
|
||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
|
||
else:
|
||
zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGONS)
|
||
|
||
# Create outline
|
||
outline = zone.Outline()
|
||
outline.NewOutline() # Create a new outline contour first
|
||
|
||
# Add points to outline
|
||
for point in points:
|
||
scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
|
||
x_nm = int(point["x"] * scale)
|
||
y_nm = int(point["y"] * scale)
|
||
outline.Append(pcbnew.VECTOR2I(x_nm, y_nm)) # Add point to outline
|
||
|
||
# Add zone to board
|
||
self.board.Add(zone)
|
||
|
||
# Fill zone
|
||
# Note: Zone filling can cause issues with SWIG API
|
||
# Comment out for now - zones will be filled when board is saved/opened in KiCAD
|
||
# filler = pcbnew.ZONE_FILLER(self.board)
|
||
# filler.Fill(self.board.Zones())
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Added copper pour",
|
||
"pour": {
|
||
"layer": layer,
|
||
"net": net,
|
||
"clearance": clearance,
|
||
"minWidth": min_width,
|
||
"priority": priority,
|
||
"fillType": fill_type,
|
||
"pointCount": len(points),
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error adding copper pour: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to add copper pour",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Route a differential pair between two sets of points or pads"""
|
||
try:
|
||
if not self.board:
|
||
return {
|
||
"success": False,
|
||
"message": "No board is loaded",
|
||
"errorDetails": "Load or create a board first",
|
||
}
|
||
|
||
start_pos = params.get("startPos")
|
||
end_pos = params.get("endPos")
|
||
net_pos = params.get("netPos")
|
||
net_neg = params.get("netNeg")
|
||
layer = params.get("layer", "F.Cu")
|
||
width = params.get("width")
|
||
gap = params.get("gap")
|
||
|
||
if not start_pos or not end_pos or not net_pos or not net_neg:
|
||
return {
|
||
"success": False,
|
||
"message": "Missing parameters",
|
||
"errorDetails": "startPos, endPos, netPos, and netNeg are required",
|
||
}
|
||
|
||
# Get layer ID
|
||
layer_id = self.board.GetLayerID(layer)
|
||
if layer_id < 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid layer",
|
||
"errorDetails": f"Layer '{layer}' does not exist",
|
||
}
|
||
|
||
# Get nets
|
||
netinfo = self.board.GetNetInfo()
|
||
nets_map = netinfo.NetsByName()
|
||
|
||
net_pos_obj = nets_map[net_pos] if nets_map.has_key(net_pos) else None
|
||
net_neg_obj = nets_map[net_neg] if nets_map.has_key(net_neg) else None
|
||
|
||
if not net_pos_obj or not net_neg_obj:
|
||
return {
|
||
"success": False,
|
||
"message": "Nets not found",
|
||
"errorDetails": "One or both nets specified for the differential pair do not exist",
|
||
}
|
||
|
||
# Get start and end points
|
||
start_point = self._get_point(start_pos)
|
||
end_point = self._get_point(end_pos)
|
||
|
||
# Calculate offset vectors for the two traces
|
||
# First, get the direction vector from start to end
|
||
dx = end_point.x - start_point.x
|
||
dy = end_point.y - start_point.y
|
||
length = math.sqrt(dx * dx + dy * dy)
|
||
|
||
if length <= 0:
|
||
return {
|
||
"success": False,
|
||
"message": "Invalid points",
|
||
"errorDetails": "Start and end points must be different",
|
||
}
|
||
|
||
# Normalize direction vector
|
||
dx /= length
|
||
dy /= length
|
||
|
||
# Get perpendicular vector
|
||
px = -dy
|
||
py = dx
|
||
|
||
# Set default gap if not provided
|
||
if gap is None:
|
||
gap = 0.2 # mm
|
||
|
||
# Convert to nm
|
||
gap_nm = int(gap * 1000000)
|
||
|
||
# Calculate offsets
|
||
offset_x = int(px * gap_nm / 2)
|
||
offset_y = int(py * gap_nm / 2)
|
||
|
||
# Create positive and negative trace points
|
||
pos_start = pcbnew.VECTOR2I(
|
||
int(start_point.x + offset_x), int(start_point.y + offset_y)
|
||
)
|
||
pos_end = pcbnew.VECTOR2I(
|
||
int(end_point.x + offset_x), int(end_point.y + offset_y)
|
||
)
|
||
neg_start = pcbnew.VECTOR2I(
|
||
int(start_point.x - offset_x), int(start_point.y - offset_y)
|
||
)
|
||
neg_end = pcbnew.VECTOR2I(
|
||
int(end_point.x - offset_x), int(end_point.y - offset_y)
|
||
)
|
||
|
||
# Create positive trace
|
||
pos_track = pcbnew.PCB_TRACK(self.board)
|
||
pos_track.SetStart(pos_start)
|
||
pos_track.SetEnd(pos_end)
|
||
pos_track.SetLayer(layer_id)
|
||
pos_track.SetNet(net_pos_obj)
|
||
|
||
# Create negative trace
|
||
neg_track = pcbnew.PCB_TRACK(self.board)
|
||
neg_track.SetStart(neg_start)
|
||
neg_track.SetEnd(neg_end)
|
||
neg_track.SetLayer(layer_id)
|
||
neg_track.SetNet(net_neg_obj)
|
||
|
||
# Set width
|
||
if width:
|
||
trace_width_nm = int(width * 1000000)
|
||
pos_track.SetWidth(trace_width_nm)
|
||
neg_track.SetWidth(trace_width_nm)
|
||
else:
|
||
# Get default width from design rules or net class
|
||
trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
|
||
pos_track.SetWidth(trace_width)
|
||
neg_track.SetWidth(trace_width)
|
||
|
||
# Add tracks to board
|
||
self.board.Add(pos_track)
|
||
self.board.Add(neg_track)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Added differential pair traces",
|
||
"diffPair": {
|
||
"posNet": net_pos,
|
||
"negNet": net_neg,
|
||
"layer": layer,
|
||
"width": pos_track.GetWidth() / 1000000,
|
||
"gap": gap,
|
||
"length": length / 1000000,
|
||
},
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error routing differential pair: {str(e)}")
|
||
return {
|
||
"success": False,
|
||
"message": "Failed to route differential pair",
|
||
"errorDetails": str(e),
|
||
}
|
||
|
||
def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
|
||
"""Convert point specification to KiCAD point"""
|
||
if "x" in point_spec and "y" in point_spec:
|
||
scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
|
||
x_nm = int(point_spec["x"] * scale)
|
||
y_nm = int(point_spec["y"] * scale)
|
||
return pcbnew.VECTOR2I(x_nm, y_nm)
|
||
elif "pad" in point_spec and "componentRef" in point_spec:
|
||
module = self.board.FindFootprintByReference(point_spec["componentRef"])
|
||
if module:
|
||
pad = module.FindPadByName(point_spec["pad"])
|
||
if pad:
|
||
return pad.GetPosition()
|
||
raise ValueError("Invalid point specification")
|
||
|
||
def _point_to_track_distance(
|
||
self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK
|
||
) -> float:
|
||
"""Calculate distance from point to track segment"""
|
||
start = track.GetStart()
|
||
end = track.GetEnd()
|
||
|
||
# Vector from start to end
|
||
v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
|
||
# Vector from start to point
|
||
w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
|
||
|
||
# Length of track squared
|
||
c1 = v.x * v.x + v.y * v.y
|
||
if c1 == 0:
|
||
return self._point_distance(point, start)
|
||
|
||
# Projection coefficient
|
||
c2 = float(w.x * v.x + w.y * v.y) / c1
|
||
|
||
if c2 < 0:
|
||
return self._point_distance(point, start)
|
||
elif c2 > 1:
|
||
return self._point_distance(point, end)
|
||
|
||
# Point on line
|
||
proj = pcbnew.VECTOR2I(int(start.x + c2 * v.x), int(start.y + c2 * v.y))
|
||
return self._point_distance(point, proj)
|
||
|
||
def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
|
||
"""Calculate distance between two points"""
|
||
dx = p1.x - p2.x
|
||
dy = p1.y - p2.y
|
||
return (dx * dx + dy * dy) ** 0.5
|