4 Commits

Author SHA1 Message Date
Eugene Mikhantyev
7e67cb91c4 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>
2026-05-03 22:30:17 +01:00
Eugene Mikhantyev
118318c2f3 test(rotate_schematic_mirror): isolate sys.modules stubs to fix test pollution
`test_rotate_handler_no_crash` permanently replaced
`sys.modules["schemas.tool_schemas"].TOOL_SCHEMAS` with `[]`, leaking into
later tests. When test_wire_connectivity (or any test) ran after this one
and did `from schemas.tool_schemas import TOOL_SCHEMAS`, it got the empty
list and `TOOL_SCHEMAS["get_wire_connections"]` raised `TypeError: list
indices must be integers or slices, not str`.

Save the original sys.modules entries and restore them in a `finally`
block so the stubs are scoped to the test body. Whole suite now passes
(678 tests, previously 4 failed in TestSchema when run in suite order).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 22:02:22 +01:00
Michael Parment
a6b4f92e4b fix: stub annotations module and add python/ to sys.path in smoke test
Upstream added `from annotations import AnnotationLoader` and moved
`from commands.wire_manager import WireManager` to module-level in
kicad_interface.py. The smoke test now stubs annotations and ensures
python/ is on sys.path so commands.* imports resolve without installing.
2026-04-28 13:38:12 +02:00
Michael Parment
53e656b952 fix: rotate_schematic_component uses sexpdata API and drags wires
Previously the handler used kicad-skip to apply rotation and mirror.
kicad-skip has no API for (mirror x/y) on placed symbols, causing:
  'NoneType' object has no attribute 'value'

Fix:
- Rewrote _handle_rotate_schematic_component to use sexpdata (same
  approach as move_schematic_component) for both rotation and mirror
- Added WireDragger.compute_pin_positions_for_rotation: computes old
  and new pin world positions when rotation/mirror changes at fixed (x,y)
- Added WireDragger.update_symbol_rotation_mirror: updates (at) rotation
  and adds/removes/replaces the (mirror x/y) sexpdata token cleanly
- Connected wires now follow pin positions after rotate/mirror via the
  existing WireDragger.drag_wires infrastructure

Tests: 10 unit tests in tests/test_rotate_schematic_mirror.py covering
update_symbol_rotation_mirror, compute_pin_positions_for_rotation, and
a handler smoke test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 13:32:43 +02:00