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:
@@ -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:
|
||||||
|
|||||||
@@ -1,308 +1,307 @@
|
|||||||
/**
|
/**
|
||||||
* Tool Registry for KiCAD MCP Server
|
* Tool Registry for KiCAD MCP Server
|
||||||
*
|
*
|
||||||
* 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;
|
||||||
description: string;
|
description: string;
|
||||||
inputSchema: z.ZodObject<any> | z.ZodType<any>;
|
inputSchema: z.ZodObject<any> | z.ZodType<any>;
|
||||||
// Handler will be registered separately in the existing tool files
|
// Handler will be registered separately in the existing tool files
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCategory {
|
export interface ToolCategory {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
tools: string[]; // Tool names in this category
|
tools: string[]; // Tool names in this category
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool category definitions
|
* Tool category definitions
|
||||||
* Each category groups related tools for better organization
|
* Each category groups related tools for better organization
|
||||||
*/
|
*/
|
||||||
export const toolCategories: ToolCategory[] = [
|
export const toolCategories: ToolCategory[] = [
|
||||||
{
|
{
|
||||||
name: "board",
|
name: "board",
|
||||||
description: "Board configuration: layers, mounting holes, zones, visualization",
|
description:
|
||||||
tools: [
|
"Board configuration: layers, mounting holes, zones, visualization",
|
||||||
"add_layer",
|
tools: [
|
||||||
"set_active_layer",
|
"add_layer",
|
||||||
"get_layer_list",
|
"set_active_layer",
|
||||||
"add_mounting_hole",
|
"get_layer_list",
|
||||||
"add_board_text",
|
"add_mounting_hole",
|
||||||
"add_zone",
|
"add_board_text",
|
||||||
"get_board_extents",
|
"add_zone",
|
||||||
"get_board_2d_view",
|
"get_board_extents",
|
||||||
"launch_kicad_ui"
|
"get_board_2d_view",
|
||||||
]
|
"launch_kicad_ui",
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: "component",
|
{
|
||||||
description: "Advanced component operations: edit, delete, search, group, annotate",
|
name: "component",
|
||||||
tools: [
|
description:
|
||||||
"rotate_component",
|
"Advanced component operations: edit, delete, search, group, annotate",
|
||||||
"delete_component",
|
tools: [
|
||||||
"edit_component",
|
"rotate_component",
|
||||||
"find_component",
|
"delete_component",
|
||||||
"get_component_properties",
|
"edit_component",
|
||||||
"add_component_annotation",
|
"find_component",
|
||||||
"group_components",
|
"get_component_properties",
|
||||||
"replace_component"
|
"add_component_annotation",
|
||||||
]
|
"group_components",
|
||||||
},
|
"replace_component",
|
||||||
{
|
],
|
||||||
name: "export",
|
},
|
||||||
description: "File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
|
{
|
||||||
tools: [
|
name: "export",
|
||||||
"export_gerber",
|
description:
|
||||||
"export_pdf",
|
"File export for fabrication and documentation: Gerber, PDF, BOM, 3D models",
|
||||||
"export_svg",
|
tools: [
|
||||||
"export_3d",
|
"export_gerber",
|
||||||
"export_bom",
|
"export_pdf",
|
||||||
"export_netlist",
|
"export_svg",
|
||||||
"export_position_file",
|
"export_3d",
|
||||||
"export_vrml"
|
"export_bom",
|
||||||
]
|
"export_netlist",
|
||||||
},
|
"export_position_file",
|
||||||
{
|
"export_vrml",
|
||||||
name: "drc",
|
],
|
||||||
description: "Design rule checking and electrical validation: DRC, net classes, clearances",
|
},
|
||||||
tools: [
|
{
|
||||||
"set_design_rules",
|
name: "drc",
|
||||||
"get_design_rules",
|
description:
|
||||||
"run_drc",
|
"Design rule checking and electrical validation: DRC, net classes, clearances",
|
||||||
"add_net_class",
|
tools: [
|
||||||
"assign_net_to_class",
|
"set_design_rules",
|
||||||
"set_layer_constraints",
|
"get_design_rules",
|
||||||
"check_clearance",
|
"run_drc",
|
||||||
"get_drc_violations"
|
"add_net_class",
|
||||||
]
|
"assign_net_to_class",
|
||||||
},
|
"set_layer_constraints",
|
||||||
{
|
"check_clearance",
|
||||||
name: "schematic",
|
"get_drc_violations",
|
||||||
description: "Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
|
],
|
||||||
tools: [
|
},
|
||||||
"create_schematic",
|
{
|
||||||
"add_schematic_component",
|
name: "schematic",
|
||||||
"list_schematic_components",
|
description:
|
||||||
"move_schematic_component",
|
"Schematic operations: create, inspect, add/edit/delete components, wire connections, netlists, annotation",
|
||||||
"rotate_schematic_component",
|
tools: [
|
||||||
"annotate_schematic",
|
"create_schematic",
|
||||||
"add_schematic_wire",
|
"add_schematic_component",
|
||||||
"delete_schematic_wire",
|
"list_schematic_components",
|
||||||
"add_schematic_junction",
|
"move_schematic_component",
|
||||||
"add_schematic_net_label",
|
"rotate_schematic_component",
|
||||||
"delete_schematic_net_label",
|
"annotate_schematic",
|
||||||
"connect_to_net",
|
"add_schematic_wire",
|
||||||
"connect_passthrough",
|
"delete_schematic_wire",
|
||||||
"get_net_connections",
|
"add_schematic_junction",
|
||||||
"list_schematic_nets",
|
"add_schematic_net_label",
|
||||||
"list_schematic_wires",
|
"delete_schematic_net_label",
|
||||||
"list_schematic_labels",
|
"connect_to_net",
|
||||||
"get_wire_connections",
|
"connect_passthrough",
|
||||||
"generate_netlist",
|
"get_net_connections",
|
||||||
"sync_schematic_to_board",
|
"list_schematic_nets",
|
||||||
"get_schematic_view",
|
"list_schematic_wires",
|
||||||
"export_schematic_svg",
|
"list_schematic_labels",
|
||||||
"export_schematic_pdf"
|
"get_wire_connections",
|
||||||
]
|
"generate_netlist",
|
||||||
},
|
"sync_schematic_to_board",
|
||||||
{
|
"get_schematic_view",
|
||||||
name: "library",
|
"export_schematic_svg",
|
||||||
description: "Footprint library access: search, browse, get footprint information",
|
"export_schematic_pdf",
|
||||||
tools: [
|
],
|
||||||
"list_libraries",
|
},
|
||||||
"search_footprints",
|
{
|
||||||
"list_library_footprints",
|
name: "library",
|
||||||
"get_footprint_info"
|
description:
|
||||||
]
|
"Footprint library access: search, browse, get footprint information",
|
||||||
},
|
tools: [
|
||||||
{
|
"list_libraries",
|
||||||
name: "routing",
|
"search_footprints",
|
||||||
description: "Advanced routing operations: vias, copper pours",
|
"list_library_footprints",
|
||||||
tools: [
|
"get_footprint_info",
|
||||||
"add_via",
|
],
|
||||||
"add_copper_pour"
|
},
|
||||||
]
|
{
|
||||||
},
|
name: "routing",
|
||||||
{
|
description: "Advanced routing operations: vias, copper pours",
|
||||||
name: "autoroute",
|
tools: ["add_via", "add_copper_pour"],
|
||||||
description: "Freerouting autorouter: automatic PCB routing via Specctra DSN/SES",
|
},
|
||||||
tools: [
|
{
|
||||||
"autoroute",
|
name: "autoroute",
|
||||||
"export_dsn",
|
description:
|
||||||
"import_ses",
|
"Freerouting autorouter: automatic PCB routing via Specctra DSN/SES",
|
||||||
"check_freerouting"
|
tools: ["autoroute", "export_dsn", "import_ses", "check_freerouting"],
|
||||||
]
|
},
|
||||||
}
|
];
|
||||||
];
|
|
||||||
|
/**
|
||||||
/**
|
* Direct tools that are always visible (not routed)
|
||||||
* Direct tools that are always visible (not routed)
|
* These are the most frequently used tools
|
||||||
* These are the most frequently used tools
|
*/
|
||||||
*/
|
export const directToolNames = [
|
||||||
export const directToolNames = [
|
// Project lifecycle
|
||||||
// Project lifecycle
|
"create_project",
|
||||||
"create_project",
|
"open_project",
|
||||||
"open_project",
|
"save_project",
|
||||||
"save_project",
|
"snapshot_project",
|
||||||
"snapshot_project",
|
"get_project_info",
|
||||||
"get_project_info",
|
|
||||||
|
// Core PCB operations
|
||||||
// Core PCB operations
|
"place_component",
|
||||||
"place_component",
|
"move_component",
|
||||||
"move_component",
|
"add_net",
|
||||||
"add_net",
|
"route_trace",
|
||||||
"route_trace",
|
"get_board_info",
|
||||||
"get_board_info",
|
"set_board_size",
|
||||||
"set_board_size",
|
|
||||||
|
// Board setup
|
||||||
// Board setup
|
"add_board_outline",
|
||||||
"add_board_outline",
|
|
||||||
|
// Schematic essentials (always visible so AI uses them correctly)
|
||||||
// Schematic essentials (always visible so AI uses them correctly)
|
"add_schematic_component",
|
||||||
"add_schematic_component",
|
"list_schematic_components",
|
||||||
"list_schematic_components",
|
"annotate_schematic",
|
||||||
"annotate_schematic",
|
"connect_passthrough",
|
||||||
"connect_passthrough",
|
"connect_to_net",
|
||||||
"connect_to_net",
|
"add_schematic_net_label",
|
||||||
"add_schematic_net_label",
|
|
||||||
|
// Schematic <-> PCB sync (F8 equivalent)
|
||||||
// Schematic <-> PCB sync (F8 equivalent)
|
"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
|
const categoryMap = new Map<string, ToolCategory>();
|
||||||
const categoryMap = new Map<string, ToolCategory>();
|
const toolCategoryMap = new Map<string, string>();
|
||||||
const toolCategoryMap = new Map<string, string>();
|
|
||||||
|
export function initializeRegistry() {
|
||||||
export function initializeRegistry() {
|
// Build category map
|
||||||
// Build category map
|
for (const category of toolCategories) {
|
||||||
for (const category of toolCategories) {
|
categoryMap.set(category.name, category);
|
||||||
categoryMap.set(category.name, category);
|
|
||||||
|
// Build tool -> category map
|
||||||
// Build tool -> category map
|
for (const toolName of category.tools) {
|
||||||
for (const toolName of category.tools) {
|
toolCategoryMap.set(toolName, category.name);
|
||||||
toolCategoryMap.set(toolName, category.name);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get a category by name
|
||||||
* Get a category by name
|
*/
|
||||||
*/
|
export function getCategory(name: string): ToolCategory | undefined {
|
||||||
export function getCategory(name: string): ToolCategory | undefined {
|
return categoryMap.get(name);
|
||||||
return categoryMap.get(name);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get the category name for a tool
|
||||||
* Get the category name for a tool
|
*/
|
||||||
*/
|
export function getToolCategory(toolName: string): string | undefined {
|
||||||
export function getToolCategory(toolName: string): string | undefined {
|
return toolCategoryMap.get(toolName);
|
||||||
return toolCategoryMap.get(toolName);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get all categories
|
||||||
* Get all categories
|
*/
|
||||||
*/
|
export function getAllCategories(): ToolCategory[] {
|
||||||
export function getAllCategories(): ToolCategory[] {
|
return toolCategories;
|
||||||
return toolCategories;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get all routed tool names (excludes direct tools)
|
||||||
* Get all routed tool names (excludes direct tools)
|
*/
|
||||||
*/
|
export function getRoutedToolNames(): string[] {
|
||||||
export function getRoutedToolNames(): string[] {
|
const allRoutedTools: string[] = [];
|
||||||
const allRoutedTools: string[] = [];
|
for (const category of toolCategories) {
|
||||||
for (const category of toolCategories) {
|
allRoutedTools.push(...category.tools);
|
||||||
allRoutedTools.push(...category.tools);
|
}
|
||||||
}
|
return allRoutedTools;
|
||||||
return allRoutedTools;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Check if a tool is a direct tool
|
||||||
* Check if a tool is a direct tool
|
*/
|
||||||
*/
|
export function isDirectTool(toolName: string): boolean {
|
||||||
export function isDirectTool(toolName: string): boolean {
|
return directToolNames.includes(toolName);
|
||||||
return directToolNames.includes(toolName);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Check if a tool is a routed tool
|
||||||
* Check if a tool is a routed tool
|
*/
|
||||||
*/
|
export function isRoutedTool(toolName: string): boolean {
|
||||||
export function isRoutedTool(toolName: string): boolean {
|
return toolCategoryMap.has(toolName);
|
||||||
return toolCategoryMap.has(toolName);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Search for tools by keyword
|
||||||
* Search for tools by keyword
|
* Searches tool names, descriptions, and category names
|
||||||
* Searches tool names, descriptions, and category names
|
*/
|
||||||
*/
|
export interface SearchResult {
|
||||||
export interface SearchResult {
|
category: string;
|
||||||
category: string;
|
tool: string;
|
||||||
tool: string;
|
description: string;
|
||||||
description: string;
|
}
|
||||||
}
|
|
||||||
|
export function searchTools(query: string): SearchResult[] {
|
||||||
export function searchTools(query: string): SearchResult[] {
|
const q = query.toLowerCase();
|
||||||
const q = query.toLowerCase();
|
const matches: SearchResult[] = [];
|
||||||
const matches: SearchResult[] = [];
|
|
||||||
|
// Search direct tools first
|
||||||
// Search direct tools first
|
for (const toolName of directToolNames) {
|
||||||
for (const toolName of directToolNames) {
|
if (toolName.toLowerCase().includes(q)) {
|
||||||
if (toolName.toLowerCase().includes(q)) {
|
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)`
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Search routed tools by name and category
|
||||||
// Search routed tools by name and category
|
for (const category of toolCategories) {
|
||||||
for (const category of toolCategories) {
|
const categoryMatch =
|
||||||
const categoryMatch =
|
category.name.toLowerCase().includes(q) ||
|
||||||
category.name.toLowerCase().includes(q) ||
|
category.description.toLowerCase().includes(q);
|
||||||
category.description.toLowerCase().includes(q);
|
|
||||||
|
for (const toolName of category.tools) {
|
||||||
for (const toolName of category.tools) {
|
if (toolName.toLowerCase().includes(q) || categoryMatch) {
|
||||||
if (toolName.toLowerCase().includes(q) || categoryMatch) {
|
matches.push({
|
||||||
matches.push({
|
category: category.name,
|
||||||
category: category.name,
|
tool: toolName,
|
||||||
tool: toolName,
|
description: `${toolName} (${category.name})`,
|
||||||
description: `${toolName} (${category.name})`
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return matches.slice(0, 20); // Limit results
|
||||||
return matches.slice(0, 20); // Limit results
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get statistics about the tool registry
|
||||||
* Get statistics about the tool registry
|
*/
|
||||||
*/
|
export function getRegistryStats() {
|
||||||
export function getRegistryStats() {
|
const routedToolCount = getRoutedToolNames().length;
|
||||||
const routedToolCount = getRoutedToolNames().length;
|
const directToolCount = directToolNames.length;
|
||||||
const directToolCount = directToolNames.length;
|
|
||||||
|
return {
|
||||||
return {
|
total_categories: toolCategories.length,
|
||||||
total_categories: toolCategories.length,
|
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
|
})),
|
||||||
}))
|
};
|
||||||
};
|
}
|
||||||
}
|
|
||||||
|
// Initialize on module load
|
||||||
// Initialize on module load
|
initializeRegistry();
|
||||||
initializeRegistry();
|
|
||||||
|
|||||||
@@ -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"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user