fix: footprint param ignored, delete duplicates, add edit_schematic_component
Bug 1 - add_schematic_component: footprint parameter silently ignored The footprint value from MCP params was never passed through to DynamicSymbolLoader.add_component() / create_component_instance(). Every placed symbol had an empty Footprint field regardless of input. Fix: added footprint: str='' to both functions, passed through all call sites, added footprint to schematic.ts tool schema. Bug 2 - delete_schematic_component: only deleted first duplicate When a reference appeared multiple times (e.g. after a failed add attempt), only the first instance was removed due to break after first match. Fix: collect all matching blocks first, then delete back-to-front to preserve indices. Response now includes deleted_count. New tool - edit_schematic_component Update footprint, value or reference of a placed symbol in-place. More efficient than delete+re-add: preserves position and UUID. Accepts: schematicPath, reference, footprint?, value?, newReference? All 3 fixes verified by live tests on a real JLCPCB/KiCAD 9 project: - R_TEST1: footprint Resistor_SMD:R_0603_1608Metric written correctly - J1 duplicate: deleted_count=2 with single call - J2 edit: PinSocket footprint assigned in-place, no delete+add needed - PCB update (F8) confirmed: only components with footprint imported
This commit is contained in:
@@ -14,7 +14,7 @@ import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger('kicad_interface')
|
||||
logger = logging.getLogger("kicad_interface")
|
||||
|
||||
|
||||
class DynamicSymbolLoader:
|
||||
@@ -43,7 +43,7 @@ class DynamicSymbolLoader:
|
||||
Path.home() / ".local" / "share" / "kicad" / "9.0" / "symbols",
|
||||
Path.home() / "Documents" / "KiCad" / "9.0" / "3rdparty" / "symbols",
|
||||
]
|
||||
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:
|
||||
possible_paths.insert(0, Path(os.environ[env_var]))
|
||||
|
||||
@@ -63,13 +63,14 @@ class DynamicSymbolLoader:
|
||||
Extract a complete symbol block from a library or schematic file by matching
|
||||
parentheses depth. Returns the raw text of the symbol definition.
|
||||
"""
|
||||
lines = text.split('\n')
|
||||
lines = text.split("\n")
|
||||
start = None
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
# 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):
|
||||
if stripped.startswith(f'(symbol "{symbol_name}"') and not re.match(
|
||||
r'.*_\d+_\d+"', stripped
|
||||
):
|
||||
start = i
|
||||
break
|
||||
|
||||
@@ -80,9 +81,9 @@ class DynamicSymbolLoader:
|
||||
end = None
|
||||
for i in range(start, len(lines)):
|
||||
for ch in lines[i]:
|
||||
if ch == '(':
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
end = i
|
||||
@@ -93,9 +94,11 @@ class DynamicSymbolLoader:
|
||||
if end is None:
|
||||
return None
|
||||
|
||||
return '\n'.join(lines[start:end + 1])
|
||||
return "\n".join(lines[start : end + 1])
|
||||
|
||||
def extract_symbol_from_library(self, library_name: str, symbol_name: str) -> Optional[str]:
|
||||
def extract_symbol_from_library(
|
||||
self, library_name: str, symbol_name: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract a symbol definition from a KiCad .kicad_sym library file.
|
||||
Returns the raw text block, ready to be injected into a schematic.
|
||||
@@ -112,12 +115,14 @@ class DynamicSymbolLoader:
|
||||
if not lib_path:
|
||||
return None
|
||||
|
||||
with open(lib_path, 'r', encoding='utf-8') as f:
|
||||
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")
|
||||
logger.warning(
|
||||
f"Symbol '{symbol_name}' not found in {library_name}.kicad_sym"
|
||||
)
|
||||
return None
|
||||
|
||||
# Check if this symbol uses (extends "ParentName")
|
||||
@@ -125,14 +130,16 @@ class DynamicSymbolLoader:
|
||||
parent_block = None
|
||||
if extends_match:
|
||||
parent_name = extends_match.group(1)
|
||||
logger.info(f"Symbol {symbol_name} extends {parent_name}, extracting parent too")
|
||||
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)
|
||||
1, # Only first occurrence (top-level)
|
||||
)
|
||||
|
||||
# Prefix top-level symbol name with library
|
||||
@@ -140,13 +147,13 @@ class DynamicSymbolLoader:
|
||||
block = block.replace(
|
||||
f'(symbol "{symbol_name}"',
|
||||
f'(symbol "{full_name}"',
|
||||
1 # Only first occurrence (top-level)
|
||||
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
|
||||
result = parent_block + "\n" + block
|
||||
else:
|
||||
result = block
|
||||
|
||||
@@ -154,14 +161,16 @@ class DynamicSymbolLoader:
|
||||
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 into a schematic's lib_symbols section.
|
||||
Uses text manipulation to preserve file formatting.
|
||||
"""
|
||||
full_name = f"{library_name}:{symbol_name}"
|
||||
|
||||
with open(schematic_path, 'r', encoding='utf-8') as f:
|
||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if symbol already exists
|
||||
@@ -172,36 +181,38 @@ class DynamicSymbolLoader:
|
||||
# Extract symbol from library
|
||||
symbol_block = self.extract_symbol_from_library(library_name, symbol_name)
|
||||
if not symbol_block:
|
||||
raise ValueError(f"Symbol '{symbol_name}' not found in library '{library_name}'")
|
||||
raise ValueError(
|
||||
f"Symbol '{symbol_name}' not found in library '{library_name}'"
|
||||
)
|
||||
|
||||
# Indent the block to match lib_symbols indentation (4 spaces for top-level)
|
||||
indented_lines = []
|
||||
for line in symbol_block.split('\n'):
|
||||
for line in symbol_block.split("\n"):
|
||||
# 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)
|
||||
indented_lines.append(" " + line if line.strip() else line)
|
||||
indented_block = "\n".join(indented_lines)
|
||||
|
||||
# Find the end of lib_symbols section to insert before closing )
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
lib_sym_start = None
|
||||
lib_sym_end = None
|
||||
depth = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if '(lib_symbols' in line and lib_sym_start is None:
|
||||
if "(lib_symbols" in line and lib_sym_start is None:
|
||||
lib_sym_start = i
|
||||
depth = 0
|
||||
for ch in line:
|
||||
if ch == '(':
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
continue
|
||||
if lib_sym_start is not None and lib_sym_end is None:
|
||||
for ch in line:
|
||||
if ch == '(':
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
lib_sym_end = i
|
||||
@@ -215,15 +226,29 @@ class DynamicSymbolLoader:
|
||||
# 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))
|
||||
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}")
|
||||
# Handle both Path objects and strings
|
||||
sch_name = (
|
||||
schematic_path.name
|
||||
if hasattr(schematic_path, "name")
|
||||
else str(schematic_path)
|
||||
)
|
||||
logger.info(f"Injected symbol {full_name} into {sch_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:
|
||||
def create_component_instance(
|
||||
self,
|
||||
schematic_path: Path,
|
||||
library_name: str,
|
||||
symbol_name: str,
|
||||
reference: str,
|
||||
value: str = "",
|
||||
footprint: str = "",
|
||||
x: float = 0,
|
||||
y: float = 0,
|
||||
) -> bool:
|
||||
"""
|
||||
Add a component instance to the schematic.
|
||||
This creates the (symbol ...) block with lib_id reference.
|
||||
@@ -231,7 +256,7 @@ class DynamicSymbolLoader:
|
||||
full_lib_id = f"{library_name}:{symbol_name}"
|
||||
new_uuid = str(uuid.uuid4())
|
||||
|
||||
instance_block = f''' (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
|
||||
instance_block = f""" (symbol (lib_id "{full_lib_id}") (at {x} {y} 0) (unit 1)
|
||||
(in_bom yes) (on_board yes) (dnp no)
|
||||
(uuid "{new_uuid}")
|
||||
(property "Reference" "{reference}" (at {x} {y - 2.54} 0)
|
||||
@@ -240,30 +265,30 @@ class DynamicSymbolLoader:
|
||||
(property "Value" "{value or symbol_name}" (at {x} {y + 2.54} 0)
|
||||
(effects (font (size 1.27 1.27)))
|
||||
)
|
||||
(property "Footprint" "" (at {x} {y} 0)
|
||||
(property "Footprint" "{footprint}" (at {x} {y} 0)
|
||||
(effects (font (size 1.27 1.27)) (hide yes))
|
||||
)
|
||||
(property "Datasheet" "~" (at {x} {y} 0)
|
||||
(effects (font (size 1.27 1.27)) (hide yes))
|
||||
)
|
||||
)'''
|
||||
)"""
|
||||
|
||||
with open(schematic_path, 'r', encoding='utf-8') as f:
|
||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Insert before (sheet_instances or at end before final )
|
||||
lines = content.split('\n')
|
||||
lines = content.split("\n")
|
||||
insert_pos = None
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if '(sheet_instances' in line:
|
||||
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() == ')':
|
||||
if lines[i].strip() == ")":
|
||||
insert_pos = i
|
||||
break
|
||||
|
||||
@@ -272,13 +297,17 @@ class DynamicSymbolLoader:
|
||||
|
||||
lines.insert(insert_pos, instance_block)
|
||||
|
||||
with open(schematic_path, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines))
|
||||
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
logger.info(f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})")
|
||||
logger.info(
|
||||
f"Added component instance {reference} ({full_lib_id}) at ({x}, {y})"
|
||||
)
|
||||
return True
|
||||
|
||||
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 definition and create a template instance.
|
||||
Returns a template reference name.
|
||||
@@ -289,21 +318,34 @@ class DynamicSymbolLoader:
|
||||
self.inject_symbol_into_schematic(schematic_path, library_name, symbol_name)
|
||||
|
||||
# Step 2: Create an offscreen template instance
|
||||
lib_clean = library_name.replace('-', '_').replace('.', '_')
|
||||
sym_clean = symbol_name.replace('-', '_').replace('.', '_')
|
||||
lib_clean = library_name.replace("-", "_").replace(".", "_")
|
||||
sym_clean = symbol_name.replace("-", "_").replace(".", "_")
|
||||
template_ref = f"_TEMPLATE_{lib_clean}_{sym_clean}"
|
||||
|
||||
self.create_component_instance(
|
||||
schematic_path, library_name, symbol_name,
|
||||
reference=template_ref, value=symbol_name,
|
||||
x=-200, y=-200
|
||||
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
|
||||
|
||||
def add_component(self, schematic_path: Path, library_name: str, symbol_name: str,
|
||||
reference: str, value: str = "", x: float = 0, y: float = 0) -> bool:
|
||||
def add_component(
|
||||
self,
|
||||
schematic_path: Path,
|
||||
library_name: str,
|
||||
symbol_name: str,
|
||||
reference: str,
|
||||
value: str = "",
|
||||
footprint: 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.
|
||||
@@ -313,12 +355,18 @@ class DynamicSymbolLoader:
|
||||
|
||||
# Add the component instance
|
||||
return self.create_component_instance(
|
||||
schematic_path, library_name, symbol_name,
|
||||
reference=reference, value=value, x=x, y=y
|
||||
schematic_path,
|
||||
library_name,
|
||||
symbol_name,
|
||||
reference=reference,
|
||||
value=value,
|
||||
footprint=footprint,
|
||||
x=x,
|
||||
y=y,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
loader = DynamicSymbolLoader()
|
||||
|
||||
@@ -329,7 +377,12 @@ if __name__ == '__main__':
|
||||
print(f" Found {len(lib_dirs)} directories")
|
||||
|
||||
print("\n2. Extracting symbols...")
|
||||
for lib, sym in [('Device', 'R'), ('Device', 'C'), ('Device', 'LED'), ('Device', 'Q_NMOS')]:
|
||||
for lib, sym in [
|
||||
("Device", "R"),
|
||||
("Device", "C"),
|
||||
("Device", "LED"),
|
||||
("Device", "Q_NMOS"),
|
||||
]:
|
||||
block = loader.extract_symbol_from_library(lib, sym)
|
||||
if block:
|
||||
print(f" OK: {lib}:{sym} ({len(block)} chars)")
|
||||
@@ -337,8 +390,8 @@ if __name__ == '__main__':
|
||||
print(f" FAIL: {lib}:{sym}")
|
||||
|
||||
print("\n3. Testing extends resolution...")
|
||||
block = loader.extract_symbol_from_library('Regulator_Switching', 'LM2596S-5')
|
||||
if block and 'LM2596S-12' in block:
|
||||
block = loader.extract_symbol_from_library("Regulator_Switching", "LM2596S-5")
|
||||
if block and "LM2596S-12" in block:
|
||||
print(f" OK: LM2596S-5 includes parent LM2596S-12 ({len(block)} chars)")
|
||||
else:
|
||||
print(f" FAIL: extends not resolved")
|
||||
|
||||
Reference in New Issue
Block a user