Files
kicad-mcp-server/python/commands/wire_manager.py
Leah Armstrong 101b4e1dad feat: add hierarchy tools (hierarchical label + sheet pin)
Two new tools for managing hierarchical schematic connections:

- add_schematic_hierarchical_label: create sheet interface ports on
  sub-sheet schematics. These are the sub-sheet side of hierarchical
  connections, linking to sheet pins on the parent.

- add_sheet_pin: add pins to sheet symbol blocks on the parent
  schematic. Targets the correct sheet by matching the Sheetname
  property. The pinName must match a hierarchical_label in the
  sub-sheet.

Both tools use text-based S-expression insertion (not sexpdata
round-trip) to preserve KiCad's native file formatting. Labels
include proper justification based on orientation: left-justify for
rightward labels (0°), right-justify for leftward labels (180°).

Wire manager additions:
- _find_insertion_point(): locates sheet_instances block or final paren
- _text_insert(): inserts formatted S-expression text at the right position
- _make_hierarchical_label_text(): generates hierarchical_label S-expression
- _make_sheet_pin_text(): generates sheet pin S-expression
- WireManager.add_hierarchical_label(): static method for label insertion
- WireManager.add_sheet_pin(): static method for pin insertion into
  named sheet blocks

12 unit tests covering insertion, orientation/justification mapping,
parameter validation, multi-sheet targeting, and error handling.
2026-04-15 12:32:09 -04:00

892 lines
32 KiB
Python

"""
Wire Manager for KiCad Schematics
Handles wire creation using S-expression manipulation, similar to dynamic symbol loading.
kicad-skip's wire API doesn't support creating wires with standard parameters, so we
manipulate the .kicad_sch file directly.
"""
import logging
import math
import re
import tempfile
import uuid
from pathlib import Path
from typing import Any, List, Optional, Tuple
import sexpdata
from sexpdata import Symbol
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_AT = Symbol("at")
_SYM_LABEL = Symbol("label")
_SYM_STROKE = Symbol("stroke")
_SYM_WIDTH = Symbol("width")
_SYM_TYPE = Symbol("type")
_SYM_UUID = Symbol("uuid")
_SYM_SHEET_INSTANCES = Symbol("sheet_instances")
def _find_insertion_point(content: str) -> int:
"""Find the right place to insert new elements in a .kicad_sch file.
Looks for (sheet_instances (KiCad 8) first, falls back to inserting
before the final closing paren (KiCad 9+).
"""
marker = "(sheet_instances"
pos = content.rfind(marker)
if pos != -1:
return pos
pos = content.rfind(")")
if pos == -1:
raise ValueError("Could not find insertion point in schematic")
return pos
def _text_insert(file_path: Path, sexp_text: str) -> bool:
"""Insert S-expression text into a .kicad_sch file preserving formatting."""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
insert_at = _find_insertion_point(content)
content = content[:insert_at] + sexp_text + content[insert_at:]
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return True
def _make_hierarchical_label_text(
text: str,
position: List[float],
shape: str = "bidirectional",
orientation: int = 0,
) -> str:
"""Generate a hierarchical_label S-expression as formatted text.
orientation: 0=right (label points right, justify left),
180=left (label points left, justify right),
90/270=vertical.
"""
uid = str(uuid.uuid4())
justify = "right" if orientation == 180 else "left"
return (
f'\t(hierarchical_label "{text}"\n'
f"\t\t(shape {shape})\n"
f"\t\t(at {position[0]} {position[1]} {orientation})\n"
f"\t\t(effects\n"
f"\t\t\t(font\n"
f"\t\t\t\t(size 1.27 1.27)\n"
f"\t\t\t)\n"
f"\t\t\t(justify {justify})\n"
f"\t\t)\n"
f'\t\t(uuid "{uid}")\n'
f"\t)\n"
)
def _make_sheet_pin_text(
pin_name: str,
pin_type: str,
position: List[float],
orientation: int = 0,
) -> str:
"""Generate a sheet pin S-expression as formatted text (indented for inside sheet block).
orientation: 0=right side of sheet box, 180=left side.
"""
uid = str(uuid.uuid4())
justify = "left" if orientation == 0 else "right"
return (
f'\t\t(pin "{pin_name}" {pin_type}\n'
f"\t\t\t(at {position[0]} {position[1]} {orientation})\n"
f'\t\t\t(uuid "{uid}")\n'
f"\t\t\t(effects\n"
f"\t\t\t\t(font\n"
f"\t\t\t\t\t(size 1.27 1.27)\n"
f"\t\t\t\t)\n"
f"\t\t\t\t(justify {justify})\n"
f"\t\t\t)\n"
f"\t\t)\n"
)
class WireManager:
"""Manage wires in KiCad schematics using S-expression manipulation"""
@staticmethod
def add_wire(
schematic_path: Path,
start_point: List[float],
end_point: List[float],
stroke_width: float = 0,
stroke_type: str = "default",
) -> bool:
"""
Add a wire to the schematic using S-expression manipulation
Args:
schematic_path: Path to .kicad_sch file
start_point: [x, y] coordinates for wire start
end_point: [x, y] coordinates for wire end
stroke_width: Wire width (default 0 for standard)
stroke_type: Stroke type (default, solid, dashed, etc.)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Break any existing wire that passes through a new endpoint (T-junction support)
for pt in (start_point, end_point):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new wire endpoint {pt}")
# Create wire S-expression
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
wire_sexp = WireManager._make_wire_sexp(
start_point, end_point, stroke_width, stroke_type
)
# Find insertion point (before sheet_instances)
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert wire before sheet_instances
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(f"Injected wire from {start_point} to {end_point}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added wire to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_polyline_wire(
schematic_path: Path,
points: List[List[float]],
stroke_width: float = 0,
stroke_type: str = "default",
) -> bool:
"""
Add a multi-segment wire (polyline) to the schematic
Args:
schematic_path: Path to .kicad_sch file
points: List of [x, y] coordinates for each point in the path
stroke_width: Wire width
stroke_type: Stroke type
Returns:
True if successful, False otherwise
"""
try:
if len(points) < 2:
logger.error("Polyline requires at least 2 points")
return False
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Break any existing wire at the outer endpoints of the new path
for pt in (points[0], points[-1]):
splits = WireManager._break_wires_at_point(sch_data, pt)
if splits:
logger.info(f"Broke {splits} wire(s) at new polyline endpoint {pt}")
# KiCAD wire elements only support exactly 2 pts each.
# Split N waypoints into N-1 individual wire segments.
wire_sexps = [
WireManager._make_wire_sexp(points[i], points[i + 1], stroke_width, stroke_type)
for i in range(len(points) - 1)
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert all segments (in reverse so order is preserved after inserts)
for wire_sexp in reversed(wire_sexps):
sch_data.insert(sheet_instances_index, wire_sexp)
logger.info(
f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline"
)
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added polyline wire to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding polyline wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_label(
schematic_path: Path,
text: str,
position: List[float],
label_type: str = "label",
orientation: int = 0,
) -> bool:
"""
Add a net label to the schematic
Args:
schematic_path: Path to .kicad_sch file
text: Label text (net name)
position: [x, y] coordinates for label
label_type: Type of label ('label', 'global_label', 'hierarchical_label')
orientation: Rotation angle (0, 90, 180, 270)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create label S-expression
# Format: (label "TEXT" (at x y angle) (effects (font (size 1.27 1.27))))
label_sexp = [
Symbol(label_type),
text,
[Symbol("at"), position[0], position[1], orientation],
[Symbol("fields_autoplaced"), Symbol("yes")],
[
Symbol("effects"),
[Symbol("font"), [Symbol("size"), 1.27, 1.27]],
[Symbol("justify"), Symbol("left"), Symbol("bottom")],
],
[Symbol("uuid"), str(uuid.uuid4())],
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert label
sch_data.insert(sheet_instances_index, label_sexp)
logger.info(f"Injected label '{text}' at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added label to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def _parse_wire(
wire_item: Any,
) -> Optional[Tuple[Tuple[float, float], Tuple[float, float], float, str]]:
"""
Parse a wire S-expression item in a single pass.
Returns ((x1,y1), (x2,y2), stroke_width, stroke_type), or None if not a valid wire.
"""
if not (isinstance(wire_item, list) and len(wire_item) >= 2 and wire_item[0] == _SYM_WIRE):
return None
start = end = None
stroke_width: float = 0
stroke_type: str = "default"
for part in wire_item[1:]:
if not isinstance(part, list) or not part:
continue
tag = part[0]
if tag == _SYM_PTS:
found: List[Tuple[float, float]] = []
for p in part[1:]:
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY:
found.append((float(p[1]), float(p[2])))
if len(found) == 2:
break
if len(found) == 2:
start, end = found[0], found[1]
elif tag == _SYM_STROKE:
for sp in part[1:]:
if isinstance(sp, list) and len(sp) >= 2:
if sp[0] == _SYM_WIDTH:
stroke_width = sp[1]
elif sp[0] == _SYM_TYPE:
stroke_type = str(sp[1])
if start is not None and end is not None:
return start, end, stroke_width, stroke_type
return None
@staticmethod
def _point_strictly_on_wire(
px: float,
py: float,
x1: float,
y1: float,
x2: float,
y2: float,
eps: float = 1e-6,
) -> bool:
"""
Return True if (px, py) lies strictly between (x1,y1) and (x2,y2)
on a horizontal or vertical wire segment (not at either endpoint).
"""
if abs(y1 - y2) < eps: # horizontal wire
if abs(py - y1) > eps:
return False
lo, hi = min(x1, x2), max(x1, x2)
return lo + eps < px < hi - eps
if abs(x1 - x2) < eps: # vertical wire
if abs(px - x1) > eps:
return False
lo, hi = min(y1, y2), max(y1, y2)
return lo + eps < py < hi - eps
return False
@staticmethod
def _make_wire_sexp(
start: List[float],
end: List[float],
stroke_width: float = 0,
stroke_type: str = "default",
) -> list:
return [
_SYM_WIRE,
[_SYM_PTS, [_SYM_XY, start[0], start[1]], [_SYM_XY, end[0], end[1]]],
[_SYM_STROKE, [_SYM_WIDTH, stroke_width], [_SYM_TYPE, Symbol(stroke_type)]],
[_SYM_UUID, str(uuid.uuid4())],
]
@staticmethod
def _break_wires_at_point(sch_data: list, position: List[float]) -> int:
"""
Split any wire segment that passes through *position* as a strict
midpoint (i.e. position is not an existing endpoint). Mirrors
KiCAD's SCH_LINE_WIRE_BUS_TOOL::BreakSegments behaviour.
Returns the number of wires split.
"""
px, py = float(position[0]), float(position[1])
splits = 0
i = 0
while i < len(sch_data):
parsed = WireManager._parse_wire(sch_data[i])
if parsed is not None:
(x1, y1), (x2, y2), stroke_width, stroke_type = parsed
if WireManager._point_strictly_on_wire(px, py, x1, y1, x2, y2):
seg_a = WireManager._make_wire_sexp(
[x1, y1], [px, py], stroke_width, stroke_type
)
seg_b = WireManager._make_wire_sexp(
[px, py], [x2, y2], stroke_width, stroke_type
)
sch_data[i : i + 1] = [seg_a, seg_b]
logger.info(f"Split wire ({x1},{y1})->({x2},{y2}) at ({px},{py})")
splits += 1
i += 2 # skip the two new segments
continue
i += 1
return splits
@staticmethod
def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
"""
Add a junction (connection dot) to the schematic.
Mirrors KiCAD's AddJunction behaviour: any wire whose interior passes
through *position* is split into two segments at that point so that
the BFS-based get_wire_connections tool can traverse the T/X branch.
Args:
schematic_path: Path to .kicad_sch file
position: [x, y] coordinates for junction
diameter: Junction diameter (0 for default)
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Split any wire that passes through the junction as a midpoint
# (mirrors KiCAD's AddJunction / BreakSegments behaviour)
splits = WireManager._break_wires_at_point(sch_data, position)
if splits:
logger.info(f"Broke {splits} wire(s) at junction position {position}")
# Create junction S-expression
# Format: (junction (at x y) (diameter 0) (color 0 0 0 0) (uuid ...))
junction_sexp = [
Symbol("junction"),
[Symbol("at"), position[0], position[1]],
[Symbol("diameter"), diameter],
[Symbol("color"), 0, 0, 0, 0],
[Symbol("uuid"), str(uuid.uuid4())],
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert junction
sch_data.insert(sheet_instances_index, junction_sexp)
logger.info(f"Injected junction at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added junction to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding junction: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_no_connect(schematic_path: Path, position: List[float]) -> bool:
"""
Add a no-connect flag to the schematic
Args:
schematic_path: Path to .kicad_sch file
position: [x, y] coordinates for no-connect flag
Returns:
True if successful, False otherwise
"""
try:
# Read schematic
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
# Create no_connect S-expression
# Format: (no_connect (at x y) (uuid ...))
no_connect_sexp = [
Symbol("no_connect"),
[Symbol("at"), position[0], position[1]],
[Symbol("uuid"), str(uuid.uuid4())],
]
# Find insertion point
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
logger.error("No sheet_instances section found in schematic")
return False
# Insert no_connect
sch_data.insert(sheet_instances_index, no_connect_sexp)
logger.info(f"Injected no-connect at {position}")
# Write back
with open(schematic_path, "w", encoding="utf-8") as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully added no-connect to {schematic_path.name}")
return True
except Exception as e:
logger.error(f"Error adding no-connect: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def delete_wire(
schematic_path: Path,
start_point: List[float],
end_point: List[float],
tolerance: float = 0.5,
) -> bool:
"""
Delete a wire from the schematic matching given start/end coordinates.
Args:
schematic_path: Path to .kicad_sch file
start_point: [x, y] coordinates for wire start
end_point: [x, y] coordinates for wire end
tolerance: Maximum coordinate difference to consider a match (mm)
Returns:
True if a wire was found and removed, False otherwise
"""
try:
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
sx, sy = start_point
ex, ey = end_point
for i, item in enumerate(sch_data):
if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_WIRE):
continue
# Extract pts from the wire s-expression
pts_list = None
for part in item[1:]:
if isinstance(part, list) and len(part) > 0 and part[0] == _SYM_PTS:
pts_list = part
break
if pts_list is None:
continue
xy_points = [
p
for p in pts_list[1:]
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_XY
]
if len(xy_points) < 2:
continue
x1, y1 = float(xy_points[0][1]), float(xy_points[0][2])
x2, y2 = float(xy_points[-1][1]), float(xy_points[-1][2])
match_fwd = (
abs(x1 - sx) < tolerance
and abs(y1 - sy) < tolerance
and abs(x2 - ex) < tolerance
and abs(y2 - ey) < tolerance
)
match_rev = (
abs(x1 - ex) < tolerance
and abs(y1 - ey) < tolerance
and abs(x2 - sx) < tolerance
and abs(y2 - sy) < tolerance
)
if match_fwd or match_rev:
del sch_data[i]
with open(schematic_path, "w", encoding="utf-8") as f:
f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted wire from {start_point} to {end_point}")
return True
logger.warning(f"No matching wire found for {start_point} to {end_point}")
return False
except Exception as e:
logger.error(f"Error deleting wire: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def delete_label(
schematic_path: Path,
net_name: str,
position: Optional[List[float]] = None,
tolerance: float = 0.5,
) -> bool:
"""
Delete a net label from the schematic by name (and optionally position).
Args:
schematic_path: Path to .kicad_sch file
net_name: Net label text to match
position: Optional [x, y] to disambiguate when multiple labels share a name
tolerance: Maximum coordinate difference to consider a match (mm)
Returns:
True if a label was found and removed, False otherwise
"""
try:
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
sch_data = sexpdata.loads(sch_content)
for i, item in enumerate(sch_data):
if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_LABEL):
continue
# Second element is the label text
if len(item) < 2 or item[1] != net_name:
continue
if position is not None:
# Find (at x y ...) sub-expression and check coordinates
at_entry = next(
(
p
for p in item[1:]
if isinstance(p, list) and len(p) >= 3 and p[0] == _SYM_AT
),
None,
)
if at_entry is None:
continue
lx, ly = float(at_entry[1]), float(at_entry[2])
if not (
abs(lx - position[0]) < tolerance and abs(ly - position[1]) < tolerance
):
continue
del sch_data[i]
with open(schematic_path, "w", encoding="utf-8") as f:
f.write(sexpdata.dumps(sch_data))
logger.info(f"Deleted label '{net_name}'")
return True
logger.warning(f"No matching label found for '{net_name}'")
return False
except Exception as e:
logger.error(f"Error deleting label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def create_orthogonal_path(
start: List[float], end: List[float], prefer_horizontal_first: bool = True
) -> List[List[float]]:
"""
Create an orthogonal (right-angle) path between two points
Args:
start: [x, y] start coordinates
end: [x, y] end coordinates
prefer_horizontal_first: If True, route horizontally first, else vertically first
Returns:
List of points defining the path: [start, corner, end]
"""
x1, y1 = start
x2, y2 = end
if prefer_horizontal_first:
# Route: start → (x2, y1) → end
corner = [x2, y1]
else:
# Route: start → (x1, y2) → end
corner = [x1, y2]
# If start and end are already aligned, return direct path
if x1 == x2 or y1 == y2:
return [start, end]
return [start, corner, end]
@staticmethod
def add_hierarchical_label(
schematic_path: Path,
text: str,
position: List[float],
shape: str = "bidirectional",
orientation: int = 0,
) -> bool:
"""Add a hierarchical label to a sub-sheet schematic."""
try:
label_text = _make_hierarchical_label_text(text, position, shape, orientation)
_text_insert(schematic_path, label_text)
logger.info(f"Added hierarchical_label '{text}' at {position} shape={shape}")
return True
except Exception as e:
logger.error(f"Error adding hierarchical label: {e}")
import traceback
logger.error(traceback.format_exc())
return False
@staticmethod
def add_sheet_pin(
content: str,
sheet_name: str,
pin_name: str,
pin_type: str,
position: List[float],
orientation: int = 0,
) -> Tuple[str, bool]:
"""Insert a sheet pin into the named sheet block in the parent schematic.
Returns (modified_content, success).
"""
lines = content.split("\n")
sheetname_pattern = re.compile(
r'\(property\s+"Sheetname"\s+"' + re.escape(sheet_name) + r'"'
)
sheet_block_pattern = re.compile(r"^\t\(sheet\b")
# Find the sheet block that contains the target Sheetname property
i = 0
while i < len(lines):
if sheet_block_pattern.match(lines[i]):
# Walk forward to find closing paren of this block
depth = sum(1 for c in lines[i] if c == "(") - sum(1 for c in lines[i] if c == ")")
j = i + 1
found_name = False
while j < len(lines) and depth > 0:
if sheetname_pattern.search(lines[j]):
found_name = True
depth += sum(1 for c in lines[j] if c == "(") - sum(
1 for c in lines[j] if c == ")"
)
j += 1
b_end = j - 1 # index of closing ")" line of the sheet block
if found_name:
# Insert pin text before the closing paren of the sheet block
pin_text = _make_sheet_pin_text(pin_name, pin_type, position, orientation)
pin_lines = pin_text.rstrip("\n").split("\n")
for offset, line in enumerate(pin_lines):
lines.insert(b_end + offset, line)
logger.info(f"Added sheet pin '{pin_name}' to sheet '{sheet_name}'")
return "\n".join(lines), True
i = b_end + 1
continue
i += 1
return content, False
if __name__ == "__main__":
# Test wire creation
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
print("=" * 80)
print("WIRE MANAGER TEST")
print("=" * 80)
# Create test schematic (cross-platform temp directory)
test_path = Path(tempfile.gettempdir()) / "test_wire_manager.kicad_sch"
template_path = Path(__file__).parent.parent / "templates" / "empty.kicad_sch"
shutil.copy(template_path, test_path)
print(f"\n✓ Created test schematic: {test_path}")
# Test 1: Add simple wire
print("\n[1/5] Testing simple wire creation...")
success = WireManager.add_wire(test_path, [50.8, 50.8], [101.6, 50.8])
print(f" {'' if success else ''} Simple wire: {success}")
# Test 2: Add orthogonal wire
print("\n[2/5] Testing orthogonal wire...")
path = WireManager.create_orthogonal_path([50.8, 60.96], [101.6, 88.9])
print(f" Orthogonal path: {path}")
success = WireManager.add_polyline_wire(test_path, path)
print(f" {'' if success else ''} Polyline wire: {success}")
# Test 3: Add label
print("\n[3/5] Testing label creation...")
success = WireManager.add_label(test_path, "VCC", [76.2, 50.8])
print(f" {'' if success else ''} Label: {success}")
# Test 4: Add junction
print("\n[4/5] Testing junction creation...")
success = WireManager.add_junction(test_path, [76.2, 50.8])
print(f" {'' if success else ''} Junction: {success}")
# Test 5: Add no-connect
print("\n[5/5] Testing no-connect creation...")
success = WireManager.add_no_connect(test_path, [127, 50.8])
print(f" {'' if success else ''} No-connect: {success}")
# Verify with kicad-skip
print("\n[Verification] Loading with kicad-skip...")
try:
from skip import Schematic
sch = Schematic(str(test_path))
wire_count = len(list(sch.wire)) if hasattr(sch, "wire") else 0
print(f" ✓ Loaded successfully")
print(f" ✓ Wire count: {wire_count}")
except Exception as e:
print(f" ✗ Failed: {e}")
print("\n" + "=" * 80)
print(f"Test schematic saved: {test_path}")
print("Open in KiCad to verify visual appearance!")
print("=" * 80)