feat: add wire/junction tools with pin-snapping and T-junction support
- Rename add_schematic_connection → add_schematic_wire with waypoints[] parameter - Add snapToPins (default true) to snap wire endpoints to nearest pin - Expose add_schematic_junction as an MCP tool - Break existing wires at new wire endpoints for T-junction support - Remove orphaned add_connection / add_wire / get_pin_location from ConnectionManager - Update tool registry to reflect renamed schematic tools in TS layer - Add 76 tests for wire/junction handler dispatch, schema validation, and WireManager corner cases - Apply Black and Prettier formatting to changed files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,172 +30,6 @@ class ConnectionManager:
|
|||||||
cls._pin_locator = PinLocator()
|
cls._pin_locator = PinLocator()
|
||||||
return cls._pin_locator
|
return cls._pin_locator
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_wire(
|
|
||||||
schematic_path: Path,
|
|
||||||
start_point: list,
|
|
||||||
end_point: list,
|
|
||||||
properties: dict = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Add a wire between two points using WireManager
|
|
||||||
|
|
||||||
Args:
|
|
||||||
schematic_path: Path to .kicad_sch file
|
|
||||||
start_point: [x, y] coordinates for wire start
|
|
||||||
end_point: [x, y] coordinates for wire end
|
|
||||||
properties: Optional wire properties (stroke_width, stroke_type)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not WIRE_MANAGER_AVAILABLE:
|
|
||||||
logger.error("WireManager not available")
|
|
||||||
return False
|
|
||||||
|
|
||||||
stroke_width = properties.get("stroke_width", 0) if properties else 0
|
|
||||||
stroke_type = (
|
|
||||||
properties.get("stroke_type", "default") if properties else "default"
|
|
||||||
)
|
|
||||||
|
|
||||||
success = WireManager.add_wire(
|
|
||||||
schematic_path,
|
|
||||||
start_point,
|
|
||||||
end_point,
|
|
||||||
stroke_width=stroke_width,
|
|
||||||
stroke_type=stroke_type,
|
|
||||||
)
|
|
||||||
return success
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding wire: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_pin_location(symbol, pin_name: str):
|
|
||||||
"""
|
|
||||||
Get the absolute location of a pin on a symbol
|
|
||||||
|
|
||||||
Args:
|
|
||||||
symbol: Symbol object
|
|
||||||
pin_name: Name or number of the pin (e.g., "1", "GND", "VCC")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
[x, y] coordinates or None if pin not found
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not hasattr(symbol, "pin"):
|
|
||||||
logger.warning(f"Symbol {symbol.property.Reference.value} has no pins")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Find the pin by name
|
|
||||||
target_pin = None
|
|
||||||
for pin in symbol.pin:
|
|
||||||
if pin.name == pin_name:
|
|
||||||
target_pin = pin
|
|
||||||
break
|
|
||||||
|
|
||||||
if not target_pin:
|
|
||||||
logger.warning(
|
|
||||||
f"Pin '{pin_name}' not found on {symbol.property.Reference.value}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Get pin location relative to symbol
|
|
||||||
pin_loc = target_pin.location
|
|
||||||
# Get symbol location
|
|
||||||
symbol_at = symbol.at.value
|
|
||||||
|
|
||||||
# Calculate absolute position
|
|
||||||
# pin_loc is relative to symbol origin, need to add symbol position
|
|
||||||
abs_x = symbol_at[0] + pin_loc[0]
|
|
||||||
abs_y = symbol_at[1] + pin_loc[1]
|
|
||||||
|
|
||||||
return [abs_x, abs_y]
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error getting pin location: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_connection(
|
|
||||||
schematic_path: Path,
|
|
||||||
source_ref: str,
|
|
||||||
source_pin: str,
|
|
||||||
target_ref: str,
|
|
||||||
target_pin: str,
|
|
||||||
routing: str = "direct",
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Add a wire connection between two component pins
|
|
||||||
|
|
||||||
Args:
|
|
||||||
schematic_path: Path to .kicad_sch file
|
|
||||||
source_ref: Reference designator of source component (e.g., "R1", "R1_")
|
|
||||||
source_pin: Pin name/number on source component
|
|
||||||
target_ref: Reference designator of target component (e.g., "C1", "C1_")
|
|
||||||
target_pin: Pin name/number on target component
|
|
||||||
routing: Routing style ('direct', 'orthogonal_h', 'orthogonal_v')
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if connection was successful, False otherwise
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if not WIRE_MANAGER_AVAILABLE:
|
|
||||||
logger.error("WireManager/PinLocator not available")
|
|
||||||
return False
|
|
||||||
|
|
||||||
locator = ConnectionManager.get_pin_locator()
|
|
||||||
if not locator:
|
|
||||||
logger.error("Pin locator unavailable")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Get pin locations
|
|
||||||
source_loc = locator.get_pin_location(
|
|
||||||
schematic_path, source_ref, source_pin
|
|
||||||
)
|
|
||||||
target_loc = locator.get_pin_location(
|
|
||||||
schematic_path, target_ref, target_pin
|
|
||||||
)
|
|
||||||
|
|
||||||
if not source_loc or not target_loc:
|
|
||||||
logger.error("Could not determine pin locations")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create wire based on routing style
|
|
||||||
if routing == "direct":
|
|
||||||
# Simple direct wire
|
|
||||||
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
|
|
||||||
elif routing == "orthogonal_h":
|
|
||||||
# Orthogonal routing (horizontal first)
|
|
||||||
path = WireManager.create_orthogonal_path(
|
|
||||||
source_loc, target_loc, prefer_horizontal_first=True
|
|
||||||
)
|
|
||||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
|
||||||
elif routing == "orthogonal_v":
|
|
||||||
# Orthogonal routing (vertical first)
|
|
||||||
path = WireManager.create_orthogonal_path(
|
|
||||||
source_loc, target_loc, prefer_horizontal_first=False
|
|
||||||
)
|
|
||||||
success = WireManager.add_polyline_wire(schematic_path, path)
|
|
||||||
else:
|
|
||||||
logger.error(f"Unknown routing style: {routing}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if success:
|
|
||||||
logger.info(
|
|
||||||
f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding connection: {e}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_net_label(schematic: Schematic, net_name: str, position: list):
|
def add_net_label(schematic: Schematic, net_name: str, position: list):
|
||||||
"""
|
"""
|
||||||
@@ -257,15 +91,20 @@ class ConnectionManager:
|
|||||||
|
|
||||||
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
|
# Add a small wire stub from the pin (2.54mm = 0.1 inch, standard grid spacing)
|
||||||
# Stub direction follows the pin's outward angle from the PinLocator
|
# Stub direction follows the pin's outward angle from the PinLocator
|
||||||
pin_angle_deg = getattr(locator, '_last_pin_angle', 0)
|
pin_angle_deg = getattr(locator, "_last_pin_angle", 0)
|
||||||
try:
|
try:
|
||||||
pin_angle_deg = locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
|
pin_angle_deg = (
|
||||||
|
locator.get_pin_angle(schematic_path, component_ref, pin_name) or 0
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pin_angle_deg = 0
|
pin_angle_deg = 0
|
||||||
import math as _math
|
import math as _math
|
||||||
|
|
||||||
angle_rad = _math.radians(pin_angle_deg)
|
angle_rad = _math.radians(pin_angle_deg)
|
||||||
stub_end = [round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
|
stub_end = [
|
||||||
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4)]
|
round(pin_loc[0] + 2.54 * _math.cos(angle_rad), 4),
|
||||||
|
round(pin_loc[1] - 2.54 * _math.sin(angle_rad), 4),
|
||||||
|
]
|
||||||
|
|
||||||
# Create wire stub using WireManager
|
# Create wire stub using WireManager
|
||||||
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
|
wire_success = WireManager.add_wire(schematic_path, pin_loc, stub_end)
|
||||||
@@ -333,9 +172,15 @@ class ConnectionManager:
|
|||||||
connected = []
|
connected = []
|
||||||
failed = []
|
failed = []
|
||||||
|
|
||||||
for pin_num in sorted(src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0):
|
for pin_num in sorted(
|
||||||
|
src_pins.keys(), key=lambda x: int(x) if x.isdigit() else 0
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
net_name = f"{net_prefix}_{int(pin_num) + pin_offset}" if pin_num.isdigit() else f"{net_prefix}_{pin_num}"
|
net_name = (
|
||||||
|
f"{net_prefix}_{int(pin_num) + pin_offset}"
|
||||||
|
if pin_num.isdigit()
|
||||||
|
else f"{net_prefix}_{pin_num}"
|
||||||
|
)
|
||||||
|
|
||||||
ok_src = ConnectionManager.connect_to_net(
|
ok_src = ConnectionManager.connect_to_net(
|
||||||
schematic_path, source_ref, pin_num, net_name
|
schematic_path, source_ref, pin_num, net_name
|
||||||
@@ -355,11 +200,15 @@ class ConnectionManager:
|
|||||||
failed.append(f"{target_ref}/{pin_num} (pin not found)")
|
failed.append(f"{target_ref}/{pin_num} (pin not found)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
connected.append(f"{source_ref}/{pin_num} <-> {target_ref}/{pin_num} [{net_name}]")
|
connected.append(
|
||||||
|
f"{source_ref}/{pin_num} <-> {target_ref}/{pin_num} [{net_name}]"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed.append(f"{source_ref}/{pin_num}: {e}")
|
failed.append(f"{source_ref}/{pin_num}: {e}")
|
||||||
|
|
||||||
logger.info(f"connect_passthrough: {len(connected)} connected, {len(failed)} failed")
|
logger.info(
|
||||||
|
f"connect_passthrough: {len(connected)} connected, {len(failed)} failed"
|
||||||
|
)
|
||||||
return {"connected": connected, "failed": failed}
|
return {"connected": connected, "failed": failed}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -607,35 +456,3 @@ class ConnectionManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error generating netlist: {e}")
|
logger.error(f"Error generating netlist: {e}")
|
||||||
return {"nets": [], "components": []}
|
return {"nets": [], "components": []}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Example Usage (for testing)
|
|
||||||
from schematic import (
|
|
||||||
SchematicManager,
|
|
||||||
) # Assuming schematic.py is in the same directory
|
|
||||||
|
|
||||||
# Create a new schematic
|
|
||||||
test_sch = SchematicManager.create_schematic("ConnectionTestSchematic")
|
|
||||||
|
|
||||||
# Add some wires
|
|
||||||
wire1 = ConnectionManager.add_wire(test_sch, [100, 100], [200, 100])
|
|
||||||
wire2 = ConnectionManager.add_wire(test_sch, [200, 100], [200, 200])
|
|
||||||
|
|
||||||
# Note: add_connection, remove_connection, get_net_connections are placeholders
|
|
||||||
# and require more complex implementation based on kicad-skip's structure.
|
|
||||||
|
|
||||||
# Example of how you might add a net label (requires finding a point on a wire)
|
|
||||||
# from skip import Label
|
|
||||||
# if wire1:
|
|
||||||
# net_label_pos = wire1.start # Or calculate a point on the wire
|
|
||||||
# net_label = test_sch.add_label(text="Net_01", at=net_label_pos)
|
|
||||||
# print(f"Added net label 'Net_01' at {net_label_pos}")
|
|
||||||
|
|
||||||
# Save the schematic (optional)
|
|
||||||
# SchematicManager.save_schematic(test_sch, "connection_test.kicad_sch")
|
|
||||||
|
|
||||||
# Clean up (if saved)
|
|
||||||
# if os.path.exists("connection_test.kicad_sch"):
|
|
||||||
# os.remove("connection_test.kicad_sch")
|
|
||||||
# print("Cleaned up connection_test.kicad_sch")
|
|
||||||
|
|||||||
@@ -11,24 +11,29 @@ import logging
|
|||||||
import math
|
import math
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple, Optional, Dict
|
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
|
||||||
|
_SYM_WIRE = Symbol('wire')
|
||||||
|
_SYM_PTS = Symbol('pts')
|
||||||
|
_SYM_XY = Symbol('xy')
|
||||||
|
_SYM_STROKE = Symbol('stroke')
|
||||||
|
_SYM_WIDTH = Symbol('width')
|
||||||
|
_SYM_TYPE = Symbol('type')
|
||||||
|
_SYM_UUID = Symbol('uuid')
|
||||||
|
_SYM_SHEET_INSTANCES = Symbol('sheet_instances')
|
||||||
|
|
||||||
|
|
||||||
class WireManager:
|
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(
|
def add_wire(schematic_path: Path, start_point: List[float], end_point: List[float],
|
||||||
schematic_path: Path,
|
stroke_width: float = 0, stroke_type: str = 'default') -> bool:
|
||||||
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
|
||||||
|
|
||||||
@@ -44,36 +49,25 @@ 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)
|
||||||
|
|
||||||
|
# Break any existing wire that passes through a new endpoint (T-junction support)
|
||||||
|
for pt in (start_point, end_point):
|
||||||
|
splits = WireManager._break_wires_at_point(sch_data, pt)
|
||||||
|
if splits:
|
||||||
|
logger.info(f"Broke {splits} wire(s) at new wire endpoint {pt}")
|
||||||
|
|
||||||
# Create wire S-expression
|
# 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 = [
|
wire_sexp = WireManager._make_wire_sexp(start_point, end_point, stroke_width, stroke_type)
|
||||||
Symbol("wire"),
|
|
||||||
[
|
|
||||||
Symbol("pts"),
|
|
||||||
[Symbol("xy"), start_point[0], start_point[1]],
|
|
||||||
[Symbol("xy"), end_point[0], end_point[1]],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
Symbol("stroke"),
|
|
||||||
[Symbol("width"), stroke_width],
|
|
||||||
[Symbol("type"), Symbol(stroke_type)],
|
|
||||||
],
|
|
||||||
[Symbol("uuid"), str(uuid.uuid4())],
|
|
||||||
]
|
|
||||||
|
|
||||||
# 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 (
|
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
|
||||||
isinstance(item, list)
|
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("sheet_instances")
|
|
||||||
):
|
|
||||||
sheet_instances_index = i
|
sheet_instances_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -86,7 +80,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)
|
||||||
|
|
||||||
@@ -96,17 +90,12 @@ 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(
|
def add_polyline_wire(schematic_path: Path, points: List[List[float]],
|
||||||
schematic_path: Path,
|
stroke_width: float = 0, stroke_type: str = 'default') -> bool:
|
||||||
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
|
||||||
|
|
||||||
@@ -125,36 +114,28 @@ 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)
|
||||||
|
|
||||||
# Create pts list
|
# Break any existing wire at the outer endpoints of the new path
|
||||||
pts_list = [Symbol("pts")]
|
for pt in (points[0], points[-1]):
|
||||||
for point in points:
|
splits = WireManager._break_wires_at_point(sch_data, pt)
|
||||||
pts_list.append([Symbol("xy"), point[0], point[1]])
|
if splits:
|
||||||
|
logger.info(f"Broke {splits} wire(s) at new polyline endpoint {pt}")
|
||||||
|
|
||||||
# Create wire S-expression with multiple points
|
# KiCAD wire elements only support exactly 2 pts each.
|
||||||
wire_sexp = [
|
# Split N waypoints into N-1 individual wire segments.
|
||||||
Symbol("wire"),
|
wire_sexps = [
|
||||||
pts_list,
|
WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type)
|
||||||
[
|
for i in range(len(points) - 1)
|
||||||
Symbol("stroke"),
|
|
||||||
[Symbol("width"), stroke_width],
|
|
||||||
[Symbol("type"), Symbol(stroke_type)],
|
|
||||||
],
|
|
||||||
[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 (
|
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
|
||||||
isinstance(item, list)
|
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("sheet_instances")
|
|
||||||
):
|
|
||||||
sheet_instances_index = i
|
sheet_instances_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -162,12 +143,13 @@ class WireManager:
|
|||||||
logger.error("No sheet_instances section found in schematic")
|
logger.error("No sheet_instances section found in schematic")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Insert wire
|
# Insert all segments (in reverse so order is preserved after inserts)
|
||||||
|
for wire_sexp in reversed(wire_sexps):
|
||||||
sch_data.insert(sheet_instances_index, wire_sexp)
|
sch_data.insert(sheet_instances_index, wire_sexp)
|
||||||
logger.info(f"Injected polyline wire with {len(points)} points")
|
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)
|
||||||
|
|
||||||
@@ -177,18 +159,12 @@ 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(
|
def add_label(schematic_path: Path, text: str, position: List[float],
|
||||||
schematic_path: Path,
|
label_type: str = 'label', orientation: int = 0) -> bool:
|
||||||
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
|
||||||
|
|
||||||
@@ -204,7 +180,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)
|
||||||
@@ -214,24 +190,19 @@ 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("effects"),
|
[Symbol('font'), [Symbol('size'), 1.27, 1.27]],
|
||||||
[Symbol("font"), [Symbol("size"), 1.27, 1.27]],
|
[Symbol('justify'), Symbol('left'), Symbol('bottom')]
|
||||||
[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 (
|
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
|
||||||
isinstance(item, list)
|
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("sheet_instances")
|
|
||||||
):
|
|
||||||
sheet_instances_index = i
|
sheet_instances_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -244,7 +215,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)
|
||||||
|
|
||||||
@@ -254,16 +225,115 @@ 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 add_junction(
|
def _parse_wire(wire_item) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
|
||||||
schematic_path: Path, position: List[float], diameter: float = 0
|
|
||||||
) -> bool:
|
|
||||||
"""
|
"""
|
||||||
Add a junction (connection dot) to the schematic
|
Parse a wire S-expression item in a single pass.
|
||||||
|
Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire.
|
||||||
|
"""
|
||||||
|
if not (isinstance(wire_item, list) and len(wire_item) >= 2
|
||||||
|
and wire_item[0] == _SYM_WIRE):
|
||||||
|
return None
|
||||||
|
start = end = None
|
||||||
|
stroke_width: float = 0
|
||||||
|
stroke_type: str = 'default'
|
||||||
|
for part in wire_item[1:]:
|
||||||
|
if not isinstance(part, list) or not part:
|
||||||
|
continue
|
||||||
|
tag = part[0]
|
||||||
|
if tag == _SYM_PTS:
|
||||||
|
found: List[Tuple[float, float]] = []
|
||||||
|
for p in part[1:]:
|
||||||
|
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY:
|
||||||
|
found.append((float(p[1]), float(p[2])))
|
||||||
|
if len(found) == 2:
|
||||||
|
break
|
||||||
|
if len(found) == 2:
|
||||||
|
start, end = found[0], found[1]
|
||||||
|
elif tag == _SYM_STROKE:
|
||||||
|
for sp in part[1:]:
|
||||||
|
if isinstance(sp, list) and len(sp) >= 2:
|
||||||
|
if sp[0] == _SYM_WIDTH:
|
||||||
|
stroke_width = sp[1]
|
||||||
|
elif sp[0] == _SYM_TYPE:
|
||||||
|
stroke_type = str(sp[1])
|
||||||
|
if start is not None and end is not None:
|
||||||
|
return start, end, stroke_width, stroke_type
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _point_strictly_on_wire(px: float, py: float,
|
||||||
|
x1: float, y1: float,
|
||||||
|
x2: float, y2: float,
|
||||||
|
eps: float = 1e-6) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if (px, py) lies strictly between (x1,y1) and (x2,y2)
|
||||||
|
on a horizontal or vertical wire segment (not at either endpoint).
|
||||||
|
"""
|
||||||
|
if abs(y1 - y2) < eps: # horizontal wire
|
||||||
|
if abs(py - y1) > eps:
|
||||||
|
return False
|
||||||
|
lo, hi = min(x1, x2), max(x1, x2)
|
||||||
|
return lo + eps < px < hi - eps
|
||||||
|
if abs(x1 - x2) < eps: # vertical wire
|
||||||
|
if abs(px - x1) > eps:
|
||||||
|
return False
|
||||||
|
lo, hi = min(y1, y2), max(y1, y2)
|
||||||
|
return lo + eps < py < hi - eps
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_wire_sexp(start: List[float], end: List[float],
|
||||||
|
stroke_width: float = 0, stroke_type: str = 'default') -> list:
|
||||||
|
return [
|
||||||
|
_SYM_WIRE,
|
||||||
|
[_SYM_PTS,
|
||||||
|
[_SYM_XY, start[0], start[1]],
|
||||||
|
[_SYM_XY, end[0], end[1]]],
|
||||||
|
[_SYM_STROKE,
|
||||||
|
[_SYM_WIDTH, stroke_width],
|
||||||
|
[_SYM_TYPE, Symbol(stroke_type)]],
|
||||||
|
[_SYM_UUID, str(uuid.uuid4())]
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _break_wires_at_point(sch_data: list, position: List[float]) -> int:
|
||||||
|
"""
|
||||||
|
Split any wire segment that passes through *position* as a strict
|
||||||
|
midpoint (i.e. position is not an existing endpoint). Mirrors
|
||||||
|
KiCAD's SCH_LINE_WIRE_BUS_TOOL::BreakSegments behaviour.
|
||||||
|
|
||||||
|
Returns the number of wires split.
|
||||||
|
"""
|
||||||
|
px, py = float(position[0]), float(position[1])
|
||||||
|
splits = 0
|
||||||
|
i = 0
|
||||||
|
while i < len(sch_data):
|
||||||
|
parsed = WireManager._parse_wire(sch_data[i])
|
||||||
|
if parsed is not None:
|
||||||
|
(x1, y1), (x2, y2), stroke_width, stroke_type = parsed
|
||||||
|
if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2):
|
||||||
|
seg_a = WireManager._make_wire_sexp([x1, y1], [px, py], stroke_width, stroke_type)
|
||||||
|
seg_b = WireManager._make_wire_sexp([px, py], [x2, y2], stroke_width, stroke_type)
|
||||||
|
sch_data[i:i + 1] = [seg_a, seg_b]
|
||||||
|
logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})")
|
||||||
|
splits += 1
|
||||||
|
i += 2 # skip the two new segments
|
||||||
|
continue
|
||||||
|
i += 1
|
||||||
|
return splits
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
|
||||||
|
"""
|
||||||
|
Add a junction (connection dot) to the schematic.
|
||||||
|
|
||||||
|
Mirrors KiCAD's AddJunction behaviour: any wire whose interior passes
|
||||||
|
through *position* is split into two segments at that point so that
|
||||||
|
the BFS-based get_wire_connections tool can traverse the T/X branch.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
schematic_path: Path to .kicad_sch file
|
schematic_path: Path to .kicad_sch file
|
||||||
@@ -275,29 +345,31 @@ 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)
|
||||||
|
|
||||||
|
# Split any wire that passes through the junction as a midpoint
|
||||||
|
# (mirrors KiCAD's AddJunction / BreakSegments behaviour)
|
||||||
|
splits = WireManager._break_wires_at_point(sch_data, position)
|
||||||
|
if splits:
|
||||||
|
logger.info(f"Broke {splits} wire(s) at junction position {position}")
|
||||||
|
|
||||||
# Create junction S-expression
|
# 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 (
|
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
|
||||||
isinstance(item, list)
|
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("sheet_instances")
|
|
||||||
):
|
|
||||||
sheet_instances_index = i
|
sheet_instances_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -310,7 +382,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)
|
||||||
|
|
||||||
@@ -320,7 +392,6 @@ 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
|
||||||
|
|
||||||
@@ -338,7 +409,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)
|
||||||
@@ -346,19 +417,15 @@ 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 (
|
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
|
||||||
isinstance(item, list)
|
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("sheet_instances")
|
|
||||||
):
|
|
||||||
sheet_instances_index = i
|
sheet_instances_index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -371,7 +438,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)
|
||||||
|
|
||||||
@@ -381,17 +448,12 @@ 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(
|
def delete_wire(schematic_path: Path, start_point: List[float], end_point: List[float],
|
||||||
schematic_path: Path,
|
tolerance: float = 0.5) -> bool:
|
||||||
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.
|
||||||
|
|
||||||
@@ -405,7 +467,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)
|
||||||
@@ -414,54 +476,34 @@ 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 (
|
if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol('wire')):
|
||||||
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 (
|
if isinstance(part, list) and len(part) > 0 and part[0] == Symbol('pts'):
|
||||||
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 = [
|
xy_points = [p for p in pts_list[1:] if isinstance(p, list) and len(p) >= 3 and p[0] == Symbol('xy')]
|
||||||
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 = (
|
match_fwd = (abs(x1 - sx) < tolerance and abs(y1 - sy) < tolerance and
|
||||||
abs(x1 - sx) < tolerance
|
abs(x2 - ex) < tolerance and abs(y2 - ey) < tolerance)
|
||||||
and abs(y1 - sy) < tolerance
|
match_rev = (abs(x1 - ex) < tolerance and abs(y1 - ey) < tolerance and
|
||||||
and abs(x2 - ex) < tolerance
|
abs(x2 - sx) < tolerance and abs(y2 - sy) < 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
|
||||||
@@ -472,17 +514,13 @@ 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(
|
def delete_label(schematic_path: Path, net_name: str,
|
||||||
schematic_path: Path,
|
|
||||||
net_name: str,
|
|
||||||
position: Optional[List[float]] = None,
|
position: Optional[List[float]] = None,
|
||||||
tolerance: float = 0.5,
|
tolerance: float = 0.5) -> bool:
|
||||||
) -> 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).
|
||||||
|
|
||||||
@@ -496,17 +534,14 @@ 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 (
|
if not (isinstance(item, list) and len(item) > 0
|
||||||
isinstance(item, list)
|
and item[0] == Symbol('label')):
|
||||||
and len(item) > 0
|
|
||||||
and item[0] == Symbol("label")
|
|
||||||
):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Second element is the label text
|
# Second element is the label text
|
||||||
@@ -516,26 +551,19 @@ 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)
|
||||||
p
|
and len(p) >= 3 and p[0] == Symbol('at')),
|
||||||
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 (
|
if not (abs(lx - position[0]) < tolerance
|
||||||
abs(lx - position[0]) < tolerance
|
and abs(ly - position[1]) < 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
|
||||||
@@ -546,14 +574,12 @@ 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(
|
def create_orthogonal_path(start: List[float], end: List[float],
|
||||||
start: List[float], end: List[float], prefer_horizontal_first: bool = True
|
prefer_horizontal_first: bool = True) -> List[List[float]]:
|
||||||
) -> List[List[float]]:
|
|
||||||
"""
|
"""
|
||||||
Create an orthogonal (right-angle) path between two points
|
Create an orthogonal (right-angle) path between two points
|
||||||
|
|
||||||
@@ -582,11 +608,10 @@ 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
|
||||||
@@ -596,10 +621,8 @@ 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(
|
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/empty.kicad_sch')
|
||||||
"/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}")
|
||||||
@@ -635,9 +658,8 @@ 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:
|
||||||
|
|||||||
@@ -376,8 +376,8 @@ class KiCADInterface:
|
|||||||
"edit_schematic_component": self._handle_edit_schematic_component,
|
"edit_schematic_component": self._handle_edit_schematic_component,
|
||||||
"get_schematic_component": self._handle_get_schematic_component,
|
"get_schematic_component": self._handle_get_schematic_component,
|
||||||
"add_schematic_wire": self._handle_add_schematic_wire,
|
"add_schematic_wire": self._handle_add_schematic_wire,
|
||||||
"add_schematic_connection": self._handle_add_schematic_connection,
|
|
||||||
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
"add_schematic_net_label": self._handle_add_schematic_net_label,
|
||||||
|
"add_schematic_junction": self._handle_add_schematic_junction,
|
||||||
"connect_to_net": self._handle_connect_to_net,
|
"connect_to_net": self._handle_connect_to_net,
|
||||||
"connect_passthrough": self._handle_connect_passthrough,
|
"connect_passthrough": self._handle_connect_passthrough,
|
||||||
"get_schematic_pin_locations": self._handle_get_schematic_pin_locations,
|
"get_schematic_pin_locations": self._handle_get_schematic_pin_locations,
|
||||||
@@ -1137,40 +1137,117 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params):
|
||||||
"""Add a wire to a schematic using WireManager"""
|
"""Add a wire to a schematic using WireManager, with optional pin snapping"""
|
||||||
logger.info("Adding wire to schematic")
|
logger.info("Adding wire to schematic")
|
||||||
try:
|
try:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from commands.wire_manager import WireManager
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
start_point = params.get("startPoint")
|
points = params.get("waypoints")
|
||||||
end_point = params.get("endPoint")
|
|
||||||
properties = params.get("properties", {})
|
properties = params.get("properties", {})
|
||||||
|
snap_to_pins = params.get("snapToPins", True)
|
||||||
|
snap_tolerance = params.get("snapTolerance", 1.0)
|
||||||
|
|
||||||
if not schematic_path:
|
if not schematic_path:
|
||||||
return {"success": False, "message": "Schematic path is required"}
|
return {"success": False, "message": "Schematic path is required"}
|
||||||
if not start_point or not end_point:
|
if not points or len(points) < 2:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": "Start and end points are required",
|
"message": "At least 2 waypoints are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Make a mutable copy of points
|
||||||
|
points = [list(p) for p in points]
|
||||||
|
|
||||||
|
# Pin snapping: adjust first and last endpoints to nearest pin
|
||||||
|
snapped_info = []
|
||||||
|
if snap_to_pins:
|
||||||
|
from commands.pin_locator import PinLocator
|
||||||
|
|
||||||
|
locator = PinLocator()
|
||||||
|
sch_path = Path(schematic_path)
|
||||||
|
|
||||||
|
# Load schematic to iterate all symbols
|
||||||
|
from skip import Schematic as SkipSchematic
|
||||||
|
|
||||||
|
sch = SkipSchematic(str(sch_path))
|
||||||
|
|
||||||
|
# Collect all pin locations: list of (ref, pin_num, [x, y])
|
||||||
|
all_pins = []
|
||||||
|
for symbol in sch.symbol:
|
||||||
|
if not hasattr(symbol.property, "Reference"):
|
||||||
|
continue
|
||||||
|
ref = symbol.property.Reference.value
|
||||||
|
if ref.startswith("_TEMPLATE"):
|
||||||
|
continue
|
||||||
|
pin_locs = locator.get_all_symbol_pins(sch_path, ref)
|
||||||
|
for pin_num, coords in pin_locs.items():
|
||||||
|
all_pins.append((ref, pin_num, coords))
|
||||||
|
|
||||||
|
def find_nearest_pin(point, tolerance):
|
||||||
|
"""Find the nearest pin within tolerance of a point."""
|
||||||
|
best = None
|
||||||
|
best_dist = tolerance
|
||||||
|
for ref, pin_num, coords in all_pins:
|
||||||
|
dx = point[0] - coords[0]
|
||||||
|
dy = point[1] - coords[1]
|
||||||
|
dist = (dx * dx + dy * dy) ** 0.5
|
||||||
|
if dist <= best_dist:
|
||||||
|
best_dist = dist
|
||||||
|
best = (ref, pin_num, coords)
|
||||||
|
return best
|
||||||
|
|
||||||
|
# Snap first endpoint
|
||||||
|
match = find_nearest_pin(points[0], snap_tolerance)
|
||||||
|
if match:
|
||||||
|
ref, pin_num, coords = match
|
||||||
|
logger.info(
|
||||||
|
f"Snapped start point {points[0]} -> {coords} (pin {ref}/{pin_num})"
|
||||||
|
)
|
||||||
|
snapped_info.append(
|
||||||
|
f"start snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
|
||||||
|
)
|
||||||
|
points[0] = list(coords)
|
||||||
|
|
||||||
|
# Snap last endpoint
|
||||||
|
match = find_nearest_pin(points[-1], snap_tolerance)
|
||||||
|
if match:
|
||||||
|
ref, pin_num, coords = match
|
||||||
|
logger.info(
|
||||||
|
f"Snapped end point {points[-1]} -> {coords} (pin {ref}/{pin_num})"
|
||||||
|
)
|
||||||
|
snapped_info.append(
|
||||||
|
f"end snapped to {ref}/{pin_num} at [{coords[0]}, {coords[1]}]"
|
||||||
|
)
|
||||||
|
points[-1] = list(coords)
|
||||||
|
|
||||||
# Extract wire properties
|
# Extract wire properties
|
||||||
stroke_width = properties.get("stroke_width", 0)
|
stroke_width = properties.get("stroke_width", 0)
|
||||||
stroke_type = properties.get("stroke_type", "default")
|
stroke_type = properties.get("stroke_type", "default")
|
||||||
|
|
||||||
# Use WireManager for S-expression manipulation
|
# Use WireManager for S-expression manipulation
|
||||||
|
if len(points) == 2:
|
||||||
success = WireManager.add_wire(
|
success = WireManager.add_wire(
|
||||||
Path(schematic_path),
|
Path(schematic_path),
|
||||||
start_point,
|
points[0],
|
||||||
end_point,
|
points[1],
|
||||||
|
stroke_width=stroke_width,
|
||||||
|
stroke_type=stroke_type,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
success = WireManager.add_polyline_wire(
|
||||||
|
Path(schematic_path),
|
||||||
|
points,
|
||||||
stroke_width=stroke_width,
|
stroke_width=stroke_width,
|
||||||
stroke_type=stroke_type,
|
stroke_type=stroke_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
return {"success": True, "message": "Wire added successfully"}
|
message = "Wire added successfully"
|
||||||
|
if snapped_info:
|
||||||
|
message += "; " + "; ".join(snapped_info)
|
||||||
|
return {"success": True, "message": message}
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": "Failed to add wire"}
|
return {"success": False, "message": "Failed to add wire"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1184,6 +1261,38 @@ class KiCADInterface:
|
|||||||
"errorDetails": traceback.format_exc(),
|
"errorDetails": traceback.format_exc(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _handle_add_schematic_junction(self, params):
|
||||||
|
"""Add a junction (connection dot) to a schematic using WireManager"""
|
||||||
|
logger.info("Adding junction to schematic")
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
|
schematic_path = params.get("schematicPath")
|
||||||
|
position = params.get("position")
|
||||||
|
|
||||||
|
if not schematic_path:
|
||||||
|
return {"success": False, "message": "Schematic path is required"}
|
||||||
|
if not position:
|
||||||
|
return {"success": False, "message": "Position is required"}
|
||||||
|
|
||||||
|
success = WireManager.add_junction(Path(schematic_path), position)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return {"success": True, "message": "Junction added successfully"}
|
||||||
|
else:
|
||||||
|
return {"success": False, "message": "Failed to add junction"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error adding junction to schematic: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": str(e),
|
||||||
|
"errorDetails": traceback.format_exc(),
|
||||||
|
}
|
||||||
|
|
||||||
def _handle_list_schematic_libraries(self, params):
|
def _handle_list_schematic_libraries(self, params):
|
||||||
"""List available symbol libraries"""
|
"""List available symbol libraries"""
|
||||||
logger.info("Listing schematic libraries")
|
logger.info("Listing schematic libraries")
|
||||||
@@ -1393,54 +1502,6 @@ class KiCADInterface:
|
|||||||
logger.error(f"Error exporting schematic to PDF: {str(e)}")
|
logger.error(f"Error exporting schematic to PDF: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_connection(self, params):
|
|
||||||
"""Add a pin-to-pin connection in schematic with automatic pin discovery and routing"""
|
|
||||||
logger.info("Adding pin-to-pin connection in schematic")
|
|
||||||
try:
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
|
||||||
source_ref = params.get("sourceRef")
|
|
||||||
source_pin = params.get("sourcePin")
|
|
||||||
target_ref = params.get("targetRef")
|
|
||||||
target_pin = params.get("targetPin")
|
|
||||||
routing = params.get(
|
|
||||||
"routing", "direct"
|
|
||||||
) # 'direct', 'orthogonal_h', 'orthogonal_v'
|
|
||||||
|
|
||||||
if not all(
|
|
||||||
[schematic_path, source_ref, source_pin, target_ref, target_pin]
|
|
||||||
):
|
|
||||||
return {"success": False, "message": "Missing required parameters"}
|
|
||||||
|
|
||||||
# Use ConnectionManager with new PinLocator and WireManager integration
|
|
||||||
success = ConnectionManager.add_connection(
|
|
||||||
Path(schematic_path),
|
|
||||||
source_ref,
|
|
||||||
source_pin,
|
|
||||||
target_ref,
|
|
||||||
target_pin,
|
|
||||||
routing=routing,
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"success": False, "message": "Failed to add connection"}
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error adding schematic connection: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": str(e),
|
|
||||||
"errorDetails": traceback.format_exc(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _handle_add_schematic_net_label(self, params):
|
def _handle_add_schematic_net_label(self, params):
|
||||||
"""Add a net label to schematic using WireManager"""
|
"""Add a net label to schematic using WireManager"""
|
||||||
logger.info("Adding net label to schematic")
|
logger.info("Adding net label to schematic")
|
||||||
|
|||||||
@@ -1362,30 +1362,8 @@ SCHEMATIC_TOOLS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "add_schematic_wire",
|
"name": "add_schematic_wire",
|
||||||
"title": "Connect Components",
|
"title": "Draw Wire Between Pins",
|
||||||
"description": "Draws a wire connection between component pins on the schematic.",
|
"description": "Draws a wire on the schematic between two or more coordinate points. Always call get_schematic_pin_locations first to get the approximate pin coordinates, then pass them as the first and last waypoints. snapToPins (on by default) will correct any float imprecision by snapping endpoints to the exact nearest pin coordinate. To route around components, add intermediate waypoints between the start and end: e.g. [[x1,y1], [xMid,y1], [xMid,y2], [x2,y2]] routes horizontally then vertically. Intermediate waypoints are never snapped.",
|
||||||
"inputSchema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"points": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "Array of [x, y] waypoints for the wire",
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {"type": "number"},
|
|
||||||
"minItems": 2,
|
|
||||||
"maxItems": 2,
|
|
||||||
},
|
|
||||||
"minItems": 2,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["points"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "add_schematic_connection",
|
|
||||||
"title": "Add Junction/Connection Point",
|
|
||||||
"description": "Adds a junction (connection point) at the specified location on the schematic where wires cross and should connect.",
|
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -1393,10 +1371,29 @@ SCHEMATIC_TOOLS = [
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to schematic file",
|
"description": "Path to schematic file",
|
||||||
},
|
},
|
||||||
"x": {"type": "number", "description": "X coordinate on schematic"},
|
"waypoints": {
|
||||||
"y": {"type": "number", "description": "Y coordinate on schematic"},
|
"type": "array",
|
||||||
|
"description": "Array of [x, y] coordinates defining the wire path. First and last points are the pin locations (from get_schematic_pin_locations). Add intermediate points to route around obstacles.",
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "number"},
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 2,
|
||||||
},
|
},
|
||||||
"required": ["schematicPath", "x", "y"],
|
"minItems": 2,
|
||||||
|
},
|
||||||
|
"snapToPins": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "When true, the first and last waypoints are snapped to the nearest schematic pin within snapTolerance mm. Intermediate waypoints are left unchanged. Enabled by default to correct float coordinate imprecision.",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
"snapTolerance": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Maximum distance in mm to search for a nearby pin when snapToPins is enabled.",
|
||||||
|
"default": 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["schematicPath", "waypoints"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1610,6 +1607,28 @@ SCHEMATIC_TOOLS = [
|
|||||||
"required": ["schematicPath", "outputPath"],
|
"required": ["schematicPath", "outputPath"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "add_schematic_junction",
|
||||||
|
"title": "Add Junction to Schematic",
|
||||||
|
"description": "Adds a junction (connection dot) at the specified coordinates on the schematic. Junctions are required in KiCAD to mark intentional connections where wires cross or where a wire branches off another wire. Without a junction, crossing wires are not electrically connected.",
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"schematicPath": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to schematic file",
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The [x, y] coordinates where the junction should be placed. Must be on an existing wire intersection or branch point.",
|
||||||
|
"items": {"type": "number"},
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["schematicPath", "position"],
|
||||||
|
},
|
||||||
|
},
|
||||||
# --- Schematic Analysis Tools (read-only) ---
|
# --- Schematic Analysis Tools (read-only) ---
|
||||||
{
|
{
|
||||||
"name": "get_schematic_view_region",
|
"name": "get_schematic_view_region",
|
||||||
|
|||||||
1038
python/tests/test_wire_junction_changes.py
Normal file
1038
python/tests/test_wire_junction_changes.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -91,9 +91,9 @@ export const toolCategories: ToolCategory[] = [
|
|||||||
"move_schematic_component",
|
"move_schematic_component",
|
||||||
"rotate_schematic_component",
|
"rotate_schematic_component",
|
||||||
"annotate_schematic",
|
"annotate_schematic",
|
||||||
"add_wire",
|
"add_schematic_wire",
|
||||||
"delete_schematic_wire",
|
"delete_schematic_wire",
|
||||||
"add_schematic_connection",
|
"add_schematic_junction",
|
||||||
"add_schematic_net_label",
|
"add_schematic_net_label",
|
||||||
"delete_schematic_net_label",
|
"delete_schematic_net_label",
|
||||||
"connect_to_net",
|
"connect_to_net",
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export function registerSchematicTools(
|
|||||||
),
|
),
|
||||||
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
reference: z.string().describe("Component reference (e.g., R1, U1)"),
|
||||||
value: z.string().optional().describe("Component value"),
|
value: z.string().optional().describe("Component value"),
|
||||||
footprint: z.string().optional().describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
footprint: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("KiCAD footprint (e.g. Resistor_SMD:R_0603_1608Metric)"),
|
||||||
position: z
|
position: z
|
||||||
.object({
|
.object({
|
||||||
x: z.number(),
|
x: z.number(),
|
||||||
@@ -119,7 +122,9 @@ To remove a footprint from a PCB, use delete_component instead.`,
|
|||||||
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
reference: z
|
reference: z
|
||||||
.string()
|
.string()
|
||||||
.describe("Reference designator of the component to remove (e.g. R1, U3)"),
|
.describe(
|
||||||
|
"Reference designator of the component to remove (e.g. R1, U3)",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; reference: string }) => {
|
async (args: { schematicPath: string; reference: string }) => {
|
||||||
const result = await callKicadScript("delete_schematic_component", args);
|
const result = await callKicadScript("delete_schematic_component", args);
|
||||||
@@ -233,66 +238,40 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect components with wire
|
// Draw wire between coordinate waypoints with optional pin snapping
|
||||||
server.tool(
|
server.tool(
|
||||||
"add_wire",
|
"add_schematic_wire",
|
||||||
"Add a wire connection in the schematic",
|
"Draws a wire on the schematic between two or more coordinate points. Always call get_schematic_pin_locations first to get the approximate pin coordinates, then pass them as the first and last waypoints. snapToPins (on by default) will correct any float imprecision by snapping endpoints to the exact nearest pin coordinate. To route around components, add intermediate waypoints between the start and end: e.g. [[x1,y1], [xMid,y1], [xMid,y2], [x2,y2]] routes horizontally then vertically. Intermediate waypoints are never snapped.",
|
||||||
{
|
{
|
||||||
start: z
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
.object({
|
waypoints: z
|
||||||
x: z.number(),
|
.array(z.array(z.number()).length(2))
|
||||||
y: z.number(),
|
.min(2)
|
||||||
})
|
.describe("Ordered list of [x, y] coordinates. Minimum 2 points."),
|
||||||
.describe("Start position"),
|
snapToPins: z
|
||||||
end: z
|
.boolean()
|
||||||
.object({
|
.optional()
|
||||||
x: z.number(),
|
.describe(
|
||||||
y: z.number(),
|
"Snap the first and last waypoints to the nearest pin (default: true)",
|
||||||
})
|
),
|
||||||
.describe("End position"),
|
snapTolerance: z
|
||||||
},
|
.number()
|
||||||
async (args: any) => {
|
.optional()
|
||||||
const result = await callKicadScript("add_wire", args);
|
.describe("Maximum snap distance in mm (default: 1.0)"),
|
||||||
return {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(result, null, 2),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add pin-to-pin connection
|
|
||||||
server.tool(
|
|
||||||
"add_schematic_connection",
|
|
||||||
"Connect two component pins with a wire. Use this for individual connections between components with different pin roles (e.g. U1.SDA → J3.2). WARNING: Do NOT use this in a loop to wire N passthrough pins — use connect_passthrough instead (single call, cleaner layout, far fewer tokens).",
|
|
||||||
{
|
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
|
||||||
sourceRef: z.string().describe("Source component reference (e.g., R1)"),
|
|
||||||
sourcePin: z
|
|
||||||
.string()
|
|
||||||
.describe("Source pin name/number (e.g., 1, 2, GND)"),
|
|
||||||
targetRef: z.string().describe("Target component reference (e.g., C1)"),
|
|
||||||
targetPin: z
|
|
||||||
.string()
|
|
||||||
.describe("Target pin name/number (e.g., 1, 2, VCC)"),
|
|
||||||
},
|
},
|
||||||
async (args: {
|
async (args: {
|
||||||
schematicPath: string;
|
schematicPath: string;
|
||||||
sourceRef: string;
|
waypoints: number[][];
|
||||||
sourcePin: string;
|
snapToPins?: boolean;
|
||||||
targetRef: string;
|
snapTolerance?: number;
|
||||||
targetPin: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const result = await callKicadScript("add_schematic_connection", args);
|
const result = await callKicadScript("add_schematic_wire", args);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text" as const,
|
||||||
text: `Successfully connected ${args.sourceRef}/${args.sourcePin} to ${args.targetRef}/${args.targetPin}`,
|
text: result.message || "Wire added successfully",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -300,8 +279,43 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text" as const,
|
||||||
text: `Failed to add connection: ${result.message || "Unknown error"}`,
|
text: `Failed to add wire: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add junction dot at a T/X intersection
|
||||||
|
server.tool(
|
||||||
|
"add_schematic_junction",
|
||||||
|
"Place a junction dot at a wire intersection in the schematic. Required at T-branch and X-cross points so KiCAD recognises the electrical connection.",
|
||||||
|
{
|
||||||
|
schematicPath: z.string().describe("Path to the .kicad_sch file"),
|
||||||
|
position: z
|
||||||
|
.array(z.number())
|
||||||
|
.length(2)
|
||||||
|
.describe("Junction position [x, y] in mm"),
|
||||||
|
},
|
||||||
|
async (args: { schematicPath: string; position: number[] }) => {
|
||||||
|
const result = await callKicadScript("add_schematic_junction", args);
|
||||||
|
if (result.success) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: result.message || "Junction added successfully",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `Failed to add junction: ${result.message || "Unknown error"}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -431,27 +445,33 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
"Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.",
|
"Returns the exact x/y coordinates of every pin on a schematic component. Use this before add_schematic_net_label to place labels correctly on pin endpoints.",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
reference: z.string().describe("Component reference designator (e.g. U1, R1, J2)"),
|
reference: z
|
||||||
|
.string()
|
||||||
|
.describe("Component reference designator (e.g. U1, R1, J2)"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; reference: string }) => {
|
async (args: { schematicPath: string; reference: string }) => {
|
||||||
const result = await callKicadScript("get_schematic_pin_locations", args);
|
const result = await callKicadScript("get_schematic_pin_locations", args);
|
||||||
if (result.success && result.pins) {
|
if (result.success && result.pins) {
|
||||||
const lines = Object.entries(result.pins as Record<string, any>).map(
|
const lines = Object.entries(result.pins as Record<string, any>).map(
|
||||||
([pinNum, data]: [string, any]) =>
|
([pinNum, data]: [string, any]) =>
|
||||||
` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°`
|
` Pin ${pinNum} (${data.name || pinNum}): x=${data.x}, y=${data.y}, angle=${data.angle ?? 0}°`,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`,
|
text: `Pin locations for ${args.reference}:\n${lines.join("\n")}`,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [
|
||||||
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Failed to get pin locations: ${result.message || "Unknown error"}`,
|
text: `Failed to get pin locations: ${result.message || "Unknown error"}`,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -465,21 +485,49 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
schematicPath: z.string().describe("Path to the schematic file"),
|
schematicPath: z.string().describe("Path to the schematic file"),
|
||||||
sourceRef: z.string().describe("Source connector reference (e.g. J1)"),
|
sourceRef: z.string().describe("Source connector reference (e.g. J1)"),
|
||||||
targetRef: z.string().describe("Target connector reference (e.g. J2)"),
|
targetRef: z.string().describe("Target connector reference (e.g. J2)"),
|
||||||
netPrefix: z.string().optional().describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"),
|
netPrefix: z
|
||||||
pinOffset: z.number().optional().describe("Add to pin number when building net name (default: 0)"),
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Net name prefix, e.g. 'CSI' → CSI_1, CSI_2 (default: PIN)"),
|
||||||
|
pinOffset: z
|
||||||
|
.number()
|
||||||
|
.optional()
|
||||||
|
.describe("Add to pin number when building net name (default: 0)"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; sourceRef: string; targetRef: string; netPrefix?: string; pinOffset?: number }) => {
|
async (args: {
|
||||||
|
schematicPath: string;
|
||||||
|
sourceRef: string;
|
||||||
|
targetRef: string;
|
||||||
|
netPrefix?: string;
|
||||||
|
pinOffset?: number;
|
||||||
|
}) => {
|
||||||
const result = await callKicadScript("connect_passthrough", args);
|
const result = await callKicadScript("connect_passthrough", args);
|
||||||
if (result.success !== false || (result.connected && result.connected.length > 0)) {
|
if (
|
||||||
|
result.success !== false ||
|
||||||
|
(result.connected && result.connected.length > 0)
|
||||||
|
) {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
if (result.connected?.length) lines.push(`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`);
|
if (result.connected?.length)
|
||||||
if (result.failed?.length) lines.push(`Failed (${result.failed.length}): ${result.failed.join(", ")}`);
|
lines.push(
|
||||||
|
`Connected (${result.connected.length}): ${result.connected.slice(0, 5).join(", ")}${result.connected.length > 5 ? " ..." : ""}`,
|
||||||
|
);
|
||||||
|
if (result.failed?.length)
|
||||||
|
lines.push(
|
||||||
|
`Failed (${result.failed.length}): ${result.failed.join(", ")}`,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: result.message + "\n" + lines.join("\n") }],
|
content: [
|
||||||
|
{ type: "text", text: result.message + "\n" + lines.join("\n") },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Passthrough failed: ${result.message || "Unknown error"}` }],
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Passthrough failed: ${result.message || "Unknown error"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -999,21 +1047,30 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
"run_erc",
|
"run_erc",
|
||||||
"Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.",
|
"Runs the KiCAD Electrical Rules Check (ERC) on a schematic and returns all violations. Use after wiring to verify the schematic before generating a netlist.",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Path to the .kicad_sch schematic file"),
|
schematicPath: z
|
||||||
|
.string()
|
||||||
|
.describe("Path to the .kicad_sch schematic file"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string }) => {
|
async (args: { schematicPath: string }) => {
|
||||||
const result = await callKicadScript("run_erc", args);
|
const result = await callKicadScript("run_erc", args);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const violations: any[] = result.violations || [];
|
const violations: any[] = result.violations || [];
|
||||||
const lines: string[] = [`ERC result: ${violations.length} violation(s)`];
|
const lines: string[] = [
|
||||||
|
`ERC result: ${violations.length} violation(s)`,
|
||||||
|
];
|
||||||
if (result.summary?.by_severity) {
|
if (result.summary?.by_severity) {
|
||||||
const s = result.summary.by_severity;
|
const s = result.summary.by_severity;
|
||||||
lines.push(` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`);
|
lines.push(
|
||||||
|
` Errors: ${s.error ?? 0} Warnings: ${s.warning ?? 0} Info: ${s.info ?? 0}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
violations.slice(0, 30).forEach((v: any, i: number) => {
|
violations.slice(0, 30).forEach((v: any, i: number) => {
|
||||||
const loc = v.location && (v.location.x !== undefined) ? ` @ (${v.location.x}, ${v.location.y})` : "";
|
const loc =
|
||||||
|
v.location && v.location.x !== undefined
|
||||||
|
? ` @ (${v.location.x}, ${v.location.y})`
|
||||||
|
: "";
|
||||||
lines.push(`${i + 1}. [${v.severity}] ${v.message}${loc}`);
|
lines.push(`${i + 1}. [${v.severity}] ${v.message}${loc}`);
|
||||||
});
|
});
|
||||||
if (violations.length > 30) {
|
if (violations.length > 30) {
|
||||||
@@ -1023,7 +1080,12 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}` }],
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `ERC failed: ${result.message || "Unknown error"}${result.errorDetails ? "\n" + result.errorDetails : ""}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1082,8 +1144,12 @@ Note: operates on .kicad_sch files only. To modify a PCB footprint use edit_comp
|
|||||||
"sync_schematic_to_board",
|
"sync_schematic_to_board",
|
||||||
"Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.",
|
"Import the schematic netlist into the PCB board — equivalent to pressing F8 in KiCAD (Tools → Update PCB from Schematic). MUST be called after the schematic is complete and before placing or routing components on the PCB. Without this step, the board has no footprints and no net assignments — place_component and route_pad_to_pad will produce an empty, unroutable board.",
|
||||||
{
|
{
|
||||||
schematicPath: z.string().describe("Absolute path to the .kicad_sch schematic file"),
|
schematicPath: z
|
||||||
boardPath: z.string().describe("Absolute path to the .kicad_pcb board file"),
|
.string()
|
||||||
|
.describe("Absolute path to the .kicad_sch schematic file"),
|
||||||
|
boardPath: z
|
||||||
|
.string()
|
||||||
|
.describe("Absolute path to the .kicad_pcb board file"),
|
||||||
},
|
},
|
||||||
async (args: { schematicPath: string; boardPath: string }) => {
|
async (args: { schematicPath: string; boardPath: string }) => {
|
||||||
const result = await callKicadScript("sync_schematic_to_board", args);
|
const result = await callKicadScript("sync_schematic_to_board", args);
|
||||||
|
|||||||
Reference in New Issue
Block a user