fix(pin_world_xy): align rotation direction and mirror axis with eeschema
Two bugs in WireDragger.pin_world_xy (and corresponding bugs in PinLocator.get_pin_angle) caused pin coordinates and angles to land on the wrong pin in 4 of 8 polarized cases (rot=90, rot=270, mirror x on a vertical part, mirror y on a vertical part). Verified end-to-end against `kicad-cli sch export netlist`. (1) Rotation direction. After PR #145's `-ly` Y-flip, calling the standard math (Y-up CCW) `_rotate` is effectively CW in screen Y-down. eeschema's TRANSFORM(0,1,-1,0) for rot=90 is screen-CCW. They agreed at 0° and 180° (where the rotation matrices coincide) but disagreed at 90° and 270°. (2) Mirror axis semantics swapped. Per eeschema symbol.h:43-44, SYM_MIRROR_X = TRANSFORM(1,0,0,-1) negates Y, and SYM_MIRROR_Y = TRANSFORM(-1,0,0,1) negates X. Our code did the inverse: `mirror_x` negated the X component and `mirror_y` negated the Y component. Fix shape for `_rotate`: chose option (b) — leave `_rotate` as standard math and negate the angle at the call site (`_rotate(lx, ly, -rotation)`). This converts math-CCW to screen-CCW without disturbing `TestRotatePoint`'s direct expectations of `_rotate`. Final composition order in `pin_world_xy` matches eeschema's parser (rotation set first into m_transform, then mirror composed via `new = old * temp` so the mirror is applied first to the coordinate): 1. Y-flip: ly = -ly (lib Y-up → screen Y-down) 2. Mirror: if mirror_x: ly = -ly (negate screen-Y) if mirror_y: lx = -lx (negate screen-X) 3. Rotate: _rotate(lx, ly, -rotation) (screen-CCW) 4. Translate: add (sym_x, sym_y) Verified by hand for {rot=90, rot=270} × {none, mirror_x, mirror_y} against the TRANSFORM matrices in transform.cpp:44 and symbol.h:43-44. `PinLocator.get_pin_angle` mirrors the same composition in angle space. For an angle, Y-flip and mirror_x both negate the angle; mirror_y maps to (180 - angle). The screen-CCW rotation in `pin_world_xy` corresponds to subtracting (not adding) the symbol rotation in standard atan2 convention — fixed accordingly. Geometry test (`test_get_pin_angle.py::test_get_pin_angle_matches_geometric_expectation`) derives expected angles from `pin_world_xy` itself, so it pins the two together. `tests/test_rotate_schematic_mirror.py::test_pin_positions_mirror_x_flips_x` encoded the OLD inverted semantics and is updated/renamed to `test_pin_positions_mirror_x_flips_y` with a pin that has non-zero Y so the assertion is meaningful under the corrected semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user