Merge pull request #49 from Kletternaut/feat/footprint-symbol-tools
fix: project-local library resolution + copy_routing_pattern geometric fallback + TypeScript tool
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -62,8 +62,11 @@ logs/
|
|||||||
|
|
||||||
# KiCAD
|
# KiCAD
|
||||||
*.kicad_pcb-bak
|
*.kicad_pcb-bak
|
||||||
|
*.kicad_pcb.bak
|
||||||
*.kicad_sch-bak
|
*.kicad_sch-bak
|
||||||
|
*.kicad_sch.bak
|
||||||
*.kicad_pro-bak
|
*.kicad_pro-bak
|
||||||
|
*.kicad_pro.bak
|
||||||
*.kicad_prl
|
*.kicad_prl
|
||||||
*-backups/
|
*-backups/
|
||||||
fp-info-cache
|
fp-info-cache
|
||||||
@@ -82,3 +85,6 @@ data/
|
|||||||
# OS
|
# OS
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
|
||||||
|
# Personal notes / local contributions (not for upstream)
|
||||||
|
myContribution/
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ class DynamicSymbolLoader:
|
|||||||
- Parent symbols must appear BEFORE child symbols that use (extends ...)
|
- Parent symbols must appear BEFORE child symbols that use (extends ...)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, project_path: Optional[Path] = None):
|
||||||
self.symbol_cache = {} # Cache: "lib:symbol" -> raw text block
|
self.symbol_cache = {} # Cache: "lib:symbol" -> raw text block
|
||||||
|
self.project_path = project_path # Project directory for project-specific libraries
|
||||||
|
|
||||||
def find_kicad_symbol_libraries(self) -> List[Path]:
|
def find_kicad_symbol_libraries(self) -> List[Path]:
|
||||||
"""Find all KiCad symbol library directories"""
|
"""Find all KiCad symbol library directories"""
|
||||||
@@ -50,14 +51,74 @@ class DynamicSymbolLoader:
|
|||||||
return [p for p in possible_paths if p.exists() and p.is_dir()]
|
return [p for p in possible_paths if p.exists() and p.is_dir()]
|
||||||
|
|
||||||
def find_library_file(self, library_name: str) -> Optional[Path]:
|
def find_library_file(self, library_name: str) -> Optional[Path]:
|
||||||
"""Find the .kicad_sym file for a given library name"""
|
"""Find the .kicad_sym file for a given library name.
|
||||||
|
|
||||||
|
Search order:
|
||||||
|
1. Project-specific sym-lib-table (if project_path is set)
|
||||||
|
2. Global KiCad symbol library directories
|
||||||
|
"""
|
||||||
|
# 1. Check project-specific sym-lib-table
|
||||||
|
if self.project_path:
|
||||||
|
project_table = Path(self.project_path) / "sym-lib-table"
|
||||||
|
if project_table.exists():
|
||||||
|
resolved = self._resolve_library_from_table(project_table, library_name)
|
||||||
|
if resolved:
|
||||||
|
logger.info(f"Found '{library_name}' in project sym-lib-table: {resolved}")
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
# 2. Fall back to global KiCad symbol directories
|
||||||
for lib_dir in self.find_kicad_symbol_libraries():
|
for lib_dir in self.find_kicad_symbol_libraries():
|
||||||
lib_file = lib_dir / f"{library_name}.kicad_sym"
|
lib_file = lib_dir / f"{library_name}.kicad_sym"
|
||||||
if lib_file.exists():
|
if lib_file.exists():
|
||||||
return lib_file
|
return lib_file
|
||||||
|
|
||||||
logger.warning(f"Library file not found: {library_name}.kicad_sym")
|
logger.warning(f"Library file not found: {library_name}.kicad_sym")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _resolve_library_from_table(self, table_path: Path, library_name: str) -> Optional[Path]:
|
||||||
|
"""Parse a sym-lib-table file and return the resolved path for the given library nickname."""
|
||||||
|
try:
|
||||||
|
with open(table_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
lib_pattern = r'\(lib\s+\(name\s+"?([^"\)\s]+)"?\)\s*\(type\s+[^)]+\)\s*\(uri\s+"?([^"\)\s]+)"?'
|
||||||
|
for match in re.finditer(lib_pattern, content, re.IGNORECASE):
|
||||||
|
nickname = match.group(1)
|
||||||
|
if nickname != library_name:
|
||||||
|
continue
|
||||||
|
uri = match.group(2)
|
||||||
|
resolved = self._resolve_sym_uri(uri)
|
||||||
|
if resolved and Path(resolved).exists():
|
||||||
|
return Path(resolved)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not parse sym-lib-table {table_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _resolve_sym_uri(self, uri: str) -> Optional[str]:
|
||||||
|
"""Resolve environment variables in a sym-lib-table URI."""
|
||||||
|
env_map = {
|
||||||
|
"KICAD9_SYMBOL_DIR": [
|
||||||
|
"C:/Program Files/KiCad/9.0/share/kicad/symbols",
|
||||||
|
"/usr/share/kicad/symbols",
|
||||||
|
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols",
|
||||||
|
],
|
||||||
|
"KICAD8_SYMBOL_DIR": [
|
||||||
|
"C:/Program Files/KiCad/8.0/share/kicad/symbols",
|
||||||
|
],
|
||||||
|
"KIPRJMOD": [str(self.project_path)] if self.project_path else [],
|
||||||
|
}
|
||||||
|
result = uri
|
||||||
|
for var, candidates in env_map.items():
|
||||||
|
if f"${{{var}}}" in result:
|
||||||
|
for candidate in candidates:
|
||||||
|
candidate_path = result.replace(f"${{{var}}}", candidate)
|
||||||
|
if Path(candidate_path).exists():
|
||||||
|
return candidate_path
|
||||||
|
# Fallback: try OS env
|
||||||
|
if var in os.environ:
|
||||||
|
return result.replace(f"${{{var}}}", os.environ[var])
|
||||||
|
return result
|
||||||
|
|
||||||
def _extract_symbol_block(self, text: str, symbol_name: str) -> Optional[str]:
|
def _extract_symbol_block(self, text: str, symbol_name: str) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Extract a complete symbol block from a library or schematic file by matching
|
Extract a complete symbol block from a library or schematic file by matching
|
||||||
@@ -345,11 +406,18 @@ class DynamicSymbolLoader:
|
|||||||
footprint: str = "",
|
footprint: str = "",
|
||||||
x: float = 0,
|
x: float = 0,
|
||||||
y: float = 0,
|
y: float = 0,
|
||||||
|
project_path: Optional[Path] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
High-level: ensure symbol definition exists in schematic, then add an instance.
|
High-level: ensure symbol definition exists in schematic, then add an instance.
|
||||||
This is the main entry point for adding components.
|
This is the main entry point for adding components.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_path: Optional project directory. When set, project-specific
|
||||||
|
sym-lib-table is also searched for the library file.
|
||||||
"""
|
"""
|
||||||
|
if project_path:
|
||||||
|
self.project_path = project_path
|
||||||
# Ensure symbol definition is in lib_symbols
|
# Ensure symbol definition is in lib_symbols
|
||||||
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,103 @@ class RoutingCommands:
|
|||||||
"errorDetails": str(e),
|
"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]:
|
def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Route a trace between two points or pads"""
|
"""Route a trace between two points or pads"""
|
||||||
try:
|
try:
|
||||||
@@ -729,29 +826,72 @@ class RoutingCommands:
|
|||||||
|
|
||||||
# Collect all nets connected to source components
|
# Collect all nets connected to source components
|
||||||
source_nets = set()
|
source_nets = set()
|
||||||
|
source_pad_positions = [] # (x, y) in nm for geometric fallback
|
||||||
for ref in source_refs:
|
for ref in source_refs:
|
||||||
fp = footprints[ref]
|
fp = footprints[ref]
|
||||||
for pad in fp.Pads():
|
for pad in fp.Pads():
|
||||||
net_name = pad.GetNetname()
|
net_name = pad.GetNetname()
|
||||||
if net_name and net_name != "":
|
if net_name and net_name != "":
|
||||||
source_nets.add(net_name)
|
source_nets.add(net_name)
|
||||||
|
pos = pad.GetPosition()
|
||||||
|
source_pad_positions.append((pos.x, pos.y))
|
||||||
|
|
||||||
# Collect traces and vias connected to source nets
|
# 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 = []
|
traces_to_copy = []
|
||||||
vias_to_copy = []
|
vias_to_copy = []
|
||||||
|
|
||||||
for track in list(self.board.Tracks()):
|
for track in list(self.board.Tracks()):
|
||||||
if track.GetNetname() not in source_nets:
|
|
||||||
continue
|
|
||||||
|
|
||||||
is_via = track.Type() == pcbnew.PCB_VIA_T
|
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 is_via:
|
||||||
if include_vias:
|
if include_vias:
|
||||||
vias_to_copy.append(track)
|
vias_to_copy.append(track)
|
||||||
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)"
|
||||||
|
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
|
# Create new traces with offset
|
||||||
created_traces = 0
|
created_traces = 0
|
||||||
created_vias = 0
|
created_vias = 0
|
||||||
@@ -796,6 +936,7 @@ class RoutingCommands:
|
|||||||
result = {
|
result = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Copied routing pattern: {created_traces} traces, {created_vias} vias",
|
"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"},
|
"offset": {"x": offset_x / scale, "y": offset_y / scale, "unit": "mm"},
|
||||||
"createdTraces": created_traces,
|
"createdTraces": created_traces,
|
||||||
"createdVias": created_vias,
|
"createdVias": created_vias,
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ class KiCADInterface:
|
|||||||
self.design_rule_commands = DesignRuleCommands(self.board)
|
self.design_rule_commands = DesignRuleCommands(self.board)
|
||||||
self.export_commands = ExportCommands(self.board)
|
self.export_commands = ExportCommands(self.board)
|
||||||
self.library_commands = LibraryCommands(self.footprint_library)
|
self.library_commands = LibraryCommands(self.footprint_library)
|
||||||
|
self._current_project_path: Optional[Path] = None # set when boardPath is known
|
||||||
|
|
||||||
# Initialize symbol library manager (for searching local KiCad symbol libraries)
|
# Initialize symbol library manager (for searching local KiCad symbol libraries)
|
||||||
self.symbol_library_commands = SymbolLibraryCommands()
|
self.symbol_library_commands = SymbolLibraryCommands()
|
||||||
@@ -307,7 +308,8 @@ class KiCADInterface:
|
|||||||
"add_text": self.board_commands.add_text,
|
"add_text": self.board_commands.add_text,
|
||||||
"add_board_text": self.board_commands.add_text, # Alias for TypeScript tool
|
"add_board_text": self.board_commands.add_text, # Alias for TypeScript tool
|
||||||
# Component commands
|
# Component commands
|
||||||
"place_component": self.component_commands.place_component,
|
"route_pad_to_pad": self.routing_commands.route_pad_to_pad,
|
||||||
|
"place_component": self._handle_place_component,
|
||||||
"move_component": self.component_commands.move_component,
|
"move_component": self.component_commands.move_component,
|
||||||
"rotate_component": self.component_commands.rotate_component,
|
"rotate_component": self.component_commands.rotate_component,
|
||||||
"delete_component": self.component_commands.delete_component,
|
"delete_component": self.component_commands.delete_component,
|
||||||
@@ -579,6 +581,21 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error loading schematic: {str(e)}")
|
logger.error(f"Error loading schematic: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def _handle_place_component(self, params):
|
||||||
|
"""Place a component on the PCB, with project-local fp-lib-table support."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
board_path = params.get("boardPath")
|
||||||
|
if board_path:
|
||||||
|
project_path = Path(board_path).parent
|
||||||
|
if project_path != getattr(self, "_current_project_path", None):
|
||||||
|
self._current_project_path = project_path
|
||||||
|
local_lib = FootprintLibraryManager(project_path=project_path)
|
||||||
|
self.component_commands = ComponentCommands(self.board, local_lib)
|
||||||
|
logger.info(f"Reloaded FootprintLibraryManager with project_path={project_path}")
|
||||||
|
|
||||||
|
return self.component_commands.place_component(params)
|
||||||
|
|
||||||
def _handle_add_schematic_component(self, params):
|
def _handle_add_schematic_component(self, params):
|
||||||
"""Add a component to a schematic using text-based injection (no sexpdata)"""
|
"""Add a component to a schematic using text-based injection (no sexpdata)"""
|
||||||
logger.info("Adding component to schematic")
|
logger.info("Adding component to schematic")
|
||||||
@@ -602,9 +619,13 @@ class KiCADInterface:
|
|||||||
x = component.get("x", 0)
|
x = component.get("x", 0)
|
||||||
y = component.get("y", 0)
|
y = component.get("y", 0)
|
||||||
|
|
||||||
loader = DynamicSymbolLoader()
|
# Derive project path from schematic path for project-local library resolution
|
||||||
|
schematic_file = Path(schematic_path)
|
||||||
|
derived_project_path = schematic_file.parent
|
||||||
|
|
||||||
|
loader = DynamicSymbolLoader(project_path=derived_project_path)
|
||||||
loader.add_component(
|
loader.add_component(
|
||||||
Path(schematic_path),
|
schematic_file,
|
||||||
library,
|
library,
|
||||||
comp_type,
|
comp_type,
|
||||||
reference=reference,
|
reference=reference,
|
||||||
@@ -612,6 +633,7 @@ class KiCADInterface:
|
|||||||
footprint=footprint,
|
footprint=footprint,
|
||||||
x=x,
|
x=x,
|
||||||
y=y,
|
y=y,
|
||||||
|
project_path=derived_project_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
(kicad_sch (version 20240101) (generator "KiCAD-MCP-Server")
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
(uuid a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d)
|
(uuid a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
(kicad_sch (version 20240101) (generator "KiCAD-MCP-Server")
|
(kicad_sch (version 20250114) (generator "KiCAD-MCP-Server")
|
||||||
|
|
||||||
(uuid c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f)
|
(uuid c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f)
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ export function registerComponentTools(
|
|||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
.describe("Optional layer (e.g., 'F.Cu', 'B.SilkS')"),
|
||||||
|
boardPath: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Path to the .kicad_pcb file – required when using project-local footprint libraries"),
|
||||||
},
|
},
|
||||||
async ({
|
async ({
|
||||||
componentId,
|
componentId,
|
||||||
@@ -66,6 +70,7 @@ export function registerComponentTools(
|
|||||||
footprint,
|
footprint,
|
||||||
rotation,
|
rotation,
|
||||||
layer,
|
layer,
|
||||||
|
boardPath,
|
||||||
}) => {
|
}) => {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`,
|
`Placing component: ${componentId} at ${position.x},${position.y} ${position.unit}`,
|
||||||
@@ -78,6 +83,7 @@ export function registerComponentTools(
|
|||||||
footprint,
|
footprint,
|
||||||
rotation,
|
rotation,
|
||||||
layer,
|
layer,
|
||||||
|
boardPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -321,4 +321,60 @@ export function registerRoutingTools(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Route pad to pad tool
|
||||||
|
server.tool(
|
||||||
|
"route_pad_to_pad",
|
||||||
|
"Route a trace directly from one component pad to another without needing separate get_pad_position calls. Automatically looks up pad coordinates and uses the pad's net. Saves token usage compared to the 3-step get_pad_position + get_pad_position + route_trace sequence.",
|
||||||
|
{
|
||||||
|
fromRef: z.string().describe("Reference of the source component (e.g. 'U2')"),
|
||||||
|
fromPad: z.union([z.string(), z.number()]).describe("Pad number on the source component (e.g. '6' or 6)"),
|
||||||
|
toRef: z.string().describe("Reference of the target component (e.g. 'U1')"),
|
||||||
|
toPad: z.union([z.string(), z.number()]).describe("Pad number on the target component (e.g. '15' or 15)"),
|
||||||
|
layer: z.string().optional().describe("PCB layer (default: F.Cu)"),
|
||||||
|
width: z.number().optional().describe("Trace width in mm (default: board default)"),
|
||||||
|
net: z.string().optional().describe("Net name override (default: auto-detected from pad)"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("route_pad_to_pad", args);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy routing pattern tool
|
||||||
|
server.tool(
|
||||||
|
"copy_routing_pattern",
|
||||||
|
"Copy routing pattern (traces and vias) from a group of source components to a matching group of target components. The offset is calculated automatically from the position difference between the first source and first target component. Useful for replicating routing between identical circuit blocks.",
|
||||||
|
{
|
||||||
|
sourceRefs: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe("References of the source components (e.g. ['U1', 'R1', 'C1'])"),
|
||||||
|
targetRefs: z
|
||||||
|
.array(z.string())
|
||||||
|
.describe(
|
||||||
|
"References of the target components in same order as sourceRefs (e.g. ['U2', 'R2', 'C2'])",
|
||||||
|
),
|
||||||
|
includeVias: z
|
||||||
|
.boolean()
|
||||||
|
.optional()
|
||||||
|
.describe("Also copy vias (default: true)"),
|
||||||
|
traceWidth: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Override trace width in mm (default: keep original width)"),
|
||||||
|
},
|
||||||
|
async (args: any) => {
|
||||||
|
const result = await callKicadScript("copy_routing_pattern", args);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user