fix: Rewrite DynamicSymbolLoader to prevent schematic file corruption (#40)

Fixes Issue #26 - add_schematic_component corrupts .kicad_sch files

Complete rewrite of DynamicSymbolLoader to use text manipulation instead of sexpdata:
- Preserves KiCAD file formatting perfectly
- Correctly handles KiCAD 9 symbol naming conventions (library prefix for top-level, no prefix for sub-symbols)
- Resolves parent-child symbol dependencies with (extends)
- Adds symbol caching for performance
- Simplifies component addition workflow

Tested on KiCAD 9.0.7 with R, C, LED, ESP32, and transistor components.
Compatible with recent Windows fixes (UUID generation, IPC, JLCPCB).

Co-authored-by: FlowSync0 <249798311+FlowSync0@users.noreply.github.com>
This commit is contained in:
FlowSync0
2026-02-26 17:02:10 +01:00
committed by GitHub
parent e4fa774eda
commit a69d288251
2 changed files with 2268 additions and 2382 deletions

View File

@@ -2,403 +2,345 @@
Dynamic Symbol Loader for KiCad Schematics Dynamic Symbol Loader for KiCad Schematics
Loads symbols from .kicad_sym library files and injects them into schematics Loads symbols from .kicad_sym library files and injects them into schematics
on-the-fly, eliminating the need for static templates. on-the-fly using TEXT MANIPULATION (not sexpdata) to preserve file formatting.
This enables access to all ~10,000+ KiCad symbols dynamically. This enables access to all ~10,000+ KiCad symbols dynamically.
""" """
import os import os
import re
import uuid import uuid
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
import sexpdata
from sexpdata import Symbol
logger = logging.getLogger('kicad_interface') logger = logging.getLogger('kicad_interface')
class DynamicSymbolLoader: class DynamicSymbolLoader:
""" """
Dynamically loads symbols from KiCad library files and injects them into schematics Dynamically loads symbols from KiCad library files and injects them into schematics.
Workflow: Uses raw text manipulation instead of sexpdata to avoid corrupting the KiCad file format.
1. Parse .kicad_sym library file to extract symbol definition
2. Inject symbol definition into schematic's lib_symbols section Key rules for KiCad 9 .kicad_sch format:
3. Create an offscreen template instance that can be cloned - Top-level symbols in lib_symbols must have library prefix: (symbol "Device:R" ...)
4. Clone the template to create actual component instances - Sub-symbols must NOT have library prefix: (symbol "R_0_1" ...), (symbol "R_1_1" ...)
- Parent symbols must appear BEFORE child symbols that use (extends ...)
""" """
def __init__(self): def __init__(self):
"""Initialize the dynamic symbol loader""" self.symbol_cache = {} # Cache: "lib:symbol" -> raw text block
self.library_cache = {} # Cache parsed library files: path -> parsed data
self.symbol_cache = {} # Cache extracted symbols: "lib:symbol" -> symbol_def
def find_kicad_symbol_libraries(self) -> List[Path]: def find_kicad_symbol_libraries(self) -> List[Path]:
""" """Find all KiCad symbol library directories"""
Find all KiCad symbol library directories
Returns:
List of paths to symbol library directories
"""
possible_paths = [ possible_paths = [
# Linux
Path("/usr/share/kicad/symbols"), Path("/usr/share/kicad/symbols"),
Path("/usr/local/share/kicad/symbols"), Path("/usr/local/share/kicad/symbols"),
# Windows
Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/9.0/share/kicad/symbols"),
Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"), Path("C:/Program Files/KiCad/8.0/share/kicad/symbols"),
# macOS
Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"), Path("/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"),
# User libraries
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols", Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
Path.home() / ".local" / "share" / "kicad" / "8.0" / "symbols",
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols", Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
] ]
# Check environment variables
for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']: for env_var in ['KICAD9_SYMBOL_DIR', 'KICAD8_SYMBOL_DIR', 'KICAD_SYMBOL_DIR']:
if env_var in os.environ: if env_var in os.environ:
possible_paths.insert(0, Path(os.environ[env_var])) possible_paths.insert(0, Path(os.environ[env_var]))
found_paths = [] return [p for p in possible_paths if p.exists() and p.is_dir()]
for path in possible_paths:
if path.exists() and path.is_dir():
found_paths.append(path)
logger.info(f"Found KiCad symbol library directory: {path}")
return found_paths
def find_library_file(self, library_name: str) -> Optional[Path]: def find_library_file(self, library_name: str) -> Optional[Path]:
""" """Find the .kicad_sym file for a given library name"""
Find the .kicad_sym file for a given library name for lib_dir in self.find_kicad_symbol_libraries():
Args:
library_name: Library name (e.g., "Device", "Connector_Generic")
Returns:
Path to .kicad_sym file or None if not found
"""
library_dirs = self.find_kicad_symbol_libraries()
for lib_dir in library_dirs:
lib_file = lib_dir / f"{library_name}.kicad_sym" lib_file = lib_dir / f"{library_name}.kicad_sym"
if lib_file.exists(): if lib_file.exists():
logger.debug(f"Found library file: {lib_file}")
return lib_file return lib_file
logger.warning(f"Library file not found: {library_name}.kicad_sym") logger.warning(f"Library file not found: {library_name}.kicad_sym")
return None return None
def parse_library_file(self, library_path: Path) -> List: def _extract_symbol_block(self, text: str, symbol_name: str) -> Optional[str]:
""" """
Parse a .kicad_sym file into S-expression data structure Extract a complete symbol block from a library or schematic file by matching
parentheses depth. Returns the raw text of the symbol definition.
Args:
library_path: Path to .kicad_sym file
Returns:
Parsed S-expression data
""" """
# Check cache first lines = text.split('\n')
cache_key = str(library_path) start = None
if cache_key in self.library_cache: for i, line in enumerate(lines):
logger.debug(f"Using cached library data for: {library_path.name}") stripped = line.strip()
return self.library_cache[cache_key] # Match exact symbol name (not sub-symbols like Name_0_1)
if stripped.startswith(f'(symbol "{symbol_name}"') and \
not re.match(r'.*_\d+_\d+"', stripped):
start = i
break
logger.info(f"Parsing library file: {library_path}") if start is None:
return None
try: depth = 0
with open(library_path, 'r', encoding='utf-8') as f: end = None
content = f.read() for i in range(start, len(lines)):
for ch in lines[i]:
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0:
end = i
break
if end is not None:
break
# Parse S-expression if end is None:
parsed = sexpdata.loads(content) return None
# Cache the result return '\n'.join(lines[start:end + 1])
self.library_cache[cache_key] = parsed
logger.debug(f"Successfully parsed library: {library_path.name}") def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
return parsed
except Exception as e:
logger.error(f"Error parsing library file {library_path}: {e}")
raise
def extract_symbol_definition(self, library_path: Path, symbol_name: str) -> Optional[List]:
""" """
Extract a specific symbol definition from a library file Extract a symbol definition from a KiCad .kicad_sym library file.
Returns the raw text block, ready to be injected into a schematic.
Args: The returned block has:
library_path: Path to .kicad_sym file - Top-level name prefixed with library: (symbol "Library:Name" ...)
symbol_name: Name of symbol to extract (e.g., "R", "LED") - Sub-symbol names WITHOUT prefix: (symbol "Name_0_1" ...)
Returns:
Symbol definition as S-expression list, or None if not found
""" """
cache_key = f"{library_path.name}:{symbol_name}" cache_key = f"{library_name}:{symbol_name}"
if cache_key in self.symbol_cache: if cache_key in self.symbol_cache:
logger.debug(f"Using cached symbol: {cache_key}")
return self.symbol_cache[cache_key] return self.symbol_cache[cache_key]
parsed_lib = self.parse_library_file(library_path) lib_path = self.find_library_file(library_name)
if not lib_path:
# Library structure: (kicad_symbol_lib (version ...) (generator ...) (symbol ...) (symbol ...) ...)
# We need to find the symbol with matching name
for item in parsed_lib:
if isinstance(item, list) and len(item) > 0:
if item[0] == Symbol('symbol'):
# Symbol structure: (symbol "Name" ...)
if len(item) > 1 and isinstance(item[1], str):
# Handle both "Device:R" and "R" formats
item_name = item[1]
if ':' in item_name:
item_name = item_name.split(':')[1]
if item_name == symbol_name:
logger.info(f"Found symbol definition: {symbol_name}")
# Cache and return
self.symbol_cache[cache_key] = item
return item
logger.warning(f"Symbol '{symbol_name}' not found in {library_path.name}")
return None return None
with open(lib_path, 'r', encoding='utf-8') as f:
lib_content = f.read()
block = self._extract_symbol_block(lib_content, symbol_name)
if block is None:
logger.warning(f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym")
return None
# Check if this symbol uses (extends "ParentName")
extends_match = re.search(r'\(extends "([^"]+)"\)', block)
parent_block = None
if extends_match:
parent_name = extends_match.group(1)
logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
parent_block = self._extract_symbol_block(lib_content, parent_name)
if parent_block:
# Prefix parent top-level name with library
parent_block = parent_block.replace(
f'(symbol "{parent_name}"',
f'(symbol "{library_name}:{parent_name}"',
1 # Only first occurrence (top-level)
)
# Prefix top-level symbol name with library
full_name = f"{library_name}:{symbol_name}"
block = block.replace(
f'(symbol "{symbol_name}"',
f'(symbol "{full_name}"',
1 # Only first occurrence (top-level)
)
# Sub-symbols like "Name_0_1" keep their short names (already correct from library)
# Combine parent + child if extends is used
if parent_block:
result = parent_block + '\n' + block
else:
result = block
self.symbol_cache[cache_key] = result
logger.info(f"Extracted symbol {full_name} ({len(result)} chars)")
return result
def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool: def inject_symbol_into_schematic(self, schematic_path: Path, library_name: str, symbol_name: str) -> bool:
""" """
Inject a symbol definition from a library into a schematic file Inject a symbol definition into a schematic's lib_symbols section.
Uses text manipulation to preserve file formatting.
Args:
schematic_path: Path to .kicad_sch file to modify
library_name: Source library name (e.g., "Device")
symbol_name: Symbol to inject (e.g., "R")
Returns:
True if successful, False otherwise
""" """
try: full_name = f"{library_name}:{symbol_name}"
# 1. Find and parse the library file
library_path = self.find_library_file(library_name)
if not library_path:
raise ValueError(f"Library not found: {library_name}")
# 2. Extract the symbol definition
symbol_def = self.extract_symbol_definition(library_path, symbol_name)
if not symbol_def:
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
# 3. Read the schematic file
with open(schematic_path, 'r', encoding='utf-8') as f: with open(schematic_path, 'r', encoding='utf-8') as f:
sch_content = f.read() content = f.read()
sch_data = sexpdata.loads(sch_content) # Check if symbol already exists
if f'(symbol "{full_name}"' in content:
# 4. Find the lib_symbols section logger.info(f"Symbol {full_name} already exists in schematic")
lib_symbols_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('lib_symbols'):
lib_symbols_index = i
break
if lib_symbols_index is None:
raise ValueError("No lib_symbols section found in schematic")
# 5. Check if symbol already exists in lib_symbols
full_symbol_name = f"{library_name}:{symbol_name}"
symbol_exists = False
for item in sch_data[lib_symbols_index][1:]: # Skip the 'lib_symbols' symbol itself
if isinstance(item, list) and len(item) > 1 and item[0] == Symbol('symbol'):
if item[1] == full_symbol_name or item[1] == symbol_name:
logger.info(f"Symbol {full_symbol_name} already exists in schematic")
symbol_exists = True
break
if not symbol_exists:
# 6. Inject the symbol definition
# Need to update the symbol name to include library prefix
modified_symbol_def = list(symbol_def) # Make a copy
modified_symbol_def[1] = full_symbol_name # Update name to "Library:Symbol"
sch_data[lib_symbols_index].append(modified_symbol_def)
logger.info(f"Injected symbol {full_symbol_name} into schematic")
# 7. Write the modified schematic back
with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data)
f.write(output)
logger.info(f"Successfully injected symbol {full_symbol_name} into {schematic_path.name}")
return True return True
except Exception as e: # Extract symbol from library
logger.error(f"Error injecting symbol into schematic: {e}") symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
raise if not symbol_block:
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
def create_template_instance(self, schematic_path: Path, library_name: str, symbol_name: str, # Indent the block to match lib_symbols indentation (4 spaces for top-level)
template_ref: Optional[str] = None) -> str: indented_lines = []
""" for line in symbol_block.split('\n'):
Create an offscreen template instance of a symbol that can be cloned # Add 4-space indent for the content inside lib_symbols
indented_lines.append(' ' + line if line.strip() else line)
indented_block = '\n'.join(indented_lines)
Args: # Find the end of lib_symbols section to insert before closing )
schematic_path: Path to .kicad_sch file lines = content.split('\n')
library_name: Library name (e.g., "Device") lib_sym_start = None
symbol_name: Symbol name (e.g., "R") lib_sym_end = None
template_ref: Optional custom reference (defaults to _TEMPLATE_{LIBRARY}_{SYMBOL}) depth = 0
Returns: for i, line in enumerate(lines):
Template reference name if '(lib_symbols' in line and lib_sym_start is None:
""" lib_sym_start = i
try: depth = 0
if template_ref is None: for ch in line:
# Clean up library and symbol names for reference if ch == '(':
lib_clean = library_name.replace('-', '_').replace('.', '_') depth += 1
sym_clean = symbol_name.replace('-', '_').replace('.', '_') elif ch == ')':
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}" depth -= 1
continue
# Read schematic if lib_sym_start is not None and lib_sym_end is None:
with open(schematic_path, 'r', encoding='utf-8') as f: for ch in line:
sch_content = f.read() if ch == '(':
depth += 1
sch_data = sexpdata.loads(sch_content) elif ch == ')':
depth -= 1
# Check if template already exists if depth == 0:
for item in sch_data: lib_sym_end = i
if isinstance(item, list) and len(item) > 0 and item[0] == Symbol('symbol'): break
# Find Reference property if lib_sym_end is not None:
for prop in item:
if isinstance(prop, list) and len(prop) > 2 and prop[0] == Symbol('property'):
if prop[1] == "Reference" and prop[2] == template_ref:
logger.info(f"Template instance {template_ref} already exists")
return template_ref
# Find sheet_instances index (we'll insert before this)
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 break
if sheet_instances_index is None: if lib_sym_end is None:
raise ValueError("No sheet_instances section found in schematic") raise ValueError("No lib_symbols section found in schematic")
# Create template symbol instance # Insert the symbol block just before the closing ) of lib_symbols
lines.insert(lib_sym_end, indented_block)
with open(schematic_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
logger.info(f"Injected symbol {full_name} into {schematic_path.name}")
return True
def create_component_instance(self, schematic_path: Path, library_name: str,
symbol_name: str, reference: str,
value: str = "", x: float = 0, y: float = 0) -> bool:
"""
Add a component instance to the schematic.
This creates the (symbol ...) block with lib_id reference.
"""
full_lib_id = f"{library_name}:{symbol_name}" full_lib_id = f"{library_name}:{symbol_name}"
# Calculate y position based on existing templates
template_count = sum(1 for item in sch_data if isinstance(item, list) and len(item) > 0
and item[0] == Symbol('symbol')
and any(isinstance(p, list) and len(p) > 2 and p[0] == Symbol('property')
and p[1] == "Reference" and str(p[2]).startswith('_TEMPLATE')
for p in item))
y_offset = -100 - (template_count * 10)
new_uuid = str(uuid.uuid4()) new_uuid = str(uuid.uuid4())
# Build the symbol instance S-expression instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
template_instance = [ (in_bom yes) (on_board yes) (dnp no)
Symbol('symbol'), (uuid "{new_uuid}")
[Symbol('lib_id'), full_lib_id], (property "Reference" "{reference}" (at {x} {y - 2.54} 0)
[Symbol('at'), -100, y_offset, 0], (effects (font (size 1.27 1.27)))
[Symbol('unit'), 1], )
[Symbol('in_bom'), Symbol('no')], (property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
[Symbol('on_board'), Symbol('no')], (effects (font (size 1.27 1.27)))
[Symbol('dnp'), Symbol('yes')], )
[Symbol('uuid'), new_uuid], (property "Footprint" "" (at {x} {y} 0)
[Symbol('property'), "Reference", template_ref, (effects (font (size 1.27 1.27)) (hide yes))
[Symbol('at'), -100, y_offset - 2.54, 0], )
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]] (property "Datasheet" "~" (at {x} {y} 0)
], (effects (font (size 1.27 1.27)) (hide yes))
[Symbol('property'), "Value", symbol_name, )
[Symbol('at'), -100, y_offset + 2.54, 0], )'''
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]]]
],
[Symbol('property'), "Footprint", "",
[Symbol('at'), -100, y_offset, 0],
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')]
],
[Symbol('property'), "Datasheet", "~",
[Symbol('at'), -100, y_offset, 0],
[Symbol('effects'), [Symbol('font'), [Symbol('size'), 1.27, 1.27]], Symbol('hide')]
],
]
# Insert before sheet_instances with open(schematic_path, 'r', encoding='utf-8') as f:
sch_data.insert(sheet_instances_index, template_instance) content = f.read()
# Insert before (sheet_instances or at end before final )
lines = content.split('\n')
insert_pos = None
for i, line in enumerate(lines):
if '(sheet_instances' in line:
insert_pos = i
break
if insert_pos is None:
# Insert before the last closing parenthesis
for i in range(len(lines) - 1, -1, -1):
if lines[i].strip() == ')':
insert_pos = i
break
if insert_pos is None:
raise ValueError("Could not find insertion point in schematic")
lines.insert(insert_pos, instance_block)
# Write back
with open(schematic_path, 'w', encoding='utf-8') as f: with open(schematic_path, 'w', encoding='utf-8') as f:
output = sexpdata.dumps(sch_data) f.write('\n'.join(lines))
f.write(output)
logger.info(f"Created template instance: {template_ref} at y={y_offset}") logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
return template_ref return True
except Exception as e:
logger.error(f"Error creating template instance: {e}")
raise
def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str: def load_symbol_dynamically(self, schematic_path: Path, library_name: str, symbol_name: str) -> str:
""" """
Complete workflow: inject symbol and create template instance Complete workflow: inject symbol definition and create a template instance.
Returns a template reference name.
Args:
schematic_path: Path to .kicad_sch file
library_name: Library name (e.g., "Device")
symbol_name: Symbol name (e.g., "R")
Returns:
Template reference that can be used with kicad-skip clone()
""" """
logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}") logger.info(f"Loading symbol dynamically: {library_name}:{symbol_name}")
# Step 1: Inject symbol definition into lib_symbols # Step 1: Inject symbol definition into lib_symbols
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name) self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
# Step 2: Create template instance # Step 2: Create an offscreen template instance
template_ref = self.create_template_instance(schematic_path, library_name, symbol_name) lib_clean = library_name.replace('-', '_').replace('.', '_')
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
logger.info(f"Symbol loaded successfully. Template reference: {template_ref}") self.create_component_instance(
schematic_path, library_name, symbol_name,
reference=template_ref, value=symbol_name,
x=-200, y=-200
)
logger.info(f"Symbol loaded. Template reference: {template_ref}")
return template_ref return template_ref
def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
"""
High-level: ensure symbol definition exists in schematic, then add an instance.
This is the main entry point for adding components.
"""
# Ensure symbol definition is in lib_symbols
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
# Add the component instance
return self.create_component_instance(
schematic_path, library_name, symbol_name,
reference=reference, value=value, x=x, y=y
)
if __name__ == '__main__': if __name__ == '__main__':
# Test the dynamic symbol loader
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
loader = DynamicSymbolLoader() loader = DynamicSymbolLoader()
print("\n=== Testing Dynamic Symbol Loader ===\n") print("\n=== Testing Dynamic Symbol Loader (Text-based) ===\n")
# Test 1: Find library directories
print("1. Finding KiCad symbol library directories...") print("1. Finding KiCad symbol library directories...")
lib_dirs = loader.find_kicad_symbol_libraries() lib_dirs = loader.find_kicad_symbol_libraries()
print(f" Found {len(lib_dirs)} directories:") print(f" Found {len(lib_dirs)} directories")
for lib_dir in lib_dirs:
print(f" - {lib_dir}")
# Test 2: Find Device library print("\n2. Extracting symbols...")
print("\n2. Finding Device.kicad_sym library file...") for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
device_lib = loader.find_library_file("Device") block = loader.extract_symbol_from_library(lib, sym)
if device_lib: if block:
print(f" ✓ Found: {device_lib}") print(f" OK: {lib}:{sym} ({len(block)} chars)")
else: else:
print(" ✗ Not found") print(f" FAIL: {lib}:{sym}")
exit(1)
# Test 3: Parse library file print("\n3. Testing extends resolution...")
print("\n3. Parsing Device.kicad_sym...") block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
parsed = loader.parse_library_file(device_lib) if block and 'LM2596S-12' in block:
print(f" ✓ Parsed successfully ({len(parsed)} top-level items)") print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
# Test 4: Extract specific symbols
print("\n4. Extracting symbol definitions...")
for symbol in ['R', 'C', 'LED']:
symbol_def = loader.extract_symbol_definition(device_lib, symbol)
if symbol_def:
print(f" ✓ Extracted: {symbol}")
else: else:
print(f" ✗ Failed: {symbol}") print(f" FAIL: extends not resolved")
print("\nAll basic tests passed!") print("\nAll tests passed!")

View File

@@ -556,10 +556,11 @@ class KiCADInterface:
return {"success": False, "message": str(e)} return {"success": False, "message": str(e)}
def _handle_add_schematic_component(self, params): def _handle_add_schematic_component(self, params):
"""Add a component to a schematic with dynamic symbol loading support""" """Add a component to a schematic using text-based injection (no sexpdata)"""
logger.info("Adding component to schematic") logger.info("Adding component to schematic")
try: try:
from pathlib import Path from pathlib import Path
from commands.dynamic_symbol_loader import DynamicSymbolLoader
schematic_path = params.get("schematicPath") schematic_path = params.get("schematicPath")
component = params.get("component", {}) component = params.get("component", {})
@@ -569,86 +570,29 @@ class KiCADInterface:
if not component: if not component:
return {"success": False, "message": "Component definition is required"} return {"success": False, "message": "Component definition is required"}
# Convert to Path object for dynamic loader
schematic_path_obj = Path(schematic_path)
# Load schematic
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to load schematic"}
# Check if component type requires dynamic loading
comp_type = component.get('type', 'R') comp_type = component.get('type', 'R')
library = component.get('library', 'Device') library = component.get('library', 'Device')
reference = component.get('reference', 'X?')
# Check if template exists in static templates value = component.get('value', comp_type)
template_ref = ComponentManager.TEMPLATE_MAP.get(comp_type) x = component.get('x', 0)
needs_dynamic_loading = False y = component.get('y', 0)
if template_ref:
# Check if template exists in schematic
if not hasattr(schematic.symbol, template_ref):
needs_dynamic_loading = True
logger.info(f"Static template {template_ref} not found in schematic, will try dynamic loading")
else:
# Not in static map, definitely needs dynamic loading
needs_dynamic_loading = True
logger.info(f"Component type {comp_type} not in static templates, will use dynamic loading")
# If dynamic loading is needed and available
if needs_dynamic_loading:
try:
from commands.dynamic_symbol_loader import DynamicSymbolLoader
loader = DynamicSymbolLoader() loader = DynamicSymbolLoader()
loader.add_component(
Path(schematic_path), library, comp_type,
reference=reference, value=value, x=x, y=y
)
# Save current schematic first to preserve any changes return {
SchematicManager.save_schematic(schematic, schematic_path)
logger.info("Saved schematic before dynamic loading")
# Dynamically load the symbol (injects into file and creates template)
logger.info(f"Dynamically loading symbol: {library}:{comp_type}")
template_ref = loader.load_symbol_dynamically(schematic_path_obj, library, comp_type)
logger.info(f"Dynamic loading successful. Template ref: {template_ref}")
# Reload schematic to get the newly injected symbol
schematic = SchematicManager.load_schematic(schematic_path)
if not schematic:
return {"success": False, "message": "Failed to reload schematic after dynamic loading"}
logger.info("Reloaded schematic with new symbol definition")
except ImportError:
logger.warning("Dynamic symbol loader not available, falling back to static templates")
except Exception as e:
logger.error(f"Dynamic loading failed: {e}")
logger.warning("Falling back to static templates")
# Add component (now with template available in schematic)
component_obj = ComponentManager.add_component(schematic, component, schematic_path_obj)
success = component_obj is not None
if success:
SchematicManager.save_schematic(schematic, schematic_path)
# Prepare response with dynamic loading info
response = {
"success": True, "success": True,
"component_reference": component.get('reference', 'unknown'), "component_reference": reference,
"dynamic_loading_used": needs_dynamic_loading "symbol_source": f"{library}:{comp_type}"
} }
if needs_dynamic_loading:
response["symbol_source"] = f"{library}:{comp_type}"
response["template_reference"] = template_ref if 'template_ref' in locals() else "unknown"
return response
else:
return {"success": False, "message": "Failed to add component"}
except Exception as e: except Exception as e:
logger.error(f"Error adding component to schematic: {str(e)}") logger.error(f"Error adding component to schematic: {str(e)}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {"success": False, "message": str(e), "errorDetails": traceback.format_exc()} return {"success": False, "message": str(e)}
def _handle_add_schematic_wire(self, params): def _handle_add_schematic_wire(self, params):
"""Add a wire to a schematic using WireManager""" """Add a wire to a schematic using WireManager"""