Merge pull request #61 from Mehanik/fix/tool-schema-descriptions

feat: add_schematic_wire & add_schematic_junction with pin snapping and polyline routing
This commit is contained in:
mixelpixx
2026-03-28 15:40:58 -04:00
committed by GitHub
7 changed files with 1584 additions and 561 deletions

View File

@@ -30,172 +30,6 @@ class ConnectionManager:
cls._pin_locator = PinLocator()
return cls._pin_locator
@staticmethod
def add_wire(
schematic_path: Path,
start_point: list,
end_point: list,
properties: dict = None,
):
"""
Add a wire between two points using WireManager
Args:
schematic_path: Path to .kicad_sch file
start_point: [x, y] coordinates for wire start
end_point: [x, y] coordinates for wire end
properties: Optional wire properties (stroke_width, stroke_type)
Returns:
True if successful, False otherwise
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager not available")
return False
stroke_width = properties.get("stroke_width", 0) if properties else 0
stroke_type = (
properties.get("stroke_type", "default") if properties else "default"
)
success = WireManager.add_wire(
schematic_path,
start_point,
end_point,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
return success
except Exception as e:
logger.error(f"Error adding wire: {e}")
return False
@staticmethod
def get_pin_location(symbol, pin_name: str):
"""
Get the absolute location of a pin on a symbol
Args:
symbol: Symbol object
pin_name: Name or number of the pin (e.g., "1", "GND", "VCC")
Returns:
[x, y] coordinates or None if pin not found
"""
try:
if not hasattr(symbol, "pin"):
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
return None
# Find the pin by name
target_pin = None
for pin in symbol.pin:
if pin.name == pin_name:
target_pin = pin
break
if not target_pin:
logger.warning(
f"Pin '{pin_name}' not found on {symbol.property.Reference.value}"
)
return None
# Get pin location relative to symbol
pin_loc = target_pin.location
# Get symbol location
symbol_at = symbol.at.value
# Calculate absolute position
# pin_loc is relative to symbol origin, need to add symbol position
abs_x = symbol_at[0] + pin_loc[0]
abs_y = symbol_at[1] + pin_loc[1]
return [abs_x, abs_y]
except Exception as e:
logger.error(f"Error getting pin location: {e}")
return None
@staticmethod
def add_connection(
schematic_path: Path,
source_ref: str,
source_pin: str,
target_ref: str,
target_pin: str,
routing: str = "direct",
):
"""
Add a wire connection between two component pins
Args:
schematic_path: Path to .kicad_sch file
source_ref: Reference designator of source component (e.g., "R1", "R1_")
source_pin: Pin name/number on source component
target_ref: Reference designator of target component (e.g., "C1", "C1_")
target_pin: Pin name/number on target component
routing: Routing style ('direct', 'orthogonal_h', 'orthogonal_v')
Returns:
True if connection was successful, False otherwise
"""
try:
if not WIRE_MANAGER_AVAILABLE:
logger.error("WireManager/PinLocator not available")
return False
locator = ConnectionManager.get_pin_locator()
if not locator:
logger.error("Pin locator unavailable")
return False
# Get pin locations
source_loc = locator.get_pin_location(
schematic_path, source_ref, source_pin
)
target_loc = locator.get_pin_location(
schematic_path, target_ref, target_pin
)
if not source_loc or not target_loc:
logger.error("Could not determine pin locations")
return False
# Create wire based on routing style
if routing == "direct":
# Simple direct wire
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
elif routing == "orthogonal_h":
# Orthogonal routing (horizontal first)
path = WireManager.create_orthogonal_path(
source_loc, target_loc, prefer_horizontal_first=True
)
success = WireManager.add_polyline_wire(schematic_path, path)
elif routing == "orthogonal_v":
# Orthogonal routing (vertical first)
path = WireManager.create_orthogonal_path(
source_loc, target_loc, prefer_horizontal_first=False
)
success = WireManager.add_polyline_wire(schematic_path, path)
else:
logger.error(f"Unknown routing style: {routing}")
return False
if success:
logger.info(
f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
)
return True
else:
return False
except Exception as e:
logger.error(f"Error adding connection: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_net_label(schematic: Schematic, net_name: str, position: list):
"""
@@ -257,15 +91,20 @@ class ConnectionManager:
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
# Stub direction follows the pin's outward angle from the PinLocator
pin_angle_deg = getattr(locator, '_last_pin_angle', 0)
pin_angle_deg = getattr(locator, "_last_pin_angle", 0)
try:
pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
pin_angle_deg = (
locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
)
except Exception:
pin_angle_deg = 0
import math as _math
angle_rad = _math.radians(pin_angle_deg)
stub_end = [round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4)]
stub_end = [
round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4),
]
# Create wire stub using WireManager
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
@@ -333,9 +172,15 @@ class ConnectionManager:
connected = []
failed = []
for pin_num in sorted(src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0):
for pin_num in sorted(
src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0
):
try:
net_name = f"{net_prefix}_{int(pin_num) + pin_offset}" if pin_num.isdigit() else f"{net_prefix}_{pin_num}"
net_name = (
f"{net_prefix}_{int(pin_num) + pin_offset}"
if pin_num.isdigit()
else f"{net_prefix}_{pin_num}"
)
ok_src = ConnectionManager.connect_to_net(
schematic_path, source_ref, pin_num, net_name
@@ -355,11 +200,15 @@ class ConnectionManager:
failed.append(f"{target_ref}/{pin_num} (pin not found)")
continue
connected.append(f"{source_ref}/{pin_num} <-> {target_ref}/{pin_num} [{net_name}]")
connected.append(
f"{source_ref}/{pin_num} <-> {target_ref}/{pin_num} [{net_name}]"
)
except Exception as e:
failed.append(f"{source_ref}/{pin_num}: {e}")
logger.info(f"connect_passthrough: {len(connected)} connected, {len(failed)} failed")
logger.info(
f"connect_passthrough: {len(connected)} connected, {len(failed)} failed"
)
return {"connected": connected, "failed": failed}
@staticmethod
@@ -607,35 +456,3 @@ class ConnectionManager:
except Exception as e:
logger.error(f"Error generating netlist: {e}")
return {"nets": [], "components": []}
if __name__ == "__main__":
# Example Usage (for testing)
from schematic import (
SchematicManager,
) # Assuming schematic.py is in the same directory
# Create a new schematic
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
# Add some wires
wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
# Note: add_connection, remove_connection, get_net_connections are placeholders
# and require more complex implementation based on kicad-skip's structure.
# Example of how you might add a net label (requires finding a point on a wire)
# from skip import Label
# if wire1:
# net_label_pos = wire1.start # Or calculate a point on the wire
# net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
# print(f"Added net label 'Net_01' at {net_label_pos}")
# Save the schematic (optional)
# SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
# Clean up (if saved)
# if os.path.exists("connection_test.kicad_sch"):
# os.remove("connection_test.kicad_sch")
# print("Cleaned up connection_test.kicad_sch")

View File

@@ -11,24 +11,29 @@ import logging
import math
import tempfile
from pathlib import Path
from typing import List, Tuple, Optional, Dict
from typing import List, Tuple, Optional
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger("kicad_interface")
logger = logging.getLogger('kicad_interface')
# Module-level Symbol constants — avoids repeated allocation on every call
_SYM_WIRE = Symbol('wire')
_SYM_PTS = Symbol('pts')
_SYM_XY = Symbol('xy')
_SYM_STROKE = Symbol('stroke')
_SYM_WIDTH = Symbol('width')
_SYM_TYPE = Symbol('type')
_SYM_UUID = Symbol('uuid')
_SYM_SHEET_INSTANCES = Symbol('sheet_instances')
class WireManager:
"""Manage wires in KiCad schematics using S-expression manipulation"""
@staticmethod
def add_wire(
schematic_path: Path,
start_point: List[float],
end_point: List[float],
stroke_width: float = 0,
stroke_type: str = "default",
) -> bool:
def add_wire(schematic_path: Path, start_point: List[float], end_point: List[float],
stroke_width: float = 0, stroke_type: str = 'default') -> bool:
"""
Add a wire to the schematic using S-expression manipulation
@@ -44,36 +49,25 @@ class WireManager:
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Break any existing wire that passes through a new endpoint (T-junction support)
for pt in (start_point, end_point):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new wire endpoint {pt}")
# Create wire S-expression
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
wire_sexp = [
Symbol("wire"),
[
Symbol("pts"),
[Symbol("xy"), start_point[0], start_point[1]],
[Symbol("xy"), end_point[0], end_point[1]],
],
[
Symbol("stroke"),
[Symbol("width"), stroke_width],
[Symbol("type"), Symbol(stroke_type)],
],
[Symbol("uuid"), str(uuid.uuid4())],
]
wire_sexp = WireManager._make_wire_sexp(start_point, end_point, stroke_width, stroke_type)
# Find insertion point (before sheet_instances)
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -86,7 +80,7 @@ class WireManager:
logger.info(f"Injected wire from {start_point} to {end_point}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
@@ -96,17 +90,12 @@ class WireManager:
except Exception as e:
logger.error(f"Error adding wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_polyline_wire(
schematic_path: Path,
points: List[List[float]],
stroke_width: float = 0,
stroke_type: str = "default",
) -> bool:
def add_polyline_wire(schematic_path: Path, points: List[List[float]],
stroke_width: float = 0, stroke_type: str = 'default') -> bool:
"""
Add a multi-segment wire (polyline) to the schematic
@@ -125,36 +114,28 @@ class WireManager:
return False
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create pts list
pts_list = [Symbol("pts")]
for point in points:
pts_list.append([Symbol("xy"), point[0], point[1]])
# Break any existing wire at the outer endpoints of the new path
for pt in (points[0], points[-1]):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new polyline endpoint {pt}")
# Create wire S-expression with multiple points
wire_sexp = [
Symbol("wire"),
pts_list,
[
Symbol("stroke"),
[Symbol("width"), stroke_width],
[Symbol("type"), Symbol(stroke_type)],
],
[Symbol("uuid"), str(uuid.uuid4())],
# KiCAD wire elements only support exactly 2 pts each.
# Split N waypoints into N-1 individual wire segments.
wire_sexps = [
WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type)
for i in range(len(points) - 1)
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -162,12 +143,13 @@ class WireManager:
logger.error("No sheet_instances section found in schematic")
return False
# Insert wire
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected polyline wire with {len(points)} points")
# Insert all segments (in reverse so order is preserved after inserts)
for wire_sexp in reversed(wire_sexps):
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
@@ -177,18 +159,12 @@ class WireManager:
except Exception as e:
logger.error(f"Error adding polyline wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_label(
schematic_path: Path,
text: str,
position: List[float],
label_type: str = "label",
orientation: int = 0,
) -> bool:
def add_label(schematic_path: Path, text: str, position: List[float],
label_type: str = 'label', orientation: int = 0) -> bool:
"""
Add a net label to the schematic
@@ -204,7 +180,7 @@ class WireManager:
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
@@ -214,24 +190,19 @@ class WireManager:
label_sexp = [
Symbol(label_type),
text,
[Symbol("at"), position[0], position[1], orientation],
[Symbol("fields_autoplaced"), Symbol("yes")],
[
Symbol("effects"),
[Symbol("font"), [Symbol("size"), 1.27, 1.27]],
[Symbol("justify"), Symbol("left"), Symbol("bottom")],
[Symbol('at'), position[0], position[1], orientation],
[Symbol('fields_autoplaced'), Symbol('yes')],
[Symbol('effects'),
[Symbol('font'), [Symbol('size'), 1.27, 1.27]],
[Symbol('justify'), Symbol('left'), Symbol('bottom')]
],
[Symbol("uuid"), str(uuid.uuid4())],
[Symbol('uuid'), str(uuid.uuid4())]
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -244,7 +215,7 @@ class WireManager:
logger.info(f"Injected label '{text}' at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
@@ -254,16 +225,115 @@ class WireManager:
except Exception as e:
logger.error(f"Error adding label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_junction(
schematic_path: Path, position: List[float], diameter: float = 0
) -> bool:
def _parse_wire(wire_item) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
"""
Add a junction (connection dot) to the schematic
Parse a wire S-expression item in a single pass.
Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire.
"""
if not (isinstance(wire_item, list) and len(wire_item) >= 2
and wire_item[0] == _SYM_WIRE):
return None
start = end = None
stroke_width: float = 0
stroke_type: str = 'default'
for part in wire_item[1:]:
if not isinstance(part, list) or not part:
continue
tag = part[0]
if tag == _SYM_PTS:
found: List[Tuple[float, float]] = []
for p in part[1:]:
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY:
found.append((float(p[1]), float(p[2])))
if len(found) == 2:
break
if len(found) == 2:
start, end = found[0], found[1]
elif tag == _SYM_STROKE:
for sp in part[1:]:
if isinstance(sp, list) and len(sp) >= 2:
if sp[0] == _SYM_WIDTH:
stroke_width = sp[1]
elif sp[0] == _SYM_TYPE:
stroke_type = str(sp[1])
if start is not None and end is not None:
return start, end, stroke_width, stroke_type
return None
@staticmethod
def _point_strictly_on_wire(px: float, py: float,
x1: float, y1: float,
x2: float, y2: float,
eps: float = 1e-6) -> bool:
"""
Return True if (px, py) lies strictly between (x1,y1) and (x2,y2)
on a horizontal or vertical wire segment (not at either endpoint).
"""
if abs(y1 - y2) < eps: # horizontal wire
if abs(py - y1) > eps:
return False
lo, hi = min(x1, x2), max(x1, x2)
return lo + eps < px < hi - eps
if abs(x1 - x2) < eps: # vertical wire
if abs(px - x1) > eps:
return False
lo, hi = min(y1, y2), max(y1, y2)
return lo + eps < py < hi - eps
return False
@staticmethod
def _make_wire_sexp(start: List[float], end: List[float],
stroke_width: float = 0, stroke_type: str = 'default') -> list:
return [
_SYM_WIRE,
[_SYM_PTS,
[_SYM_XY, start[0], start[1]],
[_SYM_XY, end[0], end[1]]],
[_SYM_STROKE,
[_SYM_WIDTH, stroke_width],
[_SYM_TYPE, Symbol(stroke_type)]],
[_SYM_UUID, str(uuid.uuid4())]
]
@staticmethod
def _break_wires_at_point(sch_data: list, position: List[float]) -> int:
"""
Split any wire segment that passes through *position* as a strict
midpoint (i.e. position is not an existing endpoint). Mirrors
KiCAD's SCH_LINE_WIRE_BUS_TOOL::BreakSegments behaviour.
Returns the number of wires split.
"""
px, py = float(position[0]), float(position[1])
splits = 0
i = 0
while i < len(sch_data):
parsed = WireManager._parse_wire(sch_data[i])
if parsed is not None:
(x1, y1), (x2, y2), stroke_width, stroke_type = parsed
if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2):
seg_a = WireManager._make_wire_sexp([x1, y1], [px, py], stroke_width, stroke_type)
seg_b = WireManager._make_wire_sexp([px, py], [x2, y2], stroke_width, stroke_type)
sch_data[i:i + 1] = [seg_a, seg_b]
logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})")
splits += 1
i += 2 # skip the two new segments
continue
i += 1
return splits
@staticmethod
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
"""
Add a junction (connection dot) to the schematic.
Mirrors KiCAD's AddJunction behaviour: any wire whose interior passes
through *position* is split into two segments at that point so that
the BFS-based get_wire_connections tool can traverse the T/X branch.
Args:
schematic_path: Path to .kicad_sch file
@@ -275,29 +345,31 @@ class WireManager:
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Split any wire that passes through the junction as a midpoint
# (mirrors KiCAD's AddJunction / BreakSegments behaviour)
splits = WireManager._break_wires_at_point(sch_data, position)
if splits:
logger.info(f"Broke {splits} wire(s) at junction position {position}")
# Create junction S-expression
# Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...))
junction_sexp = [
Symbol("junction"),
[Symbol("at"), position[0], position[1]],
[Symbol("diameter"), diameter],
[Symbol("color"), 0, 0, 0, 0],
[Symbol("uuid"), str(uuid.uuid4())],
Symbol('junction'),
[Symbol('at'), position[0], position[1]],
[Symbol('diameter'), diameter],
[Symbol('color'), 0, 0, 0, 0],
[Symbol('uuid'), str(uuid.uuid4())]
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -310,7 +382,7 @@ class WireManager:
logger.info(f"Injected junction at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
@@ -320,7 +392,6 @@ class WireManager:
except Exception as e:
logger.error(f"Error adding junction: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@@ -338,7 +409,7 @@ class WireManager:
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
@@ -346,19 +417,15 @@ class WireManager:
# Create no_connect S-expression
# Format: (no_connect (at x y) (uuid ...))
no_connect_sexp = [
Symbol("no_connect"),
[Symbol("at"), position[0], position[1]],
[Symbol("uuid"), str(uuid.uuid4())],
Symbol('no_connect'),
[Symbol('at'), position[0], position[1]],
[Symbol('uuid'), str(uuid.uuid4())]
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("sheet_instances")
):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
@@ -371,7 +438,7 @@ class WireManager:
logger.info(f"Injected no-connect at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
@@ -381,17 +448,12 @@ class WireManager:
except Exception as e:
logger.error(f"Error adding no-connect: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def delete_wire(
schematic_path: Path,
start_point: List[float],
end_point: List[float],
tolerance: float = 0.5,
) -> bool:
def delete_wire(schematic_path: Path, start_point: List[float], end_point: List[float],
tolerance: float = 0.5) -> bool:
"""
Delete a wire from the schematic matching given start/end coordinates.
@@ -405,7 +467,7 @@ class WireManager:
True if a wire was found and removed, False otherwise
"""
try:
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
@@ -414,54 +476,34 @@ class WireManager:
ex, ey = end_point
for i, item in enumerate(sch_data):
if not (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("wire")
):
if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol('wire')):
continue
# Extract pts from the wire s-expression
pts_list = None
for part in item[1:]:
if (
isinstance(part, list)
and len(part) > 0
and part[0] == Symbol("pts")
):
if isinstance(part, list) and len(part) > 0 and part[0] == Symbol('pts'):
pts_list = part
break
if pts_list is None:
continue
xy_points = [
p
for p in pts_list[1:]
if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol("xy")
]
xy_points = [p for p in pts_list[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol('xy')]
if len(xy_points) < 2:
continue
x1, y1 = float(xy_points[0][1]), float(xy_points[0][2])
x2, y2 = float(xy_points[-1][1]), float(xy_points[-1][2])
match_fwd = (
abs(x1 - sx) < tolerance
and abs(y1 - sy) < tolerance
and abs(x2 - ex) < tolerance
and abs(y2 - ey) < tolerance
)
match_rev = (
abs(x1 - ex) < tolerance
and abs(y1 - ey) < tolerance
and abs(x2 - sx) < tolerance
and abs(y2 - sy) < tolerance
)
match_fwd = (abs(x1 - sx) < tolerance and abs(y1 - sy) < tolerance and
abs(x2 - ex) < tolerance and abs(y2 - ey) < tolerance)
match_rev = (abs(x1 - ex) < tolerance and abs(y1 - ey) < tolerance and
abs(x2 - sx) < tolerance and abs(y2 - sy) < tolerance)
if match_fwd or match_rev:
del sch_data[i]
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted wire from {start_point} to {end_point}")
return True
@@ -472,17 +514,13 @@ class WireManager:
except Exception as e:
logger.error(f"Error deleting wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def delete_label(
schematic_path: Path,
net_name: str,
position: Optional[List[float]] = None,
tolerance: float = 0.5,
) -> bool:
def delete_label(schematic_path: Path, net_name: str,
position: Optional[List[float]] = None,
tolerance: float = 0.5) -> bool:
"""
Delete a net label from the schematic by name (and optionally position).
@@ -496,17 +534,14 @@ class WireManager:
True if a label was found and removed, False otherwise
"""
try:
with open(schematic_path, "r", encoding="utf-8") as f:
with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
for i, item in enumerate(sch_data):
if not (
isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("label")
):
if not (isinstance(item, list) and len(item) > 0
and item[0] == Symbol('label')):
continue
# Second element is the label text
@@ -516,26 +551,19 @@ class WireManager:
if position is not None:
# Find (at x y ...) sub-expression and check coordinates
at_entry = next(
(
p
for p in item[1:]
if isinstance(p, list)
and len(p) >= 3
and p[0] == Symbol("at")
),
(p for p in item[1:] if isinstance(p, list)
and len(p) >= 3 and p[0] == Symbol('at')),
None,
)
if at_entry is None:
continue
lx, ly = float(at_entry[1]), float(at_entry[2])
if not (
abs(lx - position[0]) < tolerance
and abs(ly - position[1]) < tolerance
):
if not (abs(lx - position[0]) < tolerance
and abs(ly - position[1]) < tolerance):
continue
del sch_data[i]
with open(schematic_path, "w", encoding="utf-8") as f:
with open(schematic_path, 'w', encoding='utf-8') as f:
f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted label '{net_name}'")
return True
@@ -546,14 +574,12 @@ class WireManager:
except Exception as e:
logger.error(f"Error deleting label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def create_orthogonal_path(
start: List[float], end: List[float], prefer_horizontal_first: bool = True
) -> List[List[float]]:
def create_orthogonal_path(start: List[float], end: List[float],
prefer_horizontal_first: bool = True) -> List[List[float]]:
"""
Create an orthogonal (right-angle) path between two points
@@ -582,11 +608,10 @@ class WireManager:
return [start, corner, end]
if __name__ == "__main__":
if __name__ == '__main__':
# Test wire creation
import sys
sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python")
sys.path.insert(0, '/home/chris/MCP/KiCAD-MCP-Server/python')
from pathlib import Path
import shutil
@@ -596,10 +621,8 @@ if __name__ == "__main__":
print("=" * 80)
# Create test schematic (cross-platform temp directory)
test_path = Path(tempfile.gettempdir()) / "test_wire_manager.kicad_sch"
template_path = Path(
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch"
)
test_path = Path(tempfile.gettempdir()) / 'test_wire_manager.kicad_sch'
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch')
shutil.copy(template_path, test_path)
print(f"\n✓ Created test schematic: {test_path}")
@@ -635,9 +658,8 @@ if __name__ == "__main__":
print("\n[Verification] Loading with kicad-skip...")
try:
from skip import Schematic
sch = Schematic(str(test_path))
wire_count = len(list(sch.wire)) if hasattr(sch, "wire") else 0
wire_count = len(list(sch.wire)) if hasattr(sch, 'wire') else 0
print(f" ✓ Loaded successfully")
print(f" ✓ Wire count: {wire_count}")
except Exception as e:

View File

@@ -376,8 +376,8 @@ class KiCADInterface:
"edit_schematic_component": self._handle_edit_schematic_component,
"get_schematic_component": self._handle_get_schematic_component,
"add_schematic_wire": self._handle_add_schematic_wire,
"add_schematic_connection": self._handle_add_schematic_connection,
"add_schematic_net_label": self._handle_add_schematic_net_label,
"add_schematic_junction": self._handle_add_schematic_junction,
"connect_to_net": self._handle_connect_to_net,
"connect_passthrough": self._handle_connect_passthrough,
"get_schematic_pin_locations": self._handle_get_schematic_pin_locations,
@@ -1138,40 +1138,117 @@ class KiCADInterface:
return {"success": False, "message": str(e)}
def _handle_add_schematic_wire(self, params):
"""Add a wire to a schematic using WireManager"""
"""Add a wire to a schematic using WireManager, with optional pin snapping"""
logger.info("Adding wire to schematic")
try:
from pathlib import Path
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
start_point = params.get("startPoint")
end_point = params.get("endPoint")
points = params.get("waypoints")
properties = params.get("properties", {})
snap_to_pins = params.get("snapToPins", True)
snap_tolerance = params.get("snapTolerance", 1.0)
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not start_point or not end_point:
if not points or len(points) < 2:
return {
"success": False,
"message": "Start and end points are required",
"message": "At least 2 waypoints are required",
}
# Make a mutable copy of points
points = [list(p) for p in points]
# Pin snapping: adjust first and last endpoints to nearest pin
snapped_info = []
if snap_to_pins:
from commands.pin_locator import PinLocator
locator = PinLocator()
sch_path = Path(schematic_path)
# Load schematic to iterate all symbols
from skip import Schematic as SkipSchematic
sch = SkipSchematic(str(sch_path))
# Collect all pin locations: list of (ref, pin_num, [x, y])
all_pins = []
for symbol in sch.symbol:
if not hasattr(symbol.property, "Reference"):
continue
ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"):
continue
pin_locs = locator.get_all_symbol_pins(sch_path, ref)
for pin_num, coords in pin_locs.items():
all_pins.append((ref, pin_num, coords))
def find_nearest_pin(point, tolerance):
"""Find the nearest pin within tolerance of a point."""
best = None
best_dist = tolerance
for ref, pin_num, coords in all_pins:
dx = point[0] - coords[0]
dy = point[1] - coords[1]
dist = (dx * dx + dy * dy) ** 0.5
if dist <= best_dist:
best_dist = dist
best = (ref, pin_num, coords)
return best
# Snap first endpoint
match = find_nearest_pin(points[0], snap_tolerance)
if match:
ref, pin_num, coords = match
logger.info(
f"Snapped start point {points[0]} -> {coords} (pin {ref}/{pin_num})"
)
snapped_info.append(
f"start snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
)
points[0] = list(coords)
# Snap last endpoint
match = find_nearest_pin(points[-1], snap_tolerance)
if match:
ref, pin_num, coords = match
logger.info(
f"Snapped end point {points[-1]} -> {coords} (pin {ref}/{pin_num})"
)
snapped_info.append(
f"end snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
)
points[-1] = list(coords)
# Extract wire properties
stroke_width = properties.get("stroke_width", 0)
stroke_type = properties.get("stroke_type", "default")
# Use WireManager for S-expression manipulation
success = WireManager.add_wire(
Path(schematic_path),
start_point,
end_point,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
if len(points) == 2:
success = WireManager.add_wire(
Path(schematic_path),
points[0],
points[1],
stroke_width=stroke_width,
stroke_type=stroke_type,
)
else:
success = WireManager.add_polyline_wire(
Path(schematic_path),
points,
stroke_width=stroke_width,
stroke_type=stroke_type,
)
if success:
return {"success": True, "message": "Wire added successfully"}
message = "Wire added successfully"
if snapped_info:
message += "; " + "; ".join(snapped_info)
return {"success": True, "message": message}
else:
return {"success": False, "message": "Failed to add wire"}
except Exception as e:
@@ -1185,6 +1262,38 @@ class KiCADInterface:
"errorDetails": traceback.format_exc(),
}
def _handle_add_schematic_junction(self, params):
"""Add a junction (connection dot) to a schematic using WireManager"""
logger.info("Adding junction to schematic")
try:
from pathlib import Path
from commands.wire_manager import WireManager
schematic_path = params.get("schematicPath")
position = params.get("position")
if not schematic_path:
return {"success": False, "message": "Schematic path is required"}
if not position:
return {"success": False, "message": "Position is required"}
success = WireManager.add_junction(Path(schematic_path), position)
if success:
return {"success": True, "message": "Junction added successfully"}
else:
return {"success": False, "message": "Failed to add junction"}
except Exception as e:
logger.error(f"Error adding junction to schematic: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"message": str(e),
"errorDetails": traceback.format_exc(),
}
def _handle_list_schematic_libraries(self, params):
"""List available symbol libraries"""
logger.info("Listing schematic libraries")
@@ -1394,54 +1503,6 @@ class KiCADInterface:
logger.error(f"Error exporting schematic to PDF: {str(e)}")
return {"success": False, "message": str(e)}
def _handle_add_schematic_connection(self, params):
"""Add a pin-to-pin connection in schematic with automatic pin discovery and routing"""
logger.info("Adding pin-to-pin connection in schematic")
try:
from pathlib import Path
schematic_path = params.get("schematicPath")
source_ref = params.get("sourceRef")
source_pin = params.get("sourcePin")
target_ref = params.get("targetRef")
target_pin = params.get("targetPin")
routing = params.get(
"routing", "direct"
) # 'direct', 'orthogonal_h', 'orthogonal_v'
if not all(
[schematic_path, source_ref, source_pin, target_ref, target_pin]
):
return {"success": False, "message": "Missing required parameters"}
# Use ConnectionManager with new PinLocator and WireManager integration
success = ConnectionManager.add_connection(
Path(schematic_path),
source_ref,
source_pin,
target_ref,
target_pin,
routing=routing,
)
if success:
return {
"success": True,
"message": f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})",
}
else:
return {"success": False, "message": "Failed to add connection"}
except Exception as e:
logger.error(f"Error adding schematic connection: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return {
"success": False,
"message": str(e),
"errorDetails": traceback.format_exc(),
}
def _handle_add_schematic_net_label(self, params):
"""Add a net label to schematic using WireManager"""
logger.info("Adding net label to schematic")

View File

@@ -1362,30 +1362,8 @@ SCHEMATIC_TOOLS = [
},
{
"name": "add_schematic_wire",
"title": "Connect Components",
"description": "Draws a wire connection between component pins on the schematic.",
"inputSchema": {
"type": "object",
"properties": {
"points": {
"type": "array",
"description": "Array of [x, y] waypoints for the wire",
"items": {
"type": "array",
"items": {"type": "number"},
"minItems": 2,
"maxItems": 2,
},
"minItems": 2,
}
},
"required": ["points"],
},
},
{
"name": "add_schematic_connection",
"title": "Add Junction/Connection Point",
"description": "Adds a junction (connection point) at the specified location on the schematic where wires cross and should connect.",
"title": "Draw Wire Between Pins",
"description": "Draws a wire on the schematic between two or more coordinate points. Always call get_schematic_pin_locations first to get the approximate pin coordinates, then pass them as the first and last waypoints. snapToPins (on by default) will correct any float imprecision by snapping endpoints to the exact nearest pin coordinate. To route around components, add intermediate waypoints between the start and end: e.g. [[x1,y1], [xMid,y1], [xMid,y2], [x2,y2]] routes horizontally then vertically. Intermediate waypoints are never snapped.",
"inputSchema": {
"type": "object",
"properties": {
@@ -1393,10 +1371,29 @@ SCHEMATIC_TOOLS = [
"type": "string",
"description": "Path to schematic file",
},
"x": {"type": "number", "description": "X coordinate on schematic"},
"y": {"type": "number", "description": "Y coordinate on schematic"},
"waypoints": {
"type": "array",
"description": "Array of [x, y] coordinates defining the wire path. First and last points are the pin locations (from get_schematic_pin_locations). Add intermediate points to route around obstacles.",
"items": {
"type": "array",
"items": {"type": "number"},
"minItems": 2,
"maxItems": 2,
},
"minItems": 2,
},
"snapToPins": {
"type": "boolean",
"description": "When true, the first and last waypoints are snapped to the nearest schematic pin within snapTolerance mm. Intermediate waypoints are left unchanged. Enabled by default to correct float coordinate imprecision.",
"default": True,
},
"snapTolerance": {
"type": "number",
"description": "Maximum distance in mm to search for a nearby pin when snapToPins is enabled.",
"default": 1.0,
},
},
"required": ["schematicPath", "x", "y"],
"required": ["schematicPath", "waypoints"],
},
},
{
@@ -1633,6 +1630,28 @@ SCHEMATIC_TOOLS = [
"required": ["schematicPath", "outputPath"],
},
},
{
"name": "add_schematic_junction",
"title": "Add Junction to Schematic",
"description": "Adds a junction (connection dot) at the specified coordinates on the schematic. Junctions are required in KiCAD to mark intentional connections where wires cross or where a wire branches off another wire. Without a junction, crossing wires are not electrically connected.",
"inputSchema": {
"type": "object",
"properties": {
"schematicPath": {
"type": "string",
"description": "Path to schematic file",
},
"position": {
"type": "array",
"description": "The [x, y] coordinates where the junction should be placed. Must be on an existing wire intersection or branch point.",
"items": {"type": "number"},
"minItems": 2,
"maxItems": 2,
},
},
"required": ["schematicPath", "position"],
},
},
# --- Schematic Analysis Tools (read-only) ---
{
"name": "get_schematic_view_region",

File diff suppressed because it is too large Load Diff

View File

@@ -91,9 +91,9 @@ export const toolCategories: ToolCategory[] = [
"move_schematic_component",
"rotate_schematic_component",
"annotate_schematic",
"add_wire",
"add_schematic_wire",
"delete_schematic_wire",
"add_schematic_connection",
"add_schematic_junction",
"add_schematic_net_label",
"delete_schematic_net_label",
"connect_to_net",

View File

@@ -43,7 +43,10 @@ export function registerSchematicTools(
),
reference: z.string().describe("Component reference (e.g., R1, U1)"),
value: z.string().optional().describe("Component value"),
footprint: z.string().optional().describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
footprint: z
.string()
.optional()
.describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
position: z
.object({
x: z.number(),
@@ -119,7 +122,9 @@ To remove a footprint from a PCB, use delete_component instead.`,
schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z
.string()
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
.describe(
"Reference designator of the component to remove (e.g. R1, U3)",
),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("delete_schematic_component", args);
@@ -233,66 +238,40 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
},
);
// Connect components with wire
// Draw wire between coordinate waypoints with optional pin snapping
server.tool(
"add_wire",
"Add a wire connection in the schematic",
"add_schematic_wire",
"Draws a wire on the schematic between two or more coordinate points. Always call get_schematic_pin_locations first to get the approximate pin coordinates, then pass them as the first and last waypoints. snapToPins (on by default) will correct any float imprecision by snapping endpoints to the exact nearest pin coordinate. To route around components, add intermediate waypoints between the start and end: e.g. [[x1,y1], [xMid,y1], [xMid,y2], [x2,y2]] routes horizontally then vertically. Intermediate waypoints are never snapped.",
{
start: z
.object({
x: z.number(),
y: z.number(),
})
.describe("Start position"),
end: z
.object({
x: z.number(),
y: z.number(),
})
.describe("End position"),
},
async (args: any) => {
const result = await callKicadScript("add_wire", args);
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
},
);
// Add pin-to-pin connection
server.tool(
"add_schematic_connection",
"Connect two component pins with a wire. Use this for individual connections between components with different pin roles (e.g. U1.SDA → J3.2). WARNING: Do NOT use this in a loop to wire N passthrough pins — use connect_passthrough instead (single call, cleaner layout, far fewer tokens).",
{
schematicPath: z.string().describe("Path to the schematic file"),
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
sourcePin: z
.string()
.describe("Source pin name/number (e.g., 1, 2, GND)"),
targetRef: z.string().describe("Target component reference (e.g., C1)"),
targetPin: z
.string()
.describe("Target pin name/number (e.g., 1, 2, VCC)"),
schematicPath: z.string().describe("Path to the .kicad_sch file"),
waypoints: z
.array(z.array(z.number()).length(2))
.min(2)
.describe("Ordered list of [x, y] coordinates. Minimum 2 points."),
snapToPins: z
.boolean()
.optional()
.describe(
"Snap the first and last waypoints to the nearest pin (default: true)",
),
snapTolerance: z
.number()
.optional()
.describe("Maximum snap distance in mm (default: 1.0)"),
},
async (args: {
schematicPath: string;
sourceRef: string;
sourcePin: string;
targetRef: string;
targetPin: string;
waypoints: number[][];
snapToPins?: boolean;
snapTolerance?: number;
}) => {
const result = await callKicadScript("add_schematic_connection", args);
const result = await callKicadScript("add_schematic_wire", args);
if (result.success) {
return {
content: [
{
type: "text",
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`,
type: "text" as const,
text: result.message || "Wire added successfully",
},
],
};
@@ -300,8 +279,43 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
return {
content: [
{
type: "text",
text: `Failed to add connection: ${result.message || "Unknown error"}`,
type: "text" as const,
text: `Failed to add wire: ${result.message || "Unknown error"}`,
},
],
};
}
},
);
// Add junction dot at a T/X intersection
server.tool(
"add_schematic_junction",
"Place a junction dot at a wire intersection in the schematic. Required at T-branch and X-cross points so KiCAD recognises the electrical connection.",
{
schematicPath: z.string().describe("Path to the .kicad_sch file"),
position: z
.array(z.number())
.length(2)
.describe("Junction position [x, y] in mm"),
},
async (args: { schematicPath: string; position: number[] }) => {
const result = await callKicadScript("add_schematic_junction", args);
if (result.success) {
return {
content: [
{
type: "text" as const,
text: result.message || "Junction added successfully",
},
],
};
} else {
return {
content: [
{
type: "text" as const,
text: `Failed to add junction: ${result.message || "Unknown error"}`,
},
],
};
@@ -470,27 +484,33 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.",
{
schematicPath: z.string().describe("Path to the schematic file"),
reference: z.string().describe("Component reference designator (e.g. U1, R1, J2)"),
reference: z
.string()
.describe("Component reference designator (e.g. U1, R1, J2)"),
},
async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("get_schematic_pin_locations", args);
if (result.success && result.pins) {
const lines = Object.entries(result.pins as Record<string, any>).map(
([pinNum, data]: [string, any]) =>
` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°`
` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°`,
);
return {
content: [{
type: "text",
text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`,
}],
content: [
{
type: "text",
text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`,
},
],
};
} else {
return {
content: [{
type: "text",
text: `Failed to get pin locations: ${result.message || "Unknown error"}`,
}],
content: [
{
type: "text",
text: `Failed to get pin locations: ${result.message || "Unknown error"}`,
},
],
};
}
},
@@ -504,21 +524,49 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
schematicPath: z.string().describe("Path to the schematic file"),
sourceRef: z.string().describe("Source connector reference (e.g. J1)"),
targetRef: z.string().describe("Target connector reference (e.g. J2)"),
netPrefix: z.string().optional().describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"),
pinOffset: z.number().optional().describe("Add to pin number when building net name (default: 0)"),
netPrefix: z
.string()
.optional()
.describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"),
pinOffset: z
.number()
.optional()
.describe("Add to pin number when building net name (default: 0)"),
},
async (args: { schematicPath: string; sourceRef: string; targetRef: string; netPrefix?: string; pinOffset?: number }) => {
async (args: {
schematicPath: string;
sourceRef: string;
targetRef: string;
netPrefix?: string;
pinOffset?: number;
}) => {
const result = await callKicadScript("connect_passthrough", args);
if (result.success !== false || (result.connected && result.connected.length > 0)) {
if (
result.success !== false ||
(result.connected && result.connected.length > 0)
) {
const lines: string[] = [];
if (result.connected?.length) lines.push(`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`);
if (result.failed?.length) lines.push(`Failed (${result.failed.length}): ${result.failed.join(", ")}`);
if (result.connected?.length)
lines.push(
`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`,
);
if (result.failed?.length)
lines.push(
`Failed (${result.failed.length}): ${result.failed.join(", ")}`,
);
return {
content: [{ type: "text", text: result.message + "\n" + lines.join("\n") }],
content: [
{ type: "text", text: result.message + "\n" + lines.join("\n") },
],
};
} else {
return {
content: [{ type: "text", text: `Passthrough failed: ${result.message || "Unknown error"}` }],
content: [
{
type: "text",
text: `Passthrough failed: ${result.message || "Unknown error"}`,
},
],
};
}
},
@@ -1038,21 +1086,30 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"run_erc",
"Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.",
{
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
schematicPath: z
.string()
.describe("Path to the .kicad_sch schematic file"),
},
async (args: { schematicPath: string }) => {
const result = await callKicadScript("run_erc", args);
if (result.success) {
const violations: any[] = result.violations || [];
const lines: string[] = [`ERC result: ${violations.length} violation(s)`];
const lines: string[] = [
`ERC result: ${violations.length} violation(s)`,
];
if (result.summary?.by_severity) {
const s = result.summary.by_severity;
lines.push(` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`);
lines.push(
` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`,
);
}
if (violations.length > 0) {
lines.push("");
violations.slice(0, 30).forEach((v: any, i: number) => {
const loc = v.location && (v.location.x !== undefined) ? ` @ (${v.location.x}, ${v.location.y})` : "";
const loc =
v.location && v.location.x !== undefined
? ` @ (${v.location.x}, ${v.location.y})`
: "";
lines.push(`${i + 1}. [${v.severity}] ${v.message}${loc}`);
});
if (violations.length > 30) {
@@ -1062,7 +1119,12 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
return { content: [{ type: "text", text: lines.join("\n") }] };
} else {
return {
content: [{ type: "text", text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}` }],
content: [
{
type: "text",
text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}`,
},
],
};
}
},
@@ -1121,8 +1183,12 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"sync_schematic_to_board",
"Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.",
{
schematicPath: z.string().describe("Absolute path to the .kicad_sch schematic file"),
boardPath: z.string().describe("Absolute path to the .kicad_pcb board file"),
schematicPath: z
.string()
.describe("Absolute path to the .kicad_sch schematic file"),
boardPath: z
.string()
.describe("Absolute path to the .kicad_pcb board file"),
},
async (args: { schematicPath: string; boardPath: string }) => {
const result = await callKicadScript("sync_schematic_to_board", args);