fix(ipc): rotate_component uses absolute angle (matches schema) (#159)
* Fix: IPC rotate_component now uses absolute angle as documented The IPC rotate handler was adding the angle to the current rotation (relative), but the schema documents it as absolute. This caused unexpected behavior where setting angle=0 had no effect on a component already at 180°. Now correctly sets the rotation to the exact angle specified, matching the SWIG backend behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs(changelog): add unreleased entry for rotate_component absolute-angle fix --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
tests/test_ipc_rotate_absolute.py
Normal file
76
tests/test_ipc_rotate_absolute.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Regression test for IPC rotate_component absolute-angle behavior.
|
||||
|
||||
Bug: ``_ipc_rotate_component`` accumulated the supplied angle on top of the
|
||||
current rotation, but the tool schema documents ``rotation`` as the absolute
|
||||
target angle. Two consecutive ``rotate_component(angle=90)`` calls would land
|
||||
the part at 180° instead of leaving it at 90°.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "python"))
|
||||
|
||||
|
||||
def _make_iface(current_rotation: float) -> Any:
|
||||
with patch("kicad_interface.USE_IPC_BACKEND", True):
|
||||
from kicad_interface import KiCADInterface
|
||||
|
||||
iface = KiCADInterface.__new__(KiCADInterface)
|
||||
|
||||
iface.use_ipc = True
|
||||
iface.board = None
|
||||
iface.ipc_board_api = MagicMock()
|
||||
iface.ipc_board_api.list_components.return_value = [
|
||||
{
|
||||
"reference": "R1",
|
||||
"position": {"x": 10.0, "y": 20.0, "unit": "mm"},
|
||||
"rotation": current_rotation,
|
||||
}
|
||||
]
|
||||
iface.ipc_board_api.move_component.return_value = True
|
||||
return iface
|
||||
|
||||
|
||||
def test_rotation_is_absolute_not_additive():
|
||||
"""angle=90 sets rotation to 90° regardless of current rotation."""
|
||||
iface = _make_iface(current_rotation=270)
|
||||
result = iface._ipc_rotate_component({"reference": "R1", "angle": 90})
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["newRotation"] == 90
|
||||
_, kwargs = iface.ipc_board_api.move_component.call_args
|
||||
assert kwargs["rotation"] == 90
|
||||
|
||||
|
||||
def test_rotation_is_absolute_when_current_is_zero():
|
||||
iface = _make_iface(current_rotation=0)
|
||||
result = iface._ipc_rotate_component({"reference": "R1", "angle": 180})
|
||||
assert result["newRotation"] == 180
|
||||
|
||||
|
||||
def test_rotation_normalized_to_modulo_360():
|
||||
"""Values >= 360 are normalized."""
|
||||
iface = _make_iface(current_rotation=0)
|
||||
result = iface._ipc_rotate_component({"reference": "R1", "angle": 450})
|
||||
assert result["newRotation"] == 90
|
||||
|
||||
|
||||
def test_position_preserved_during_rotate():
|
||||
"""Rotating must not move the part."""
|
||||
iface = _make_iface(current_rotation=0)
|
||||
iface._ipc_rotate_component({"reference": "R1", "angle": 90})
|
||||
_, kwargs = iface.ipc_board_api.move_component.call_args
|
||||
assert kwargs["x"] == 10.0
|
||||
assert kwargs["y"] == 20.0
|
||||
|
||||
|
||||
def test_unknown_reference_returns_failure():
|
||||
iface = _make_iface(current_rotation=0)
|
||||
result = iface._ipc_rotate_component({"reference": "DOES_NOT_EXIST", "angle": 90})
|
||||
assert result["success"] is False
|
||||
iface.ipc_board_api.move_component.assert_not_called()
|
||||
Reference in New Issue
Block a user