Merge pull request #80 from mixelpixx/fix/symbol-constants-and-paths
fix: symbol constants and hardcoded path cleanup
This commit is contained in:
@@ -220,14 +220,20 @@ class PinLocator:
|
|||||||
symbol_at = target_symbol.at.value
|
symbol_at = target_symbol.at.value
|
||||||
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
||||||
|
|
||||||
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
|
lib_id = (
|
||||||
|
target_symbol.lib_id.value if hasattr(target_symbol, "lib_id") else None
|
||||||
|
)
|
||||||
if not lib_id:
|
if not lib_id:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
pins = self.get_symbol_pins(schematic_path, lib_id)
|
pins = self.get_symbol_pins(schematic_path, lib_id)
|
||||||
if pin_number not in pins:
|
if pin_number not in pins:
|
||||||
matched_num = next(
|
matched_num = next(
|
||||||
(num for num, data in pins.items() if data.get("name") == pin_number),
|
(
|
||||||
|
num
|
||||||
|
for num, data in pins.items()
|
||||||
|
if data.get("name") == pin_number
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if matched_num:
|
if matched_num:
|
||||||
@@ -305,11 +311,17 @@ class PinLocator:
|
|||||||
if pin_number not in pins:
|
if pin_number not in pins:
|
||||||
# Try matching by pin name (e.g. "VCC1", "SDA", "GND")
|
# Try matching by pin name (e.g. "VCC1", "SDA", "GND")
|
||||||
matched_num = next(
|
matched_num = next(
|
||||||
(num for num, data in pins.items() if data.get("name") == pin_number),
|
(
|
||||||
|
num
|
||||||
|
for num, data in pins.items()
|
||||||
|
if data.get("name") == pin_number
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if matched_num:
|
if matched_num:
|
||||||
logger.debug(f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}")
|
logger.debug(
|
||||||
|
f"Resolved pin name '{pin_number}' to pin number '{matched_num}' on {symbol_reference}"
|
||||||
|
)
|
||||||
pin_number = matched_num
|
pin_number = matched_num
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -418,21 +430,23 @@ if __name__ == "__main__":
|
|||||||
# Test pin location discovery
|
# Test pin location discovery
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, "/home/chris/MCP/KiCAD-MCP-Server/python")
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from commands.component_schematic import ComponentManager
|
from commands.component_schematic import ComponentManager
|
||||||
from commands.schematic import SchematicManager
|
from commands.schematic import SchematicManager
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
print("PIN LOCATOR TEST")
|
print("PIN LOCATOR TEST")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
# Create test schematic with components (cross-platform temp directory)
|
# Create test schematic with components (cross-platform temp directory)
|
||||||
test_path = Path(tempfile.gettempdir()) / "test_pin_locator.kicad_sch"
|
test_path = Path(tempfile.gettempdir()) / "test_pin_locator.kicad_sch"
|
||||||
template_path = Path(
|
template_path = (
|
||||||
"/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch"
|
Path(__file__).parent.parent
|
||||||
|
/ "templates"
|
||||||
|
/ "template_with_symbols_expanded.kicad_sch"
|
||||||
)
|
)
|
||||||
|
|
||||||
shutil.copy(template_path, test_path)
|
shutil.copy(template_path, test_path)
|
||||||
|
|||||||
@@ -15,25 +15,32 @@ 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_AT = Symbol("at")
|
||||||
_SYM_WIDTH = Symbol('width')
|
_SYM_LABEL = Symbol("label")
|
||||||
_SYM_TYPE = Symbol('type')
|
_SYM_STROKE = Symbol("stroke")
|
||||||
_SYM_UUID = Symbol('uuid')
|
_SYM_WIDTH = Symbol("width")
|
||||||
_SYM_SHEET_INSTANCES = Symbol('sheet_instances')
|
_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(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 +56,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 +69,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 +93,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 +103,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 +132,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 +146,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 +170,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 +185,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 +212,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 +222,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 +252,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 +262,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 +308,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 +334,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,8 +364,12 @@ 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
|
||||||
|
)
|
||||||
|
seg_b = WireManager._make_wire_sexp(
|
||||||
|
[px, py], [x2, y2], stroke_width, stroke_type
|
||||||
|
)
|
||||||
sch_data[i : i + 1] = [seg_a, seg_b]
|
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
|
||||||
@@ -327,7 +379,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 +399,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 +413,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 +440,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 +450,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 +468,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 +476,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 +501,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 +511,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 +535,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 +544,48 @@ 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] == _SYM_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] == _SYM_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] == _SYM_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 +596,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(
|
||||||
|
schematic_path: Path,
|
||||||
|
net_name: str,
|
||||||
position: Optional[List[float]] = None,
|
position: Optional[List[float]] = None,
|
||||||
tolerance: float = 0.5) -> bool:
|
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 +620,15 @@ 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] == _SYM_LABEL
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Second element is the label text
|
# Second element is the label text
|
||||||
@@ -551,19 +638,24 @@ 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] == _SYM_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 +666,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,21 +702,22 @@ 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')
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
print("WIRE MANAGER TEST")
|
print("WIRE MANAGER TEST")
|
||||||
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(__file__).parent.parent / "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 +753,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:
|
||||||
|
|||||||
Reference in New Issue
Block a user