diff --git a/python/commands/pin_locator.py b/python/commands/pin_locator.py index 5b1c2ae..e49289b 100644 --- a/python/commands/pin_locator.py +++ b/python/commands/pin_locator.py @@ -279,24 +279,26 @@ class PinLocator: pin_def_angle = pins[pin_number].get("angle", 0) - # Mirror flips the angle before applying symbol rotation. - # mirror_x flips the X component of local vectors → reflects across Y axis → 180 - angle. - # mirror_y flips the Y component of local vectors → reflects across X axis → negate angle. - if mirror_x: - pin_def_angle = (180 - pin_def_angle) % 360 - if mirror_y: - pin_def_angle = (-pin_def_angle) % 360 - - # Library symbols are Y-up; the schematic is Y-down. Match the - # lib→screen Y-flip applied by WireDragger.pin_world_xy (mirror in - # lib space → Y-flip → rotate → translate). For an angle this - # negates the Y component, i.e. negates the angle. Without this - # step pin angles are 180° off along the Y axis; before PR #145 - # this was masked because pin_world_xy was missing the same flip, - # so the two were "wrong in the same direction" and consistent. + # Mirror this exactly the way WireDragger.pin_world_xy does, in the + # same order: Y-flip (lib Y-up → screen Y-down) → mirror → rotate. + # + # Y-flip on an angle: negate it (reflects across X axis). pin_def_angle = (-pin_def_angle) % 360 - absolute_angle = (pin_def_angle + symbol_rotation) % 360 + # eeschema (symbol.h:43-44): + # (mirror x) = SYM_MIRROR_X = TRANSFORM(1,0,0,-1) → negates Y → + # reflect angle across X axis → -angle. + # (mirror y) = SYM_MIRROR_Y = TRANSFORM(-1,0,0,1) → negates X → + # reflect angle across Y axis → 180 - angle. + if mirror_x: + pin_def_angle = (-pin_def_angle) % 360 + if mirror_y: + pin_def_angle = (180 - pin_def_angle) % 360 + + # eeschema's rotation TRANSFORM is screen-CCW in Y-down, which is + # math-CW in standard atan2 convention — so subtract the rotation + # to match `pin_world_xy`'s `_rotate(..., -rotation)` call. + absolute_angle = (pin_def_angle - symbol_rotation) % 360 return absolute_angle except Exception: diff --git a/python/commands/wire_dragger.py b/python/commands/wire_dragger.py index aac0fde..7c8e110 100644 --- a/python/commands/wire_dragger.py +++ b/python/commands/wire_dragger.py @@ -156,15 +156,22 @@ class WireDragger: Compute the world coordinate of a pin given the symbol transform. Library pins are stored Y-up; the schematic is Y-down. Order matches - eeschema: mirror in lib space → Y-flip to screen → rotate → translate. - Without the Y-flip, polarized parts get pin 1/pin 2 silently swapped. + eeschema: Y-flip to screen → mirror → rotate (screen-CCW) → translate. + + eeschema's TRANSFORM matrix for rotation 90 is (0, 1, -1, 0) — + i.e. screen-CCW in Y-down: (x, y) → (y, -x). Our `_rotate` helper is + standard math (Y-up CCW), so we negate the rotation angle to convert. + + Mirror axis semantics match eeschema's symbol.h: + (mirror x) = SYM_MIRROR_X = TRANSFORM(1, 0, 0, -1) → negates Y. + (mirror y) = SYM_MIRROR_Y = TRANSFORM(-1, 0, 0, 1) → negates X. """ - lx, ly = px, py + lx, ly = px, -py # Y-flip: lib Y-up → screen Y-down if mirror_x: - lx = -lx + ly = -ly # SYM_MIRROR_X negates screen-Y if mirror_y: - ly = -ly - rx, ry = _rotate(lx, -ly, rotation) + lx = -lx # SYM_MIRROR_Y negates screen-X + rx, ry = _rotate(lx, ly, -rotation) # negate angle: math-CCW → screen-CCW return sym_x + rx, sym_y + ry @staticmethod diff --git a/tests/test_rotate_schematic_mirror.py b/tests/test_rotate_schematic_mirror.py index f5837ae..eb67545 100644 --- a/tests/test_rotate_schematic_mirror.py +++ b/tests/test_rotate_schematic_mirror.py @@ -163,19 +163,21 @@ def test_pin_positions_unchanged_at_same_transform(): assert old_xy == new_xy -def test_pin_positions_mirror_x_flips_x(): - """mirror_x should negate the local X coordinate before rotation.""" +def test_pin_positions_mirror_x_flips_y(): + """mirror_x = SYM_MIRROR_X = TRANSFORM(1,0,0,-1) negates the screen-Y + coordinate (eeschema symbol.h:43-44), not X. With the lib→screen Y-flip + applied first, this means the pin's screen Y is reflected back to lib Y.""" sch = _make_sch() # at (75, 105, 0), no mirror - fake_pins = {"1": {"x": 2.0, "y": 0.0}} + fake_pins = {"1": {"x": 0.0, "y": 2.0}} with patch.object(WireDragger, "get_pin_defs", return_value=fake_pins): pos = WireDragger.compute_pin_positions_for_rotation(sch, "Q1", 0.0, True, False) _, (old_xy, new_xy) = next(iter(pos.items())) - # old: pin at local (2, 0), world = (75+2, 105) = (77, 105) - assert abs(old_xy[0] - 77.0) < 1e-4 - # new: mirror_x → local (-2, 0), world = (75-2, 105) = (73, 105) - assert abs(new_xy[0] - 73.0) < 1e-4 + # old: pin at lib (0, 2). Y-flip → (0, -2). No mirror. World = (75, 105-2) = (75, 103). + assert abs(old_xy[1] - 103.0) < 1e-4 + # new: mirror_x → negate screen-Y → (0, 2). World = (75, 105+2) = (75, 107). + assert abs(new_xy[1] - 107.0) < 1e-4 # ---------------------------------------------------------------------------