Fix two regressions from PR #88 (rotate/mirror)

1. add_schematic_net_label failed on schematics with no existing labels.
   The clone-based path required a pre-existing label to copy from;
   the documented "fallback to sexpdata" was a misleading log line —
   the RuntimeError was caught and the call silently returned False.
   Restore hand-built sexpdata construction (without the buggy
   fields_autoplaced token, with orientation-aware justify).

2. get_pin_angle returned the wrong angle for every mirrored symbol
   (off by exactly 180°, all rotations, both mirror axes). The
   mirror_x and mirror_y formulas were swapped relative to the
   pin_world_xy convention — pin_world_xy mirrors a position by
   flipping its local axis component, so the matching angle
   transform is (180 - θ) for mirror_x and -θ for mirror_y.

Add regression tests:
- test_add_label_empty_schematic.py — first label on empty schematic,
  orientation-aware justify.
- test_get_pin_angle.py — full 24-case matrix
  (4 rotations × 3 mirror states × 2 pins).
This commit is contained in:
Eugene Mikhantyev
2026-04-29 21:40:39 +01:00
parent 109ba3f23a
commit 5907954b3e
4 changed files with 309 additions and 21 deletions

View File

@@ -306,25 +306,41 @@ class WireManager:
True if successful, False otherwise
"""
try:
from skip import Schematic
from sexpdata import Symbol as SexpSymbol
with open(schematic_path, "r", encoding="utf-8") as f:
sch_content = f.read()
schematic = Schematic(str(schematic_path))
sch_data = sexpdata.loads(sch_content)
existing_labels = list(schematic.label)
if not existing_labels:
logger.warning("No existing labels to clone from; falling back to sexpdata")
raise RuntimeError("no existing labels")
# Orientation-aware justify: KiCAD flips horizontal alignment for 180°/270°
justify_h = Symbol("right") if orientation in (180, 270) else Symbol("left")
new_label = existing_labels[0].clone()
new_label.value = text
new_label.at.value = [position[0], position[1], orientation]
label_sexp = [
Symbol(label_type),
text,
[Symbol("at"), position[0], position[1], orientation],
[
Symbol("effects"),
[Symbol("font"), [Symbol("size"), 1.27, 1.27]],
[Symbol("justify"), justify_h, Symbol("bottom")],
],
[Symbol("uuid"), str(uuid.uuid4())],
]
# justify: left for 0°/90°, right for 180°/270° (matches KiCAD convention)
justify_val = "right" if orientation in (180, 270) else "left"
new_label.effects.justify._tree[1] = SexpSymbol(justify_val)
sheet_instances_index = None
for i, item in enumerate(sch_data):
if isinstance(item, list) and len(item) > 0 and item[0] == _SYM_SHEET_INSTANCES:
sheet_instances_index = i
break
if sheet_instances_index is None:
# Sub-sheets in hierarchical designs don't have (sheet_instances).
sheet_instances_index = len(sch_data)
sch_data.insert(sheet_instances_index, label_sexp)
with open(schematic_path, "w", encoding="utf-8") as f:
f.write(sexpdata.dumps(sch_data))
schematic.write(str(schematic_path))
logger.info(f"Successfully added label '{text}' to {schematic_path.name}")
return True