style: apply Black and Prettier formatting to PR-changed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-28 13:11:17 +00:00
parent a76c5bc74b
commit a11dd5fac9
3 changed files with 687 additions and 467 deletions

View File

@@ -15,25 +15,30 @@ from typing import List, Tuple, Optional
import sexpdata import sexpdata
from sexpdata import Symbol from sexpdata import Symbol
logger = logging.getLogger('kicad_interface') logger = logging.getLogger("kicad_interface")
# Module-level Symbol constants — avoids repeated allocation on every call # Module-level Symbol constants — avoids repeated allocation on every call
_SYM_WIRE = Symbol('wire') _SYM_WIRE = Symbol("wire")
_SYM_PTS = Symbol('pts') _SYM_PTS = Symbol("pts")
_SYM_XY = Symbol('xy') _SYM_XY = Symbol("xy")
_SYM_STROKE = Symbol('stroke') _SYM_STROKE = Symbol("stroke")
_SYM_WIDTH = Symbol('width') _SYM_WIDTH = Symbol("width")
_SYM_TYPE = Symbol('type') _SYM_TYPE = Symbol("type")
_SYM_UUID = Symbol('uuid') _SYM_UUID = Symbol("uuid")
_SYM_SHEET_INSTANCES = Symbol('sheet_instances') _SYM_SHEET_INSTANCES = Symbol("sheet_instances")
class WireManager: class WireManager:
"""Manage wires in KiCad schematics using S-expression manipulation""" """Manage wires in KiCad schematics using S-expression manipulation"""
@staticmethod @staticmethod
def add_wire(schematic_path: Path, start_point: List[float], end_point: List[float], def add_wire(
stroke_width: float = 0, stroke_type: str = 'default') -> bool: 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 Add a wire to the schematic using S-expression manipulation
@@ -49,7 +54,7 @@ class WireManager:
""" """
try: try:
# Read schematic # 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -62,12 +67,18 @@ class WireManager:
# Create wire S-expression # Create wire S-expression
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...)) # Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
wire_sexp = WireManager._make_wire_sexp(start_point, end_point, stroke_width, stroke_type) wire_sexp = WireManager._make_wire_sexp(
start_point, end_point, stroke_width, stroke_type
)
# Find insertion point (before sheet_instances) # Find insertion point (before sheet_instances)
sheet_instances_index = None sheet_instances_index = None
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: if (
isinstance(item, list)
and len(item) > 0
and item[0] == _SYM_SHEET_INSTANCES
):
sheet_instances_index = i sheet_instances_index = i
break break
@@ -80,7 +91,7 @@ class WireManager:
logger.info(f"Injected wire from {start_point} to {end_point}") logger.info(f"Injected wire from {start_point} to {end_point}")
# Write back # 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) output = sexpdata.dumps(sch_data)
f.write(output) f.write(output)
@@ -90,12 +101,17 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error adding wire: {e}") logger.error(f"Error adding wire: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def add_polyline_wire(schematic_path: Path, points: List[List[float]], def add_polyline_wire(
stroke_width: float = 0, stroke_type: str = 'default') -> bool: 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 Add a multi-segment wire (polyline) to the schematic
@@ -114,7 +130,7 @@ class WireManager:
return False return False
# Read schematic # 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -128,14 +144,20 @@ class WireManager:
# KiCAD wire elements only support exactly 2 pts each. # KiCAD wire elements only support exactly 2 pts each.
# Split N waypoints into N-1 individual wire segments. # Split N waypoints into N-1 individual wire segments.
wire_sexps = [ wire_sexps = [
WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type) WireManager._make_wire_sexp(
points[i], points[i + 1], stroke_width, stroke_type
)
for i in range(len(points) - 1) for i in range(len(points) - 1)
] ]
# Find insertion point # Find insertion point
sheet_instances_index = None sheet_instances_index = None
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: if (
isinstance(item, list)
and len(item) > 0
and item[0] == _SYM_SHEET_INSTANCES
):
sheet_instances_index = i sheet_instances_index = i
break break
@@ -146,10 +168,12 @@ class WireManager:
# Insert all segments (in reverse so order is preserved after inserts) # Insert all segments (in reverse so order is preserved after inserts)
for wire_sexp in reversed(wire_sexps): for wire_sexp in reversed(wire_sexps):
sch_data.insert(sheet_instances_index, wire_sexp) sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline") logger.info(
f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline"
)
# Write back # 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) output = sexpdata.dumps(sch_data)
f.write(output) f.write(output)
@@ -159,12 +183,18 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error adding polyline wire: {e}") logger.error(f"Error adding polyline wire: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def add_label(schematic_path: Path, text: str, position: List[float], def add_label(
label_type: str = 'label', orientation: int = 0) -> bool: schematic_path: Path,
text: str,
position: List[float],
label_type: str = "label",
orientation: int = 0,
) -> bool:
""" """
Add a net label to the schematic Add a net label to the schematic
@@ -180,7 +210,7 @@ class WireManager:
""" """
try: try:
# Read schematic # 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -190,19 +220,24 @@ class WireManager:
label_sexp = [ label_sexp = [
Symbol(label_type), Symbol(label_type),
text, text,
[Symbol('at'), position[0], position[1], orientation], [Symbol("at"), position[0], position[1], orientation],
[Symbol('fields_autoplaced'), Symbol('yes')], [Symbol("fields_autoplaced"), Symbol("yes")],
[Symbol('effects'), [
[Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol("effects"),
[Symbol('justify'), Symbol('left'), Symbol('bottom')] [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 # Find insertion point
sheet_instances_index = None sheet_instances_index = None
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: if (
isinstance(item, list)
and len(item) > 0
and item[0] == _SYM_SHEET_INSTANCES
):
sheet_instances_index = i sheet_instances_index = i
break break
@@ -215,7 +250,7 @@ class WireManager:
logger.info(f"Injected label '{text}' at {position}") logger.info(f"Injected label '{text}' at {position}")
# Write back # 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) output = sexpdata.dumps(sch_data)
f.write(output) f.write(output)
@@ -225,21 +260,27 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error adding label: {e}") logger.error(f"Error adding label: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def _parse_wire(wire_item) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]: def _parse_wire(
wire_item,
) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
""" """
Parse a wire S-expression item in a single pass. 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. 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 if not (
and wire_item[0] == _SYM_WIRE): isinstance(wire_item, list)
and len(wire_item) >= 2
and wire_item[0] == _SYM_WIRE
):
return None return None
start = end = None start = end = None
stroke_width: float = 0 stroke_width: float = 0
stroke_type: str = 'default' stroke_type: str = "default"
for part in wire_item[1:]: for part in wire_item[1:]:
if not isinstance(part, list) or not part: if not isinstance(part, list) or not part:
continue continue
@@ -265,10 +306,15 @@ class WireManager:
return None return None
@staticmethod @staticmethod
def _point_strictly_on_wire(px: float, py: float, def _point_strictly_on_wire(
x1: float, y1: float, px: float,
x2: float, y2: float, py: float,
eps: float = 1e-6) -> bool: 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) Return True if (px, py) lies strictly between (x1,y1) and (x2,y2)
on a horizontal or vertical wire segment (not at either endpoint). on a horizontal or vertical wire segment (not at either endpoint).
@@ -286,17 +332,17 @@ class WireManager:
return False return False
@staticmethod @staticmethod
def _make_wire_sexp(start: List[float], end: List[float], def _make_wire_sexp(
stroke_width: float = 0, stroke_type: str = 'default') -> list: start: List[float],
end: List[float],
stroke_width: float = 0,
stroke_type: str = "default",
) -> list:
return [ return [
_SYM_WIRE, _SYM_WIRE,
[_SYM_PTS, [_SYM_PTS, [_SYM_XY, start[0], start[1]], [_SYM_XY, end[0], end[1]]],
[_SYM_XY, start[0], start[1]], [_SYM_STROKE, [_SYM_WIDTH, stroke_width], [_SYM_TYPE, Symbol(stroke_type)]],
[_SYM_XY, end[0], end[1]]], [_SYM_UUID, str(uuid.uuid4())],
[_SYM_STROKE,
[_SYM_WIDTH, stroke_width],
[_SYM_TYPE, Symbol(stroke_type)]],
[_SYM_UUID, str(uuid.uuid4())]
] ]
@staticmethod @staticmethod
@@ -316,9 +362,13 @@ class WireManager:
if parsed is not None: if parsed is not None:
(x1, y1), (x2, y2), stroke_width, stroke_type = parsed (x1, y1), (x2, y2), stroke_width, stroke_type = parsed
if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2): 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_a = WireManager._make_wire_sexp(
seg_b = WireManager._make_wire_sexp([px, py], [x2, y2], stroke_width, stroke_type) [x1, y1], [px, py], stroke_width, stroke_type
sch_data[i:i + 1] = [seg_a, seg_b] )
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})") logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})")
splits += 1 splits += 1
i += 2 # skip the two new segments i += 2 # skip the two new segments
@@ -327,7 +377,9 @@ class WireManager:
return splits return splits
@staticmethod @staticmethod
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool: def add_junction(
schematic_path: Path, position: List[float], diameter: float = 0
) -> bool:
""" """
Add a junction (connection dot) to the schematic. Add a junction (connection dot) to the schematic.
@@ -345,7 +397,7 @@ class WireManager:
""" """
try: try:
# Read schematic # 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -359,17 +411,21 @@ class WireManager:
# Create junction S-expression # Create junction S-expression
# Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...)) # Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...))
junction_sexp = [ junction_sexp = [
Symbol('junction'), Symbol("junction"),
[Symbol('at'), position[0], position[1]], [Symbol("at"), position[0], position[1]],
[Symbol('diameter'), diameter], [Symbol("diameter"), diameter],
[Symbol('color'), 0, 0, 0, 0], [Symbol("color"), 0, 0, 0, 0],
[Symbol('uuid'), str(uuid.uuid4())] [Symbol("uuid"), str(uuid.uuid4())],
] ]
# Find insertion point # Find insertion point
sheet_instances_index = None sheet_instances_index = None
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: if (
isinstance(item, list)
and len(item) > 0
and item[0] == _SYM_SHEET_INSTANCES
):
sheet_instances_index = i sheet_instances_index = i
break break
@@ -382,7 +438,7 @@ class WireManager:
logger.info(f"Injected junction at {position}") logger.info(f"Injected junction at {position}")
# Write back # 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) output = sexpdata.dumps(sch_data)
f.write(output) f.write(output)
@@ -392,6 +448,7 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error adding junction: {e}") logger.error(f"Error adding junction: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@@ -409,7 +466,7 @@ class WireManager:
""" """
try: try:
# Read schematic # 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -417,15 +474,19 @@ class WireManager:
# Create no_connect S-expression # Create no_connect S-expression
# Format: (no_connect (at x y) (uuid ...)) # Format: (no_connect (at x y) (uuid ...))
no_connect_sexp = [ no_connect_sexp = [
Symbol('no_connect'), Symbol("no_connect"),
[Symbol('at'), position[0], position[1]], [Symbol("at"), position[0], position[1]],
[Symbol('uuid'), str(uuid.uuid4())] [Symbol("uuid"), str(uuid.uuid4())],
] ]
# Find insertion point # Find insertion point
sheet_instances_index = None sheet_instances_index = None
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES: if (
isinstance(item, list)
and len(item) > 0
and item[0] == _SYM_SHEET_INSTANCES
):
sheet_instances_index = i sheet_instances_index = i
break break
@@ -438,7 +499,7 @@ class WireManager:
logger.info(f"Injected no-connect at {position}") logger.info(f"Injected no-connect at {position}")
# Write back # 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) output = sexpdata.dumps(sch_data)
f.write(output) f.write(output)
@@ -448,12 +509,17 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error adding no-connect: {e}") logger.error(f"Error adding no-connect: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def delete_wire(schematic_path: Path, start_point: List[float], end_point: List[float], def delete_wire(
tolerance: float = 0.5) -> bool: 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. Delete a wire from the schematic matching given start/end coordinates.
@@ -467,7 +533,7 @@ class WireManager:
True if a wire was found and removed, False otherwise True if a wire was found and removed, False otherwise
""" """
try: 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
@@ -476,34 +542,54 @@ class WireManager:
ex, ey = end_point ex, ey = end_point
for i, item in enumerate(sch_data): 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 continue
# Extract pts from the wire s-expression # Extract pts from the wire s-expression
pts_list = None pts_list = None
for part in item[1:]: 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 pts_list = part
break break
if pts_list is None: if pts_list is None:
continue 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: if len(xy_points) < 2:
continue continue
x1, y1 = float(xy_points[0][1]), float(xy_points[0][2]) x1, y1 = float(xy_points[0][1]), float(xy_points[0][2])
x2, y2 = float(xy_points[-1][1]), float(xy_points[-1][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 match_fwd = (
abs(x2 - ex) < tolerance and abs(y2 - ey) < tolerance) abs(x1 - sx) < tolerance
match_rev = (abs(x1 - ex) < tolerance and abs(y1 - ey) < tolerance and and abs(y1 - sy) < tolerance
abs(x2 - sx) < tolerance and abs(y2 - 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: if match_fwd or match_rev:
del sch_data[i] 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)) f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted wire from {start_point} to {end_point}") logger.info(f"Deleted wire from {start_point} to {end_point}")
return True return True
@@ -514,13 +600,17 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error deleting wire: {e}") logger.error(f"Error deleting wire: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def delete_label(schematic_path: Path, net_name: str, def delete_label(
position: Optional[List[float]] = None, schematic_path: Path,
tolerance: float = 0.5) -> bool: 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). Delete a net label from the schematic by name (and optionally position).
@@ -534,14 +624,17 @@ class WireManager:
True if a label was found and removed, False otherwise True if a label was found and removed, False otherwise
""" """
try: 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_content = f.read()
sch_data = sexpdata.loads(sch_content) sch_data = sexpdata.loads(sch_content)
for i, item in enumerate(sch_data): for i, item in enumerate(sch_data):
if not (isinstance(item, list) and len(item) > 0 if not (
and item[0] == Symbol('label')): isinstance(item, list)
and len(item) > 0
and item[0] == Symbol("label")
):
continue continue
# Second element is the label text # Second element is the label text
@@ -551,19 +644,26 @@ class WireManager:
if position is not None: if position is not None:
# Find (at x y ...) sub-expression and check coordinates # Find (at x y ...) sub-expression and check coordinates
at_entry = next( 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, None,
) )
if at_entry is None: if at_entry is None:
continue continue
lx, ly = float(at_entry[1]), float(at_entry[2]) lx, ly = float(at_entry[1]), float(at_entry[2])
if not (abs(lx - position[0]) < tolerance if not (
and abs(ly - position[1]) < tolerance): abs(lx - position[0]) < tolerance
and abs(ly - position[1]) < tolerance
):
continue continue
del sch_data[i] 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)) f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted label '{net_name}'") logger.info(f"Deleted label '{net_name}'")
return True return True
@@ -574,12 +674,14 @@ class WireManager:
except Exception as e: except Exception as e:
logger.error(f"Error deleting label: {e}") logger.error(f"Error deleting label: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return False return False
@staticmethod @staticmethod
def create_orthogonal_path(start: List[float], end: List[float], def create_orthogonal_path(
prefer_horizontal_first: bool = True) -> List[List[float]]: start: List[float], end: List[float], prefer_horizontal_first: bool = True
) -> List[List[float]]:
""" """
Create an orthogonal (right-angle) path between two points Create an orthogonal (right-angle) path between two points
@@ -608,10 +710,11 @@ class WireManager:
return [start, corner, end] return [start, corner, end]
if __name__ == '__main__': if __name__ == "__main__":
# Test wire creation # Test wire creation
import sys 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 from pathlib import Path
import shutil import shutil
@@ -621,8 +724,10 @@ if __name__ == '__main__':
print("=" * 80) print("=" * 80)
# Create test schematic (cross-platform temp directory) # Create test schematic (cross-platform temp directory)
test_path = Path(tempfile.gettempdir()) / 'test_wire_manager.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') template_path = Path(
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch"
)
shutil.copy(template_path, test_path) shutil.copy(template_path, test_path)
print(f"\n✓ Created test schematic: {test_path}") print(f"\n✓ Created test schematic: {test_path}")
@@ -658,8 +763,9 @@ if __name__ == '__main__':
print("\n[Verification] Loading with kicad-skip...") print("\n[Verification] Loading with kicad-skip...")
try: try:
from skip import Schematic from skip import Schematic
sch = Schematic(str(test_path)) 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" ✓ Loaded successfully")
print(f" ✓ Wire count: {wire_count}") print(f" ✓ Wire count: {wire_count}")
except Exception as e: except Exception as e:

View File

@@ -4,7 +4,7 @@
* Centralizes all tool definitions and provides lookup/search functionality * Centralizes all tool definitions and provides lookup/search functionality
*/ */
import { z } from 'zod'; import { z } from "zod";
export interface ToolDefinition { export interface ToolDefinition {
name: string; name: string;
@@ -26,7 +26,8 @@ export interface ToolCategory {
export const toolCategories: ToolCategory[] = [ export const toolCategories: ToolCategory[] = [
{ {
name: "board", name: "board",
description: "Board configuration: layers, mounting holes, zones, visualization", description:
"Board configuration: layers, mounting holes, zones, visualization",
tools: [ tools: [
"add_layer", "add_layer",
"set_active_layer", "set_active_layer",
@@ -36,12 +37,13 @@ export const toolCategories: ToolCategory[] = [
"add_zone", "add_zone",
"get_board_extents", "get_board_extents",
"get_board_2d_view", "get_board_2d_view",
"launch_kicad_ui" "launch_kicad_ui",
] ],
}, },
{ {
name: "component", name: "component",
description: "Advanced component operations: edit, delete, search, group, annotate", description:
"Advanced component operations: edit, delete, search, group, annotate",
tools: [ tools: [
"rotate_component", "rotate_component",
"delete_component", "delete_component",
@@ -50,12 +52,13 @@ export const toolCategories: ToolCategory[] = [
"get_component_properties", "get_component_properties",
"add_component_annotation", "add_component_annotation",
"group_components", "group_components",
"replace_component" "replace_component",
] ],
}, },
{ {
name: "export", name: "export",
description: "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models", description:
"File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
tools: [ tools: [
"export_gerber", "export_gerber",
"export_pdf", "export_pdf",
@@ -64,12 +67,13 @@ export const toolCategories: ToolCategory[] = [
"export_bom", "export_bom",
"export_netlist", "export_netlist",
"export_position_file", "export_position_file",
"export_vrml" "export_vrml",
] ],
}, },
{ {
name: "drc", name: "drc",
description: "Design rule checking and electrical validation: DRC, net classes, clearances", description:
"Design rule checking and electrical validation: DRC, net classes, clearances",
tools: [ tools: [
"set_design_rules", "set_design_rules",
"get_design_rules", "get_design_rules",
@@ -78,12 +82,13 @@ export const toolCategories: ToolCategory[] = [
"assign_net_to_class", "assign_net_to_class",
"set_layer_constraints", "set_layer_constraints",
"check_clearance", "check_clearance",
"get_drc_violations" "get_drc_violations",
] ],
}, },
{ {
name: "schematic", name: "schematic",
description: "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation", description:
"Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
tools: [ tools: [
"create_schematic", "create_schematic",
"add_schematic_component", "add_schematic_component",
@@ -107,37 +112,31 @@ export const toolCategories: ToolCategory[] = [
"sync_schematic_to_board", "sync_schematic_to_board",
"get_schematic_view", "get_schematic_view",
"export_schematic_svg", "export_schematic_svg",
"export_schematic_pdf" "export_schematic_pdf",
] ],
}, },
{ {
name: "library", name: "library",
description: "Footprint library access: search, browse, get footprint information", description:
"Footprint library access: search, browse, get footprint information",
tools: [ tools: [
"list_libraries", "list_libraries",
"search_footprints", "search_footprints",
"list_library_footprints", "list_library_footprints",
"get_footprint_info" "get_footprint_info",
] ],
}, },
{ {
name: "routing", name: "routing",
description: "Advanced routing operations: vias, copper pours", description: "Advanced routing operations: vias, copper pours",
tools: [ tools: ["add_via", "add_copper_pour"],
"add_via",
"add_copper_pour"
]
}, },
{ {
name: "autoroute", name: "autoroute",
description: "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES", description:
tools: [ "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES",
"autoroute", tools: ["autoroute", "export_dsn", "import_ses", "check_freerouting"],
"export_dsn", },
"import_ses",
"check_freerouting"
]
}
]; ];
/** /**
@@ -175,7 +174,7 @@ export const directToolNames = [
"sync_schematic_to_board", "sync_schematic_to_board",
// UI management // UI management
"check_kicad_ui" "check_kicad_ui",
]; ];
// Build lookup maps at module load time // Build lookup maps at module load time
@@ -260,7 +259,7 @@ export function searchTools(query: string): SearchResult[] {
matches.push({ matches.push({
category: "direct", category: "direct",
tool: toolName, tool: toolName,
description: `${toolName} (direct tool — call directly, no execute_tool needed)` description: `${toolName} (direct tool — call directly, no execute_tool needed)`,
}); });
} }
} }
@@ -276,7 +275,7 @@ export function searchTools(query: string): SearchResult[] {
matches.push({ matches.push({
category: category.name, category: category.name,
tool: toolName, tool: toolName,
description: `${toolName} (${category.name})` description: `${toolName} (${category.name})`,
}); });
} }
} }
@@ -297,10 +296,10 @@ export function getRegistryStats() {
total_routed_tools: routedToolCount, total_routed_tools: routedToolCount,
total_direct_tools: directToolCount, total_direct_tools: directToolCount,
total_tools: routedToolCount + directToolCount, total_tools: routedToolCount + directToolCount,
categories: toolCategories.map(c => ({ categories: toolCategories.map((c) => ({
name: c.name, name: c.name,
tool_count: c.tools.length tool_count: c.tools.length,
})) })),
}; };
} }

View File

@@ -161,15 +161,37 @@ preserves the component's position and UUID.
Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.`, Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_component.`,
{ {
schematicPath: z.string().describe("Path to the .kicad_sch file"), schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Current reference designator of the component (e.g. R1, U3)"), reference: z
footprint: z.string().optional().describe("New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)"), .string()
value: z.string().optional().describe("New value string (e.g. 10k, 100nF)"), .describe(
newReference: z.string().optional().describe("Rename the reference designator (e.g. R1 → R10)"), "Current reference designator of the component (e.g. R1, U3)",
fieldPositions: z.record(z.object({ ),
x: z.number(), footprint: z
y: z.number(), .string()
angle: z.number().optional().default(0), .optional()
})).optional().describe("Reposition field labels: map of field name to {x, y, angle} (e.g. {\"Reference\": {\"x\": 12.5, \"y\": 17.0}})"), .describe(
"New KiCAD footprint string (e.g. Resistor_SMD:R_0603_1608Metric)",
),
value: z
.string()
.optional()
.describe("New value string (e.g. 10k, 100nF)"),
newReference: z
.string()
.optional()
.describe("Rename the reference designator (e.g. R1 → R10)"),
fieldPositions: z
.record(
z.object({
x: z.number(),
y: z.number(),
angle: z.number().optional().default(0),
}),
)
.optional()
.describe(
'Reposition field labels: map of field name to {x, y, angle} (e.g. {"Reference": {"x": 12.5, "y": 17.0}})',
),
}, },
async (args: { async (args: {
schematicPath: string; schematicPath: string;
@@ -210,7 +232,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"Get full component info from a schematic: position, field values, and each field's label position (at x/y/angle). Use this to inspect or prepare repositioning of Reference/Value labels.", "Get full component info from a schematic: position, field values, and each field's label position (at x/y/angle). Use this to inspect or prepare repositioning of Reference/Value labels.",
{ {
schematicPath: z.string().describe("Path to the .kicad_sch file"), schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Component reference designator (e.g. R1, U1)"), reference: z
.string()
.describe("Component reference designator (e.g. R1, U1)"),
}, },
async (args: { schematicPath: string; reference: string }) => { async (args: { schematicPath: string; reference: string }) => {
const result = await callKicadScript("get_schematic_component", args); const result = await callKicadScript("get_schematic_component", args);
@@ -220,20 +244,24 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
: "unknown"; : "unknown";
const fieldLines = Object.entries(result.fields ?? {}).map( const fieldLines = Object.entries(result.fields ?? {}).map(
([name, f]: [string, any]) => ([name, f]: [string, any]) =>
` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)` ` ${name}: "${f.value}" @ (${f.x}, ${f.y}, angle=${f.angle}°)`,
); );
return { return {
content: [{ content: [
type: "text", {
text: `Component ${result.reference} at ${pos}\nFields:\n${fieldLines.join("\n")}`, type: "text",
}], text: `Component ${result.reference} at ${pos}\nFields:\n${fieldLines.join("\n")}`,
},
],
}; };
} }
return { return {
content: [{ content: [
type: "text", {
text: `Failed to get component: ${result.message || "Unknown error"}`, type: "text",
}], text: `Failed to get component: ${result.message || "Unknown error"}`,
},
],
}; };
}, },
); );
@@ -580,8 +608,14 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
schematicPath: z.string().describe("Path to the .kicad_sch file"), schematicPath: z.string().describe("Path to the .kicad_sch file"),
filter: z filter: z
.object({ .object({
libId: z.string().optional().describe("Filter by library ID (e.g., 'Device:R')"), libId: z
referencePrefix: z.string().optional().describe("Filter by reference prefix (e.g., 'R', 'C', 'U')"), .string()
.optional()
.describe("Filter by library ID (e.g., 'Device:R')"),
referencePrefix: z
.string()
.optional()
.describe("Filter by reference prefix (e.g., 'R', 'C', 'U')"),
}) })
.optional() .optional()
.describe("Optional filters"), .describe("Optional filters"),
@@ -595,7 +629,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
const comps = result.components || []; const comps = result.components || [];
if (comps.length === 0) { if (comps.length === 0) {
return { return {
content: [{ type: "text", text: "No components found in schematic." }], content: [
{ type: "text", text: "No components found in schematic." },
],
}; };
} }
const lines = comps.map( const lines = comps.map(
@@ -656,7 +692,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
} }
return { return {
content: [ content: [
{ type: "text", text: `Failed to list nets: ${result.message || "Unknown error"}` }, {
type: "text",
text: `Failed to list nets: ${result.message || "Unknown error"}`,
},
], ],
isError: true, isError: true,
}; };
@@ -694,7 +733,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
} }
return { return {
content: [ content: [
{ type: "text", text: `Failed to list wires: ${result.message || "Unknown error"}` }, {
type: "text",
text: `Failed to list wires: ${result.message || "Unknown error"}`,
},
], ],
isError: true, isError: true,
}; };
@@ -732,7 +774,10 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
} }
return { return {
content: [ content: [
{ type: "text", text: `Failed to list labels: ${result.message || "Unknown error"}` }, {
type: "text",
text: `Failed to list labels: ${result.message || "Unknown error"}`,
},
], ],
isError: true, isError: true,
}; };
@@ -789,10 +834,7 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
schematicPath: z.string().describe("Path to the .kicad_sch file"), schematicPath: z.string().describe("Path to the .kicad_sch file"),
reference: z.string().describe("Reference designator (e.g., R1, U1)"), reference: z.string().describe("Reference designator (e.g., R1, U1)"),
angle: z.number().describe("Rotation angle in degrees (0, 90, 180, 270)"), angle: z.number().describe("Rotation angle in degrees (0, 90, 180, 270)"),
mirror: z mirror: z.enum(["x", "y"]).optional().describe("Optional mirror axis"),
.enum(["x", "y"])
.optional()
.describe("Optional mirror axis"),
}, },
async (args: { async (args: {
schematicPath: string; schematicPath: string;
@@ -1036,8 +1078,14 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
.enum(["png", "svg"]) .enum(["png", "svg"])
.optional() .optional()
.describe("Output format (default: png)"), .describe("Output format (default: png)"),
width: z.number().optional().describe("Image width in pixels (default: 1200)"), width: z
height: z.number().optional().describe("Image height in pixels (default: 900)"), .number()
.optional()
.describe("Image width in pixels (default: 1200)"),
height: z
.number()
.optional()
.describe("Image height in pixels (default: 900)"),
}, },
async (args: { async (args: {
schematicPath: string; schematicPath: string;
@@ -1207,19 +1255,35 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"get_schematic_view_region", "get_schematic_view_region",
"Export a cropped region of the schematic as an image (PNG or SVG). Specify bounding box coordinates in schematic mm. Useful for zooming into a specific area to inspect wiring or layout.", "Export a cropped region of the schematic as an image (PNG or SVG). Specify bounding box coordinates in schematic mm. Useful for zooming into a specific area to inspect wiring or layout.",
{ {
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"), schematicPath: z
.string()
.describe("Path to the .kicad_sch schematic file"),
x1: z.number().describe("Left X coordinate of the region in mm"), x1: z.number().describe("Left X coordinate of the region in mm"),
y1: z.number().describe("Top Y coordinate of the region in mm"), y1: z.number().describe("Top Y coordinate of the region in mm"),
x2: z.number().describe("Right X coordinate of the region in mm"), x2: z.number().describe("Right X coordinate of the region in mm"),
y2: z.number().describe("Bottom Y coordinate of the region in mm"), y2: z.number().describe("Bottom Y coordinate of the region in mm"),
format: z.enum(["png", "svg"]).optional().describe("Output image format (default: png)"), format: z
width: z.number().optional().describe("Output image width in pixels (default: 800)"), .enum(["png", "svg"])
height: z.number().optional().describe("Output image height in pixels (default: 600)"), .optional()
.describe("Output image format (default: png)"),
width: z
.number()
.optional()
.describe("Output image width in pixels (default: 800)"),
height: z
.number()
.optional()
.describe("Output image height in pixels (default: 600)"),
}, },
async (args: { async (args: {
schematicPath: string; schematicPath: string;
x1: number; y1: number; x2: number; y2: number; x1: number;
format?: string; width?: number; height?: number; y1: number;
x2: number;
y2: number;
format?: string;
width?: number;
height?: number;
}) => { }) => {
const result = await callKicadScript("get_schematic_view_region", args); const result = await callKicadScript("get_schematic_view_region", args);
if (result.success && result.imageData) { if (result.success && result.imageData) {
@@ -1227,27 +1291,40 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
return { content: [{ type: "text", text: result.imageData }] }; return { content: [{ type: "text", text: result.imageData }] };
} }
return { return {
content: [{ content: [
type: "image", {
data: result.imageData, type: "image",
mimeType: "image/png", data: result.imageData,
}], mimeType: "image/png",
},
],
}; };
} }
return { return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }], content: [
{
type: "text",
text: `Failed: ${result.message || "Unknown error"}`,
},
],
}; };
}, },
); );
// Find overlapping elements // Find overlapping elements
server.tool( server.tool(
"find_overlapping_elements", "find_overlapping_elements",
"Detect spatially overlapping symbols, wires, and labels in the schematic. Finds duplicate power symbols at the same position, collinear overlapping wires, and labels stacked on top of each other.", "Detect spatially overlapping symbols, wires, and labels in the schematic. Finds duplicate power symbols at the same position, collinear overlapping wires, and labels stacked on top of each other.",
{ {
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"), schematicPath: z
tolerance: z.number().optional().describe("Distance threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection. (default: 0.5)"), .string()
.describe("Path to the .kicad_sch schematic file"),
tolerance: z
.number()
.optional()
.describe(
"Distance threshold in mm for label proximity and wire collinearity checks. Symbol overlap uses bounding-box intersection. (default: 0.5)",
),
}, },
async (args: { schematicPath: string; tolerance?: number }) => { async (args: { schematicPath: string; tolerance?: number }) => {
const result = await callKicadScript("find_overlapping_elements", args); const result = await callKicadScript("find_overlapping_elements", args);
@@ -1259,25 +1336,36 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
if (syms.length) { if (syms.length) {
lines.push(`\nOverlapping symbols (${syms.length}):`); lines.push(`\nOverlapping symbols (${syms.length}):`);
syms.slice(0, 20).forEach((o: any) => { syms.slice(0, 20).forEach((o: any) => {
lines.push(` ${o.element1.reference}${o.element2.reference} (${o.distance}mm) [${o.type}]`); lines.push(
` ${o.element1.reference}${o.element2.reference} (${o.distance}mm) [${o.type}]`,
);
}); });
} }
if (lbls.length) { if (lbls.length) {
lines.push(`\nOverlapping labels (${lbls.length}):`); lines.push(`\nOverlapping labels (${lbls.length}):`);
lbls.slice(0, 20).forEach((o: any) => { lbls.slice(0, 20).forEach((o: any) => {
lines.push(` "${o.element1.name}" ↔ "${o.element2.name}" (${o.distance}mm)`); lines.push(
` "${o.element1.name}" ↔ "${o.element2.name}" (${o.distance}mm)`,
);
}); });
} }
if (wires.length) { if (wires.length) {
lines.push(`\nOverlapping wires (${wires.length}):`); lines.push(`\nOverlapping wires (${wires.length}):`);
wires.slice(0, 20).forEach((o: any) => { wires.slice(0, 20).forEach((o: any) => {
lines.push(` wire @ (${o.wire1.start.x},${o.wire1.start.y})→(${o.wire1.end.x},${o.wire1.end.y}) overlaps with another`); lines.push(
` wire @ (${o.wire1.start.x},${o.wire1.start.y})→(${o.wire1.end.x},${o.wire1.end.y}) overlaps with another`,
);
}); });
} }
return { content: [{ type: "text", text: lines.join("\n") }] }; return { content: [{ type: "text", text: lines.join("\n") }] };
} }
return { return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }], content: [
{
type: "text",
text: `Failed: ${result.message || "Unknown error"}`,
},
],
}; };
}, },
); );
@@ -1287,7 +1375,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"get_elements_in_region", "get_elements_in_region",
"List all symbols, wires, and labels within a rectangular region of the schematic. Useful for understanding what is in a specific area before modifying it.", "List all symbols, wires, and labels within a rectangular region of the schematic. Useful for understanding what is in a specific area before modifying it.",
{ {
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"), schematicPath: z
.string()
.describe("Path to the .kicad_sch schematic file"),
x1: z.number().describe("Left X coordinate of the region in mm"), x1: z.number().describe("Left X coordinate of the region in mm"),
y1: z.number().describe("Top Y coordinate of the region in mm"), y1: z.number().describe("Top Y coordinate of the region in mm"),
x2: z.number().describe("Right X coordinate of the region in mm"), x2: z.number().describe("Right X coordinate of the region in mm"),
@@ -1295,39 +1385,56 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
}, },
async (args: { async (args: {
schematicPath: string; schematicPath: string;
x1: number; y1: number; x2: number; y2: number; x1: number;
y1: number;
x2: number;
y2: number;
}) => { }) => {
const result = await callKicadScript("get_elements_in_region", args); const result = await callKicadScript("get_elements_in_region", args);
if (result.success) { if (result.success) {
const c = result.counts; const c = result.counts;
const lines = [`Region (${args.x1},${args.y1})→(${args.x2},${args.y2}): ${c.symbols} symbols, ${c.wires} wires, ${c.labels} labels`]; const lines = [
`Region (${args.x1},${args.y1})→(${args.x2},${args.y2}): ${c.symbols} symbols, ${c.wires} wires, ${c.labels} labels`,
];
const syms: any[] = result.symbols || []; const syms: any[] = result.symbols || [];
if (syms.length) { if (syms.length) {
lines.push("\nSymbols:"); lines.push("\nSymbols:");
syms.forEach((s: any) => { syms.forEach((s: any) => {
const pinCount = s.pins ? Object.keys(s.pins).length : 0; const pinCount = s.pins ? Object.keys(s.pins).length : 0;
lines.push(` ${s.reference} (${s.libId}) @ (${s.position.x}, ${s.position.y}) [${pinCount} pins]`); lines.push(
` ${s.reference} (${s.libId}) @ (${s.position.x}, ${s.position.y}) [${pinCount} pins]`,
);
}); });
} }
const wires: any[] = result.wires || []; const wires: any[] = result.wires || [];
if (wires.length) { if (wires.length) {
lines.push(`\nWires (${wires.length}):`); lines.push(`\nWires (${wires.length}):`);
wires.slice(0, 30).forEach((w: any) => { wires.slice(0, 30).forEach((w: any) => {
lines.push(` (${w.start.x},${w.start.y}) → (${w.end.x},${w.end.y})`); lines.push(
` (${w.start.x},${w.start.y}) → (${w.end.x},${w.end.y})`,
);
}); });
if (wires.length > 30) lines.push(` ... and ${wires.length - 30} more`); if (wires.length > 30)
lines.push(` ... and ${wires.length - 30} more`);
} }
const labels: any[] = result.labels || []; const labels: any[] = result.labels || [];
if (labels.length) { if (labels.length) {
lines.push(`\nLabels (${labels.length}):`); lines.push(`\nLabels (${labels.length}):`);
labels.forEach((l: any) => { labels.forEach((l: any) => {
lines.push(` "${l.name}" [${l.type}] @ (${l.position.x}, ${l.position.y})`); lines.push(
` "${l.name}" [${l.type}] @ (${l.position.x}, ${l.position.y})`,
);
}); });
} }
return { content: [{ type: "text", text: lines.join("\n") }] }; return { content: [{ type: "text", text: lines.join("\n") }] };
} }
return { return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }], content: [
{
type: "text",
text: `Failed: ${result.message || "Unknown error"}`,
},
],
}; };
}, },
); );
@@ -1337,7 +1444,9 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
"find_wires_crossing_symbols", "find_wires_crossing_symbols",
"Find all wires that cross over component symbol bodies. Wires passing over symbols are unacceptable in schematics — they indicate routing mistakes where a wire was drawn across a component instead of around it.", "Find all wires that cross over component symbol bodies. Wires passing over symbols are unacceptable in schematics — they indicate routing mistakes where a wire was drawn across a component instead of around it.",
{ {
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 }) => { async (args: { schematicPath: string }) => {
const result = await callKicadScript("find_wires_crossing_symbols", args); const result = await callKicadScript("find_wires_crossing_symbols", args);
@@ -1346,14 +1455,20 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
const lines = [`Found ${collisions.length} wire(s) crossing symbols:`]; const lines = [`Found ${collisions.length} wire(s) crossing symbols:`];
collisions.slice(0, 30).forEach((c: any, i: number) => { collisions.slice(0, 30).forEach((c: any, i: number) => {
lines.push( lines.push(
` ${i + 1}. Wire (${c.wire.start.x},${c.wire.start.y})→(${c.wire.end.x},${c.wire.end.y}) crosses ${c.component.reference} (${c.component.libId})` ` ${i + 1}. Wire (${c.wire.start.x},${c.wire.start.y})→(${c.wire.end.x},${c.wire.end.y}) crosses ${c.component.reference} (${c.component.libId})`,
); );
}); });
if (collisions.length > 30) lines.push(` ... and ${collisions.length - 30} more`); if (collisions.length > 30)
lines.push(` ... and ${collisions.length - 30} more`);
return { content: [{ type: "text", text: lines.join("\n") }] }; return { content: [{ type: "text", text: lines.join("\n") }] };
} }
return { return {
content: [{ type: "text", text: `Failed: ${result.message || "Unknown error"}` }], content: [
{
type: "text",
text: `Failed: ${result.message || "Unknown error"}`,
},
],
}; };
}, },
); );