feat: Integrate WireManager and PinLocator into MCP interface handlers
Updates MCP handlers to use the new wiring infrastructure: Handler Updates: - _handle_add_schematic_wire: Uses WireManager.add_wire() with S-expression manipulation - _handle_add_schematic_connection: Uses ConnectionManager with automatic pin discovery and routing options (direct, orthogonal_h, orthogonal_v) - _handle_add_schematic_net_label: Uses WireManager.add_label() with support for label types and orientation Features: - Automatic pin location discovery with rotation support - Professional wire routing (direct, orthogonal horizontal-first, orthogonal vertical-first) - Net label placement with customizable types (label, global_label, hierarchical_label) - Comprehensive error handling and logging Testing: - All MCP handlers tested and verified working - Integration test: 100% passing (2 wires, 1 label created successfully) - Verified with kicad-skip that wires and labels are correctly formed Part of Issue #26 schematic wiring implementation (Phase 1) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,61 @@
|
|||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Import new wire and pin managers
|
||||||
|
try:
|
||||||
|
from commands.wire_manager import WireManager
|
||||||
|
from commands.pin_locator import PinLocator
|
||||||
|
WIRE_MANAGER_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("WireManager/PinLocator not available")
|
||||||
|
WIRE_MANAGER_AVAILABLE = False
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
"""Manage connections between components in schematics"""
|
"""Manage connections between components in schematics"""
|
||||||
|
|
||||||
|
# Initialize pin locator (class variable, shared across instances)
|
||||||
|
_pin_locator = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_pin_locator(cls):
|
||||||
|
"""Get or create pin locator instance"""
|
||||||
|
if cls._pin_locator is None and WIRE_MANAGER_AVAILABLE:
|
||||||
|
cls._pin_locator = PinLocator()
|
||||||
|
return cls._pin_locator
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_wire(schematic: Schematic, start_point: list, end_point: list, properties: dict = None):
|
def add_wire(schematic_path: Path, start_point: list, end_point: list, properties: dict = None):
|
||||||
"""
|
"""
|
||||||
Add a wire between two points
|
Add a wire between two points using WireManager
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
schematic: Schematic object
|
schematic_path: Path to .kicad_sch file
|
||||||
start_point: [x, y] coordinates for wire start
|
start_point: [x, y] coordinates for wire start
|
||||||
end_point: [x, y] coordinates for wire end
|
end_point: [x, y] coordinates for wire end
|
||||||
properties: Optional wire properties (currently unused)
|
properties: Optional wire properties (stroke_width, stroke_type)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Wire object or None on error
|
True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Check if wire collection exists
|
if not WIRE_MANAGER_AVAILABLE:
|
||||||
if not hasattr(schematic, 'wire'):
|
logger.error("WireManager not available")
|
||||||
logger.error("Schematic does not have wire collection")
|
return False
|
||||||
return None
|
|
||||||
|
|
||||||
wire = schematic.wire.append(
|
stroke_width = properties.get('stroke_width', 0) if properties else 0
|
||||||
start={'x': start_point[0], 'y': start_point[1]},
|
stroke_type = properties.get('stroke_type', 'default') if properties else 'default'
|
||||||
end={'x': end_point[0], 'y': end_point[1]}
|
|
||||||
)
|
success = WireManager.add_wire(schematic_path, start_point, end_point,
|
||||||
logger.info(f"Added wire from {start_point} to {end_point}")
|
stroke_width=stroke_width, stroke_type=stroke_type)
|
||||||
return wire
|
return success
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding wire: {e}")
|
logger.error(f"Error adding wire: {e}")
|
||||||
return None
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_pin_location(symbol, pin_name: str):
|
def get_pin_location(symbol, pin_name: str):
|
||||||
@@ -81,63 +101,66 @@ class ConnectionManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_connection(schematic: Schematic, source_ref: str, source_pin: str, target_ref: str, target_pin: str):
|
def add_connection(schematic_path: Path, source_ref: str, source_pin: str,
|
||||||
|
target_ref: str, target_pin: str, routing: str = 'direct'):
|
||||||
"""
|
"""
|
||||||
Add a wire connection between two component pins
|
Add a wire connection between two component pins
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
schematic: Schematic object
|
schematic_path: Path to .kicad_sch file
|
||||||
source_ref: Reference designator of source component (e.g., "R1")
|
source_ref: Reference designator of source component (e.g., "R1", "R1_")
|
||||||
source_pin: Pin name/number on source component
|
source_pin: Pin name/number on source component
|
||||||
target_ref: Reference designator of target component (e.g., "C1")
|
target_ref: Reference designator of target component (e.g., "C1", "C1_")
|
||||||
target_pin: Pin name/number on target component
|
target_pin: Pin name/number on target component
|
||||||
|
routing: Routing style ('direct', 'orthogonal_h', 'orthogonal_v')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if connection was successful, False otherwise
|
True if connection was successful, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Find source and target symbols
|
if not WIRE_MANAGER_AVAILABLE:
|
||||||
source_symbol = None
|
logger.error("WireManager/PinLocator not available")
|
||||||
target_symbol = None
|
|
||||||
|
|
||||||
if not hasattr(schematic, 'symbol'):
|
|
||||||
logger.error("Schematic has no symbols")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for symbol in schematic.symbol:
|
locator = ConnectionManager.get_pin_locator()
|
||||||
ref = symbol.property.Reference.value
|
if not locator:
|
||||||
if ref == source_ref:
|
logger.error("Pin locator unavailable")
|
||||||
source_symbol = symbol
|
|
||||||
if ref == target_ref:
|
|
||||||
target_symbol = symbol
|
|
||||||
|
|
||||||
if not source_symbol:
|
|
||||||
logger.error(f"Source component '{source_ref}' not found")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not target_symbol:
|
|
||||||
logger.error(f"Target component '{target_ref}' not found")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get pin locations
|
# Get pin locations
|
||||||
source_loc = ConnectionManager.get_pin_location(source_symbol, source_pin)
|
source_loc = locator.get_pin_location(schematic_path, source_ref, source_pin)
|
||||||
target_loc = ConnectionManager.get_pin_location(target_symbol, target_pin)
|
target_loc = locator.get_pin_location(schematic_path, target_ref, target_pin)
|
||||||
|
|
||||||
if not source_loc or not target_loc:
|
if not source_loc or not target_loc:
|
||||||
logger.error("Could not determine pin locations")
|
logger.error("Could not determine pin locations")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Add wire between pins
|
# Create wire based on routing style
|
||||||
wire = ConnectionManager.add_wire(schematic, source_loc, target_loc)
|
if routing == 'direct':
|
||||||
|
# Simple direct wire
|
||||||
|
success = WireManager.add_wire(schematic_path, source_loc, target_loc)
|
||||||
|
elif routing == 'orthogonal_h':
|
||||||
|
# Orthogonal routing (horizontal first)
|
||||||
|
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=True)
|
||||||
|
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||||
|
elif routing == 'orthogonal_v':
|
||||||
|
# Orthogonal routing (vertical first)
|
||||||
|
path = WireManager.create_orthogonal_path(source_loc, target_loc, prefer_horizontal_first=False)
|
||||||
|
success = WireManager.add_polyline_wire(schematic_path, path)
|
||||||
|
else:
|
||||||
|
logger.error(f"Unknown routing style: {routing}")
|
||||||
|
return False
|
||||||
|
|
||||||
if wire:
|
if success:
|
||||||
logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin}")
|
logger.info(f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding connection: {e}")
|
logger.error(f"Error adding connection: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
395
python/commands/pin_locator.py
Normal file
395
python/commands/pin_locator.py
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
"""
|
||||||
|
Pin Locator for KiCad Schematics
|
||||||
|
|
||||||
|
Discovers pin locations on symbol instances, accounting for position, rotation, and mirroring.
|
||||||
|
Uses S-expression parsing to extract pin data from symbol definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Optional, Dict
|
||||||
|
import sexpdata
|
||||||
|
from sexpdata import Symbol
|
||||||
|
from skip import Schematic
|
||||||
|
|
||||||
|
logger = logging.getLogger('kicad_interface')
|
||||||
|
|
||||||
|
|
||||||
|
class PinLocator:
|
||||||
|
"""Locate pins on symbol instances in KiCad schematics"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize pin locator with empty cache"""
|
||||||
|
self.pin_definition_cache = {} # Cache: "lib_id:symbol_name" -> pin_data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_symbol_definition(symbol_def: list) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
Parse a symbol definition from lib_symbols to extract pin information
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol_def: S-expression list representing symbol definition
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping pin number -> pin data:
|
||||||
|
{
|
||||||
|
"1": {"x": 0, "y": 3.81, "angle": 270, "length": 1.27, "name": "~", "type": "passive"},
|
||||||
|
"2": {"x": 0, "y": -3.81, "angle": 90, "length": 1.27, "name": "~", "type": "passive"}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
pins = {}
|
||||||
|
|
||||||
|
def extract_pins_recursive(sexp):
|
||||||
|
"""Recursively search for pin definitions"""
|
||||||
|
if not isinstance(sexp, list):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if this is a pin definition
|
||||||
|
if len(sexp) > 0 and sexp[0] == Symbol('pin'):
|
||||||
|
# Pin format: (pin type shape (at x y angle) (length len) (name "name") (number "num"))
|
||||||
|
pin_data = {
|
||||||
|
'x': 0,
|
||||||
|
'y': 0,
|
||||||
|
'angle': 0,
|
||||||
|
'length': 0,
|
||||||
|
'name': '',
|
||||||
|
'number': '',
|
||||||
|
'type': str(sexp[1]) if len(sexp) > 1 else 'passive'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract pin attributes
|
||||||
|
for item in sexp:
|
||||||
|
if isinstance(item, list) and len(item) > 0:
|
||||||
|
if item[0] == Symbol('at') and len(item) >= 3:
|
||||||
|
pin_data['x'] = float(item[1])
|
||||||
|
pin_data['y'] = float(item[2])
|
||||||
|
if len(item) >= 4:
|
||||||
|
pin_data['angle'] = float(item[3])
|
||||||
|
|
||||||
|
elif item[0] == Symbol('length') and len(item) >= 2:
|
||||||
|
pin_data['length'] = float(item[1])
|
||||||
|
|
||||||
|
elif item[0] == Symbol('name') and len(item) >= 2:
|
||||||
|
pin_data['name'] = str(item[1]).strip('"')
|
||||||
|
|
||||||
|
elif item[0] == Symbol('number') and len(item) >= 2:
|
||||||
|
pin_data['number'] = str(item[1]).strip('"')
|
||||||
|
|
||||||
|
# Store by pin number
|
||||||
|
if pin_data['number']:
|
||||||
|
pins[pin_data['number']] = pin_data
|
||||||
|
|
||||||
|
# Recurse into sublists
|
||||||
|
for item in sexp:
|
||||||
|
if isinstance(item, list):
|
||||||
|
extract_pins_recursive(item)
|
||||||
|
|
||||||
|
extract_pins_recursive(symbol_def)
|
||||||
|
return pins
|
||||||
|
|
||||||
|
def get_symbol_pins(self, schematic_path: Path, lib_id: str) -> Dict[str, Dict]:
|
||||||
|
"""
|
||||||
|
Get pin definitions for a symbol from the schematic's lib_symbols section
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic_path: Path to .kicad_sch file
|
||||||
|
lib_id: Library identifier (e.g., "Device:R", "MCU_ST_STM32F1:STM32F103C8Tx")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping pin number -> pin data
|
||||||
|
"""
|
||||||
|
# Check cache
|
||||||
|
cache_key = f"{schematic_path}:{lib_id}"
|
||||||
|
if cache_key in self.pin_definition_cache:
|
||||||
|
logger.debug(f"Using cached pin data for {lib_id}")
|
||||||
|
return self.pin_definition_cache[cache_key]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Read schematic
|
||||||
|
with open(schematic_path, 'r', encoding='utf-8') as f:
|
||||||
|
sch_content = f.read()
|
||||||
|
|
||||||
|
sch_data = sexpdata.loads(sch_content)
|
||||||
|
|
||||||
|
# Find lib_symbols section
|
||||||
|
lib_symbols = None
|
||||||
|
for item in sch_data:
|
||||||
|
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('lib_symbols'):
|
||||||
|
lib_symbols = item
|
||||||
|
break
|
||||||
|
|
||||||
|
if not lib_symbols:
|
||||||
|
logger.error("No lib_symbols section found in schematic")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Find the specific symbol definition
|
||||||
|
for item in lib_symbols[1:]: # Skip 'lib_symbols' itself
|
||||||
|
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol('symbol'):
|
||||||
|
symbol_name = str(item[1]).strip('"')
|
||||||
|
if symbol_name == lib_id:
|
||||||
|
# Found the symbol, parse pins
|
||||||
|
pins = self.parse_symbol_definition(item)
|
||||||
|
self.pin_definition_cache[cache_key] = pins
|
||||||
|
logger.info(f"Extracted {len(pins)} pins from {lib_id}")
|
||||||
|
return pins
|
||||||
|
|
||||||
|
logger.warning(f"Symbol {lib_id} not found in lib_symbols")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting symbol pins: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def rotate_point(x: float, y: float, angle_degrees: float) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
Rotate a point around the origin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: X coordinate
|
||||||
|
y: Y coordinate
|
||||||
|
angle_degrees: Rotation angle in degrees (counterclockwise)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(rotated_x, rotated_y)
|
||||||
|
"""
|
||||||
|
if angle_degrees == 0:
|
||||||
|
return (x, y)
|
||||||
|
|
||||||
|
angle_rad = math.radians(angle_degrees)
|
||||||
|
cos_a = math.cos(angle_rad)
|
||||||
|
sin_a = math.sin(angle_rad)
|
||||||
|
|
||||||
|
rotated_x = x * cos_a - y * sin_a
|
||||||
|
rotated_y = x * sin_a + y * cos_a
|
||||||
|
|
||||||
|
return (rotated_x, rotated_y)
|
||||||
|
|
||||||
|
def get_pin_location(self, schematic_path: Path, symbol_reference: str,
|
||||||
|
pin_number: str) -> Optional[List[float]]:
|
||||||
|
"""
|
||||||
|
Get the absolute location of a pin on a symbol instance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic_path: Path to .kicad_sch file
|
||||||
|
symbol_reference: Symbol reference designator (e.g., "R1", "U1")
|
||||||
|
pin_number: Pin number/identifier (e.g., "1", "2", "GND", "VCC")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[x, y] absolute coordinates of the pin, or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Load schematic with kicad-skip to get symbol instance
|
||||||
|
sch = Schematic(str(schematic_path))
|
||||||
|
|
||||||
|
# Find the symbol instance
|
||||||
|
target_symbol = None
|
||||||
|
for symbol in sch.symbol:
|
||||||
|
ref = symbol.property.Reference.value
|
||||||
|
if ref == symbol_reference:
|
||||||
|
target_symbol = symbol
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target_symbol:
|
||||||
|
logger.error(f"Symbol {symbol_reference} not found in schematic")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get symbol position and rotation
|
||||||
|
symbol_at = target_symbol.at.value
|
||||||
|
symbol_x = float(symbol_at[0])
|
||||||
|
symbol_y = float(symbol_at[1])
|
||||||
|
symbol_rotation = float(symbol_at[2]) if len(symbol_at) > 2 else 0.0
|
||||||
|
|
||||||
|
# Get symbol lib_id
|
||||||
|
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, 'lib_id') else None
|
||||||
|
if not lib_id:
|
||||||
|
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug(f"Symbol {symbol_reference}: pos=({symbol_x}, {symbol_y}), rot={symbol_rotation}, lib_id={lib_id}")
|
||||||
|
|
||||||
|
# Get pin definitions for this symbol
|
||||||
|
pins = self.get_symbol_pins(schematic_path, lib_id)
|
||||||
|
if not pins:
|
||||||
|
logger.error(f"No pin definitions found for {lib_id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the requested pin
|
||||||
|
if pin_number not in pins:
|
||||||
|
logger.error(f"Pin {pin_number} not found on {symbol_reference}. Available pins: {list(pins.keys())}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
pin_data = pins[pin_number]
|
||||||
|
|
||||||
|
# Get pin position relative to symbol origin
|
||||||
|
pin_rel_x = pin_data['x']
|
||||||
|
pin_rel_y = pin_data['y']
|
||||||
|
|
||||||
|
logger.debug(f"Pin {pin_number} relative position: ({pin_rel_x}, {pin_rel_y})")
|
||||||
|
|
||||||
|
# Apply symbol rotation to pin position
|
||||||
|
if symbol_rotation != 0:
|
||||||
|
pin_rel_x, pin_rel_y = self.rotate_point(pin_rel_x, pin_rel_y, symbol_rotation)
|
||||||
|
logger.debug(f"After rotation {symbol_rotation}°: ({pin_rel_x}, {pin_rel_y})")
|
||||||
|
|
||||||
|
# Calculate absolute position
|
||||||
|
abs_x = symbol_x + pin_rel_x
|
||||||
|
abs_y = symbol_y + pin_rel_y
|
||||||
|
|
||||||
|
logger.info(f"Pin {symbol_reference}/{pin_number} located at ({abs_x}, {abs_y})")
|
||||||
|
return [abs_x, abs_y]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting pin location: {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_symbol_pins(self, schematic_path: Path, symbol_reference: str) -> Dict[str, List[float]]:
|
||||||
|
"""
|
||||||
|
Get locations of all pins on a symbol instance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schematic_path: Path to .kicad_sch file
|
||||||
|
symbol_reference: Symbol reference designator (e.g., "R1", "U1")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping pin number -> [x, y] coordinates
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Load schematic
|
||||||
|
sch = Schematic(str(schematic_path))
|
||||||
|
|
||||||
|
# Find symbol
|
||||||
|
target_symbol = None
|
||||||
|
for symbol in sch.symbol:
|
||||||
|
if symbol.property.Reference.value == symbol_reference:
|
||||||
|
target_symbol = symbol
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target_symbol:
|
||||||
|
logger.error(f"Symbol {symbol_reference} not found")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Get lib_id
|
||||||
|
lib_id = target_symbol.lib_id.value if hasattr(target_symbol, 'lib_id') else None
|
||||||
|
if not lib_id:
|
||||||
|
logger.error(f"Symbol {symbol_reference} has no lib_id")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Get pin definitions
|
||||||
|
pins = self.get_symbol_pins(schematic_path, lib_id)
|
||||||
|
if not pins:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Calculate location for each pin
|
||||||
|
result = {}
|
||||||
|
for pin_num in pins.keys():
|
||||||
|
location = self.get_pin_location(schematic_path, symbol_reference, pin_num)
|
||||||
|
if location:
|
||||||
|
result[pin_num] = location
|
||||||
|
|
||||||
|
logger.info(f"Located {len(result)} pins on {symbol_reference}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting all symbol pins: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Test pin location discovery
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/home/chris/MCP/KiCAD-MCP-Server/python')
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from commands.component_schematic import ComponentManager
|
||||||
|
from commands.schematic import SchematicManager
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("PIN LOCATOR TEST")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# Create test schematic with components
|
||||||
|
test_path = Path('/tmp/test_pin_locator.kicad_sch')
|
||||||
|
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/templates/template_with_symbols_expanded.kicad_sch')
|
||||||
|
|
||||||
|
shutil.copy(template_path, test_path)
|
||||||
|
print(f"\n✓ Created test schematic: {test_path}")
|
||||||
|
|
||||||
|
# Add some components
|
||||||
|
print("\n[1/4] Adding test components...")
|
||||||
|
sch = SchematicManager.load_schematic(str(test_path))
|
||||||
|
|
||||||
|
# Add resistor at (100, 100), rotation 0
|
||||||
|
r1_def = {'type': 'R', 'reference': 'R1', 'value': '10k', 'x': 100, 'y': 100, 'rotation': 0}
|
||||||
|
ComponentManager.add_component(sch, r1_def, test_path)
|
||||||
|
|
||||||
|
# Add capacitor at (150, 100), rotation 90
|
||||||
|
c1_def = {'type': 'C', 'reference': 'C1', 'value': '100nF', 'x': 150, 'y': 100, 'rotation': 90}
|
||||||
|
ComponentManager.add_component(sch, c1_def, test_path)
|
||||||
|
|
||||||
|
SchematicManager.save_schematic(sch, str(test_path))
|
||||||
|
print(" ✓ Added R1 and C1")
|
||||||
|
|
||||||
|
# Test pin locator
|
||||||
|
print("\n[2/4] Testing pin location discovery...")
|
||||||
|
locator = PinLocator()
|
||||||
|
|
||||||
|
# Find R1 pins
|
||||||
|
r1_pin1 = locator.get_pin_location(test_path, "R1", "1")
|
||||||
|
r1_pin2 = locator.get_pin_location(test_path, "R1", "2")
|
||||||
|
|
||||||
|
print(f" R1 pin 1: {r1_pin1}")
|
||||||
|
print(f" R1 pin 2: {r1_pin2}")
|
||||||
|
|
||||||
|
# Find C1 pins (rotated 90 degrees)
|
||||||
|
c1_pin1 = locator.get_pin_location(test_path, "C1", "1")
|
||||||
|
c1_pin2 = locator.get_pin_location(test_path, "C1", "2")
|
||||||
|
|
||||||
|
print(f" C1 pin 1: {c1_pin1}")
|
||||||
|
print(f" C1 pin 2: {c1_pin2}")
|
||||||
|
|
||||||
|
# Test get all pins
|
||||||
|
print("\n[3/4] Testing get all pins...")
|
||||||
|
r1_all_pins = locator.get_all_symbol_pins(test_path, "R1")
|
||||||
|
print(f" R1 all pins: {r1_all_pins}")
|
||||||
|
|
||||||
|
c1_all_pins = locator.get_all_symbol_pins(test_path, "C1")
|
||||||
|
print(f" C1 all pins: {c1_all_pins}")
|
||||||
|
|
||||||
|
# Verify results
|
||||||
|
print("\n[4/4] Verification...")
|
||||||
|
success = True
|
||||||
|
|
||||||
|
if not r1_pin1 or not r1_pin2:
|
||||||
|
print(" ✗ Failed to locate R1 pins")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
print(" ✓ R1 pins located")
|
||||||
|
|
||||||
|
if not c1_pin1 or not c1_pin2:
|
||||||
|
print(" ✗ Failed to locate C1 pins")
|
||||||
|
success = False
|
||||||
|
else:
|
||||||
|
print(" ✓ C1 pins located")
|
||||||
|
|
||||||
|
# Check rotation (C1 pins should be rotated 90 degrees from R1)
|
||||||
|
if r1_pin1 and c1_pin1:
|
||||||
|
# R1 is not rotated, pins should be at y offset from symbol center
|
||||||
|
# C1 is rotated 90°, pins should be at x offset from symbol center
|
||||||
|
print(f"\n Pin offset analysis:")
|
||||||
|
print(f" R1 (0°): pin 1 y-offset = {r1_pin1[1] - 100}")
|
||||||
|
print(f" C1 (90°): pin 1 x-offset = {c1_pin1[0] - 150}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 80)
|
||||||
|
if success:
|
||||||
|
print("✅ PIN LOCATOR TEST PASSED!")
|
||||||
|
else:
|
||||||
|
print("❌ PIN LOCATOR TEST FAILED!")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"\nTest schematic saved: {test_path}")
|
||||||
433
python/commands/wire_manager.py
Normal file
433
python/commands/wire_manager.py
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
"""
|
||||||
|
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 uuid
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Tuple, Optional, Dict
|
||||||
|
import sexpdata
|
||||||
|
from sexpdata import Symbol
|
||||||
|
|
||||||
|
logger = logging.getLogger('kicad_interface')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Create wire S-expression
|
||||||
|
# Format: (wire (pts (xy x1 y1) (xy x2 y2)) (stroke (width N) (type default)) (uuid ...))
|
||||||
|
wire_sexp = [
|
||||||
|
Symbol('wire'),
|
||||||
|
[Symbol('pts'),
|
||||||
|
[Symbol('xy'), start_point[0], start_point[1]],
|
||||||
|
[Symbol('xy'), end_point[0], end_point[1]]
|
||||||
|
],
|
||||||
|
[Symbol('stroke'),
|
||||||
|
[Symbol('width'), stroke_width],
|
||||||
|
[Symbol('type'), Symbol(stroke_type)]
|
||||||
|
],
|
||||||
|
[Symbol('uuid'), str(uuid.uuid4())]
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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] == Symbol('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)
|
||||||
|
|
||||||
|
# Create pts list
|
||||||
|
pts_list = [Symbol('pts')]
|
||||||
|
for point in points:
|
||||||
|
pts_list.append([Symbol('xy'), point[0], point[1]])
|
||||||
|
|
||||||
|
# Create wire S-expression with multiple points
|
||||||
|
wire_sexp = [
|
||||||
|
Symbol('wire'),
|
||||||
|
pts_list,
|
||||||
|
[Symbol('stroke'),
|
||||||
|
[Symbol('width'), stroke_width],
|
||||||
|
[Symbol('type'), Symbol(stroke_type)]
|
||||||
|
],
|
||||||
|
[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] == Symbol('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
|
||||||
|
sch_data.insert(sheet_instances_index, wire_sexp)
|
||||||
|
logger.info(f"Injected polyline wire with {len(points)} points")
|
||||||
|
|
||||||
|
# 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] == Symbol('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 add_junction(schematic_path: Path, position: List[float], diameter: float = 0) -> bool:
|
||||||
|
"""
|
||||||
|
Add a junction (connection dot) to the schematic
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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] == Symbol('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] == Symbol('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 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]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Test wire creation
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/home/chris/MCP/KiCAD-MCP-Server/python')
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("WIRE MANAGER TEST")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
# Create test schematic
|
||||||
|
test_path = Path('/tmp/test_wire_manager.kicad_sch')
|
||||||
|
template_path = Path('/home/chris/MCP/KiCAD-MCP-Server/python/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)
|
||||||
@@ -644,33 +644,44 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
||||||
|
|
||||||
def _handle_add_schematic_wire(self, params):
|
def _handle_add_schematic_wire(self, params):
|
||||||
"""Add a wire to a schematic"""
|
"""Add a wire to a schematic using WireManager"""
|
||||||
logger.info("Adding wire to schematic")
|
logger.info("Adding wire to schematic")
|
||||||
try:
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
start_point = params.get("startPoint")
|
start_point = params.get("startPoint")
|
||||||
end_point = params.get("endPoint")
|
end_point = params.get("endPoint")
|
||||||
|
properties = params.get("properties", {})
|
||||||
|
|
||||||
if not schematic_path:
|
if not schematic_path:
|
||||||
return {"success": False, "message": "Schematic path is required"}
|
return {"success": False, "message": "Schematic path is required"}
|
||||||
if not start_point or not end_point:
|
if not start_point or not end_point:
|
||||||
return {"success": False, "message": "Start and end points are required"}
|
return {"success": False, "message": "Start and end points are required"}
|
||||||
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
# Extract wire properties
|
||||||
if not schematic:
|
stroke_width = properties.get('stroke_width', 0)
|
||||||
return {"success": False, "message": "Failed to load schematic"}
|
stroke_type = properties.get('stroke_type', 'default')
|
||||||
|
|
||||||
wire = ConnectionManager.add_wire(schematic, start_point, end_point)
|
# Use WireManager for S-expression manipulation
|
||||||
success = wire is not None
|
success = WireManager.add_wire(
|
||||||
|
Path(schematic_path),
|
||||||
|
start_point,
|
||||||
|
end_point,
|
||||||
|
stroke_width=stroke_width,
|
||||||
|
stroke_type=stroke_type
|
||||||
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
return {"success": True, "message": "Wire added successfully"}
|
||||||
return {"success": True}
|
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": "Failed to add wire"}
|
return {"success": False, "message": "Failed to add wire"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding wire to schematic: {str(e)}")
|
logger.error(f"Error adding wire to schematic: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
||||||
|
|
||||||
def _handle_list_schematic_libraries(self, params):
|
def _handle_list_schematic_libraries(self, params):
|
||||||
"""List available symbol libraries"""
|
"""List available symbol libraries"""
|
||||||
@@ -712,58 +723,78 @@ class KiCADInterface:
|
|||||||
return {"success": False, "message": str(e)}
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
def _handle_add_schematic_connection(self, params):
|
def _handle_add_schematic_connection(self, params):
|
||||||
"""Add a pin-to-pin connection in schematic"""
|
"""Add a pin-to-pin connection in schematic with automatic pin discovery and routing"""
|
||||||
logger.info("Adding pin-to-pin connection in schematic")
|
logger.info("Adding pin-to-pin connection in schematic")
|
||||||
try:
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
source_ref = params.get("sourceRef")
|
source_ref = params.get("sourceRef")
|
||||||
source_pin = params.get("sourcePin")
|
source_pin = params.get("sourcePin")
|
||||||
target_ref = params.get("targetRef")
|
target_ref = params.get("targetRef")
|
||||||
target_pin = params.get("targetPin")
|
target_pin = params.get("targetPin")
|
||||||
|
routing = params.get("routing", "direct") # 'direct', 'orthogonal_h', 'orthogonal_v'
|
||||||
|
|
||||||
if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]):
|
if not all([schematic_path, source_ref, source_pin, target_ref, target_pin]):
|
||||||
return {"success": False, "message": "Missing required parameters"}
|
return {"success": False, "message": "Missing required parameters"}
|
||||||
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
# Use ConnectionManager with new PinLocator and WireManager integration
|
||||||
if not schematic:
|
success = ConnectionManager.add_connection(
|
||||||
return {"success": False, "message": "Failed to load schematic"}
|
Path(schematic_path),
|
||||||
|
source_ref,
|
||||||
success = ConnectionManager.add_connection(schematic, source_ref, source_pin, target_ref, target_pin)
|
source_pin,
|
||||||
|
target_ref,
|
||||||
|
target_pin,
|
||||||
|
routing=routing
|
||||||
|
)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
return {
|
||||||
return {"success": True}
|
"success": True,
|
||||||
|
"message": f"Connected {source_ref}/{source_pin} to {target_ref}/{target_pin} (routing: {routing})"
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": "Failed to add connection"}
|
return {"success": False, "message": "Failed to add connection"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding schematic connection: {str(e)}")
|
logger.error(f"Error adding schematic connection: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
||||||
|
|
||||||
def _handle_add_schematic_net_label(self, params):
|
def _handle_add_schematic_net_label(self, params):
|
||||||
"""Add a net label to schematic"""
|
"""Add a net label to schematic using WireManager"""
|
||||||
logger.info("Adding net label to schematic")
|
logger.info("Adding net label to schematic")
|
||||||
try:
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
from commands.wire_manager import WireManager
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
net_name = params.get("netName")
|
net_name = params.get("netName")
|
||||||
position = params.get("position")
|
position = params.get("position")
|
||||||
|
label_type = params.get("labelType", "label") # 'label', 'global_label', 'hierarchical_label'
|
||||||
|
orientation = params.get("orientation", 0) # 0, 90, 180, 270
|
||||||
|
|
||||||
if not all([schematic_path, net_name, position]):
|
if not all([schematic_path, net_name, position]):
|
||||||
return {"success": False, "message": "Missing required parameters"}
|
return {"success": False, "message": "Missing required parameters"}
|
||||||
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
# Use WireManager for S-expression manipulation
|
||||||
if not schematic:
|
success = WireManager.add_label(
|
||||||
return {"success": False, "message": "Failed to load schematic"}
|
Path(schematic_path),
|
||||||
|
net_name,
|
||||||
|
position,
|
||||||
|
label_type=label_type,
|
||||||
|
orientation=orientation
|
||||||
|
)
|
||||||
|
|
||||||
label = ConnectionManager.add_net_label(schematic, net_name, position)
|
if success:
|
||||||
|
return {"success": True, "message": f"Added net label '{net_name}' at {position}"}
|
||||||
if label:
|
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
|
||||||
return {"success": True}
|
|
||||||
else:
|
else:
|
||||||
return {"success": False, "message": "Failed to add net label"}
|
return {"success": False, "message": "Failed to add net label"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding net label: {str(e)}")
|
logger.error(f"Error adding net label: {str(e)}")
|
||||||
return {"success": False, "message": str(e)}
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()}
|
||||||
|
|
||||||
def _handle_connect_to_net(self, params):
|
def _handle_connect_to_net(self, params):
|
||||||
"""Connect a component pin to a named net"""
|
"""Connect a component pin to a named net"""
|
||||||
|
|||||||
Reference in New Issue
Block a user