diff --git a/.gitignore b/.gitignore index 21b91f1..c13e2ca 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ Desktop.ini # Generated local config files (contain machine-specific paths) windows-mcp-config.json +.mcp.json # Personal notes / local contributions (not for upstream) myContribution/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d00ab29..d251130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +> **⚠️ Research Project — No Stability Guarantees** +> This is an unstable, experimental research project. APIs, tool names, parameters, +> and file formats may change at any time without notice. Back-compatibility is +> explicitly **not** preserved between versions. + All notable changes to the KiCAD MCP Server project are documented here. ## [Unreleased] @@ -71,6 +76,14 @@ All notable changes to the KiCAD MCP Server project are documented here. no-op removal, special-character escaping, UUID preservation, and the two new convenience tools. +### Removed + +- `add_schematic_junction` MCP tool has been removed. Junctions are now + inserted and removed automatically via `WireManager.sync_junctions` whenever + wires are added, deleted, or moved. +- Junction placement is pin-aware: `sync_junctions` consults component pin + positions so that T-junctions at component pins are correctly recognised. + --- ## [2.2.3] - 2026-03-11 diff --git a/python/commands/wire_manager.py b/python/commands/wire_manager.py index f52662d..20c3043 100644 --- a/python/commands/wire_manager.py +++ b/python/commands/wire_manager.py @@ -32,6 +32,12 @@ _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") +_IU_PER_MM = 10000 def _find_insertion_point(content: str) -> int: @@ -176,6 +182,8 @@ class WireManager: sch_data.insert(sheet_instances_index, wire_sexp) logger.info(f"Injected wire from {start_point} to {end_point}") + WireManager.sync_junctions(sch_data) + # Write back with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) @@ -252,6 +260,8 @@ class WireManager: f"Injected {len(wire_sexps)} wire segments for {len(points)}-point polyline" ) + WireManager.sync_junctions(sch_data) + # Write back with open(schematic_path, "w", encoding="utf-8") as f: output = sexpdata.dumps(sch_data) @@ -450,74 +460,266 @@ class WireManager: return splits @staticmethod - def add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool: + def _collect_wire_endpoints(sch_data: list) -> List[Tuple[float, float]]: + """Return all (x, y) endpoints for every wire in sch_data.""" + endpoints: List[Tuple[float, float]] = [] + for item in sch_data: + parsed = WireManager._parse_wire(item) + if parsed is not None: + (x1, y1), (x2, y2), _, _ = parsed + endpoints.append((x1, y1)) + endpoints.append((x2, y2)) + return endpoints + + @staticmethod + def _get_existing_junctions(sch_data: list) -> dict: + """Return {(iu_x, iu_y): index_in_sch_data} for every junction element.""" + result: dict = {} + for i, item in enumerate(sch_data): + if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_JUNCTION): + continue + 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 + x, y = float(at_entry[1]), float(at_entry[2]) + result[(round(x * _IU_PER_MM), round(y * _IU_PER_MM))] = i + return result + + @staticmethod + def _make_junction_sexp(x: float, y: float, diameter: float = 0) -> list: + return [ + _SYM_JUNCTION, + [_SYM_AT, x, y], + [Symbol("diameter"), diameter], + [Symbol("color"), 0, 0, 0, 0], + [_SYM_UUID, str(uuid.uuid4())], + ] + + # Regex to parse sub-unit names like "LM324_2_1" → (base="LM324", unit=2, style=1) + # The sub-unit suffix is __