"""
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_GLOBAL_LABEL = Symbol("global_label")
_SYM_HIERARCHICAL_LABEL = Symbol("hierarchical_label")
_SYM_STROKE = Symbol("stroke")
_SYM_WIDTH = Symbol("width")
_SYM_TYPE = Symbol("type")
_SYM_UUID = Symbol("uuid")
_SYM_SHEET_INSTANCES = Symbol("sheet_instances")
_SYM_JUNCTION = Symbol("junction")
_SYM_LIB_SYMBOLS = Symbol("lib_symbols")
_SYM_LIB_ID = Symbol("lib_id")
_SYM_MIRROR = Symbol("mirror")
_SYM_PIN = Symbol("pin")
_SYM_SYMBOL = Symbol("symbol")
_SYM_UNIT = Symbol("unit")
_IU_PER_MM = 10000
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"""
# Regex to parse sub-unit names like "LM324_2_1" → (base="LM324", unit=2, style=1)
# The sub-unit suffix is __