Tidy auto-junction-sync code
- Refactor _handle_rotate_schematic_component to use raw sexp throughout
and write the schematic once instead of three times
- Hoist sexpdata and WireManager imports to module scope; drop the
unnecessary underscore aliases in move/rotate handlers
- Move WireManager._SUB_UNIT_RE to the top of the class body
- Promote the per-call Symbol("symbol")/Symbol("unit") allocations in
_parse_lib_pins / _collect_pin_positions to module-level _SYM_*
constants
- Document the assumption behind _SUB_UNIT_RE's greedy match
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,8 @@ _SYM_LIB_SYMBOLS = Symbol("lib_symbols")
|
|||||||
_SYM_LIB_ID = Symbol("lib_id")
|
_SYM_LIB_ID = Symbol("lib_id")
|
||||||
_SYM_MIRROR = Symbol("mirror")
|
_SYM_MIRROR = Symbol("mirror")
|
||||||
_SYM_PIN = Symbol("pin")
|
_SYM_PIN = Symbol("pin")
|
||||||
|
_SYM_SYMBOL = Symbol("symbol")
|
||||||
|
_SYM_UNIT = Symbol("unit")
|
||||||
_IU_PER_MM = 10000
|
_IU_PER_MM = 10000
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +129,11 @@ def _make_sheet_pin_text(
|
|||||||
class WireManager:
|
class WireManager:
|
||||||
"""Manage wires in KiCad schematics using S-expression manipulation"""
|
"""Manage wires in KiCad schematics using S-expression manipulation"""
|
||||||
|
|
||||||
|
# Regex to parse sub-unit names like "LM324_2_1" → (base="LM324", unit=2, style=1)
|
||||||
|
# The sub-unit suffix is <base>_<unit>_<style> where unit and style are integers.
|
||||||
|
# Assumes KiCad's <base>_<unit>_<style> convention (rightmost two underscore-separated numeric groups are unit/style); unparseable names fall back to including all pins via the else branch in _parse_lib_pins.
|
||||||
|
_SUB_UNIT_RE = re.compile(r"^(.+)_(\d+)_(\d+)$")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_wire(
|
def add_wire(
|
||||||
schematic_path: Path,
|
schematic_path: Path,
|
||||||
@@ -498,10 +505,6 @@ class WireManager:
|
|||||||
[_SYM_UUID, str(uuid.uuid4())],
|
[_SYM_UUID, str(uuid.uuid4())],
|
||||||
]
|
]
|
||||||
|
|
||||||
# Regex to parse sub-unit names like "LM324_2_1" → (base="LM324", unit=2, style=1)
|
|
||||||
# The sub-unit suffix is <base>_<unit>_<style> where unit and style are integers.
|
|
||||||
_SUB_UNIT_RE = re.compile(r"^(.+)_(\d+)_(\d+)$")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_lib_pins(sym_def: list, unit: int = 1) -> List[Tuple[float, float]]:
|
def _parse_lib_pins(sym_def: list, unit: int = 1) -> List[Tuple[float, float]]:
|
||||||
"""Extract pin local (x, y) positions for *unit* from a lib_symbols symbol definition.
|
"""Extract pin local (x, y) positions for *unit* from a lib_symbols symbol definition.
|
||||||
@@ -515,7 +518,6 @@ class WireManager:
|
|||||||
|
|
||||||
Uses a stack instead of recursion to handle nested sub-unit symbols.
|
Uses a stack instead of recursion to handle nested sub-unit symbols.
|
||||||
"""
|
"""
|
||||||
_SYM_SYMBOL = Symbol("symbol")
|
|
||||||
pins: List[Tuple[float, float]] = []
|
pins: List[Tuple[float, float]] = []
|
||||||
|
|
||||||
# Separate top-level direct children into sub-unit symbols vs other nodes.
|
# Separate top-level direct children into sub-unit symbols vs other nodes.
|
||||||
@@ -583,8 +585,6 @@ class WireManager:
|
|||||||
Parses lib_symbols for pin local coordinates (unit-aware), then applies the KiCad
|
Parses lib_symbols for pin local coordinates (unit-aware), then applies the KiCad
|
||||||
transform chain (y-negate → mirror → rotate → translate) to each pin.
|
transform chain (y-negate → mirror → rotate → translate) to each pin.
|
||||||
"""
|
"""
|
||||||
_SYM_UNIT = Symbol("unit")
|
|
||||||
|
|
||||||
# Build {lib_id: sym_def} from the embedded lib_symbols section.
|
# Build {lib_id: sym_def} from the embedded lib_symbols section.
|
||||||
# We defer pin extraction until we know which unit each placed instance uses.
|
# We defer pin extraction until we know which unit each placed instance uses.
|
||||||
lib_sym_defs: dict = {}
|
lib_sym_defs: dict = {}
|
||||||
@@ -593,9 +593,7 @@ class WireManager:
|
|||||||
continue
|
continue
|
||||||
for sym_def in item[1:]:
|
for sym_def in item[1:]:
|
||||||
if not (
|
if not (
|
||||||
isinstance(sym_def, list)
|
isinstance(sym_def, list) and len(sym_def) > 1 and sym_def[0] == _SYM_SYMBOL
|
||||||
and len(sym_def) > 1
|
|
||||||
and sym_def[0] == Symbol("symbol")
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
lib_id = sym_def[1] if isinstance(sym_def[1], str) else str(sym_def[1])
|
lib_id = sym_def[1] if isinstance(sym_def[1], str) else str(sym_def[1])
|
||||||
@@ -605,7 +603,7 @@ class WireManager:
|
|||||||
# Transform each placed symbol's pins to world coordinates
|
# Transform each placed symbol's pins to world coordinates
|
||||||
world_positions: List[Tuple[float, float]] = []
|
world_positions: List[Tuple[float, float]] = []
|
||||||
for item in sch_data:
|
for item in sch_data:
|
||||||
if not (isinstance(item, list) and len(item) > 0 and item[0] == Symbol("symbol")):
|
if not (isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SYMBOL):
|
||||||
continue
|
continue
|
||||||
lib_id_part = next(
|
lib_id_part = next(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import traceback
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import sexpdata
|
||||||
from annotations import AnnotationLoader
|
from annotations import AnnotationLoader
|
||||||
|
from commands.wire_manager import WireManager
|
||||||
from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
|
from resources.resource_definitions import RESOURCE_DEFINITIONS, handle_resource_read
|
||||||
|
|
||||||
# Import tool schemas, resource definitions, and IPC API annotations
|
# Import tool schemas, resource definitions, and IPC API annotations
|
||||||
@@ -2531,7 +2533,6 @@ class KiCADInterface:
|
|||||||
"""Move a schematic component to a new position, dragging connected wires."""
|
"""Move a schematic component to a new position, dragging connected wires."""
|
||||||
logger.info("Moving schematic component")
|
logger.info("Moving schematic component")
|
||||||
try:
|
try:
|
||||||
import sexpdata as _sexpdata
|
|
||||||
from commands.wire_dragger import WireDragger
|
from commands.wire_dragger import WireDragger
|
||||||
|
|
||||||
schematic_path = params.get("schematicPath")
|
schematic_path = params.get("schematicPath")
|
||||||
@@ -2553,7 +2554,7 @@ class KiCADInterface:
|
|||||||
}
|
}
|
||||||
|
|
||||||
with open(schematic_path, "r", encoding="utf-8") as f:
|
with open(schematic_path, "r", encoding="utf-8") as f:
|
||||||
sch_data = _sexpdata.loads(f.read())
|
sch_data = sexpdata.loads(f.read())
|
||||||
|
|
||||||
# Find symbol and record old position
|
# Find symbol and record old position
|
||||||
found = WireDragger.find_symbol(sch_data, reference)
|
found = WireDragger.find_symbol(sch_data, reference)
|
||||||
@@ -2592,12 +2593,10 @@ class KiCADInterface:
|
|||||||
# Update symbol position
|
# Update symbol position
|
||||||
WireDragger.update_symbol_position(sch_data, reference, float(new_x), float(new_y))
|
WireDragger.update_symbol_position(sch_data, reference, float(new_x), float(new_y))
|
||||||
|
|
||||||
from commands.wire_manager import WireManager as _WireManager
|
WireManager.sync_junctions(sch_data)
|
||||||
|
|
||||||
_WireManager.sync_junctions(sch_data)
|
|
||||||
|
|
||||||
with open(schematic_path, "w", encoding="utf-8") as f:
|
with open(schematic_path, "w", encoding="utf-8") as f:
|
||||||
f.write(_sexpdata.dumps(sch_data))
|
f.write(sexpdata.dumps(sch_data))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -2630,46 +2629,77 @@ class KiCADInterface:
|
|||||||
"message": "schematicPath and reference are required",
|
"message": "schematicPath and reference are required",
|
||||||
}
|
}
|
||||||
|
|
||||||
schematic = SchematicManager.load_schematic(schematic_path)
|
sym_k = sexpdata.Symbol("symbol")
|
||||||
if not schematic:
|
prop_k = sexpdata.Symbol("property")
|
||||||
return {"success": False, "message": "Failed to load schematic"}
|
at_k = sexpdata.Symbol("at")
|
||||||
|
mirror_k = sexpdata.Symbol("mirror")
|
||||||
|
|
||||||
for symbol in schematic.symbol:
|
with open(schematic_path, "r", encoding="utf-8") as _f:
|
||||||
if not hasattr(symbol.property, "Reference"):
|
sch_data = sexpdata.load(_f)
|
||||||
|
|
||||||
|
target = None
|
||||||
|
for item in sch_data:
|
||||||
|
if not (isinstance(item, list) and item and item[0] == sym_k):
|
||||||
continue
|
continue
|
||||||
if symbol.property.Reference.value == reference:
|
ref_val = None
|
||||||
pos = list(symbol.at.value)
|
for sub in item[1:]:
|
||||||
while len(pos) < 3:
|
if (
|
||||||
pos.append(0)
|
isinstance(sub, list)
|
||||||
pos[2] = angle
|
and len(sub) >= 3
|
||||||
symbol.at.value = pos
|
and sub[0] == prop_k
|
||||||
|
and str(sub[1]).strip('"') == "Reference"
|
||||||
|
):
|
||||||
|
ref_val = str(sub[2]).strip('"')
|
||||||
|
break
|
||||||
|
if ref_val == reference:
|
||||||
|
target = item
|
||||||
|
break
|
||||||
|
|
||||||
if mirror:
|
if target is None:
|
||||||
if hasattr(symbol, "mirror"):
|
return {"success": False, "message": f"Component {reference} not found"}
|
||||||
symbol.mirror.value = mirror
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
f"Mirror '{mirror}' requested for {reference}, "
|
|
||||||
f"but symbol has no mirror attribute; skipped"
|
|
||||||
)
|
|
||||||
|
|
||||||
SchematicManager.save_schematic(schematic, schematic_path)
|
# Update (at x y rot)
|
||||||
|
at_node = None
|
||||||
|
mirror_idx = None
|
||||||
|
for idx, sub in enumerate(target[1:], start=1):
|
||||||
|
if not isinstance(sub, list) or not sub:
|
||||||
|
continue
|
||||||
|
if sub[0] == at_k:
|
||||||
|
at_node = sub
|
||||||
|
elif sub[0] == mirror_k:
|
||||||
|
mirror_idx = idx
|
||||||
|
|
||||||
# Re-open with raw sexpdata so sync_junctions can operate on
|
if at_node is None:
|
||||||
# the mutated file (mirrors the pattern used in move handler).
|
return {
|
||||||
import sexpdata as _sexpdata
|
"success": False,
|
||||||
|
"message": f"Component {reference} has no (at ...) node",
|
||||||
|
}
|
||||||
|
while len(at_node) < 3:
|
||||||
|
at_node.append(0)
|
||||||
|
if len(at_node) < 4:
|
||||||
|
at_node.append(angle)
|
||||||
|
else:
|
||||||
|
at_node[3] = angle
|
||||||
|
|
||||||
with open(schematic_path, "r", encoding="utf-8") as _f:
|
if mirror:
|
||||||
sch_data = _sexpdata.load(_f)
|
mirror_node = [mirror_k, sexpdata.Symbol(str(mirror))]
|
||||||
from commands.wire_manager import WireManager as _WireManager
|
if mirror_idx is not None:
|
||||||
|
target[mirror_idx] = mirror_node
|
||||||
|
else:
|
||||||
|
# Insert after (at ...) for stability
|
||||||
|
insert_at = len(target)
|
||||||
|
for idx, sub in enumerate(target[1:], start=1):
|
||||||
|
if isinstance(sub, list) and sub and sub[0] == at_k:
|
||||||
|
insert_at = idx + 1
|
||||||
|
break
|
||||||
|
target.insert(insert_at, mirror_node)
|
||||||
|
|
||||||
_WireManager.sync_junctions(sch_data)
|
WireManager.sync_junctions(sch_data)
|
||||||
with open(schematic_path, "w", encoding="utf-8") as _f:
|
|
||||||
_f.write(_sexpdata.dumps(sch_data))
|
|
||||||
|
|
||||||
return {"success": True, "reference": reference, "angle": angle}
|
with open(schematic_path, "w", encoding="utf-8") as _f:
|
||||||
|
_f.write(sexpdata.dumps(sch_data))
|
||||||
|
|
||||||
return {"success": False, "message": f"Component {reference} not found"}
|
return {"success": True, "reference": reference, "angle": angle}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error rotating schematic component: {e}")
|
logger.error(f"Error rotating schematic component: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user