""" 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 __