diff --git a/rollingmill/tui.py b/rollingmill/tui.py index 797e496..1eeb021 100644 --- a/rollingmill/tui.py +++ b/rollingmill/tui.py @@ -774,8 +774,6 @@ def _wcs_handle_key(self, key: int) -> None: elif key in (10, 13): # Enter — activate self._m.set_active_wcs(self._wcs_sel) elif key in (ord('m'), ord('M')): # Move to stored origin - if self._wcs_sel == 0: - return # MCS origin is always (0,0,0,0) — no-op / already there if self._wcs_data and self._wcs_data[self._wcs_sel]: ox, oy, oz, oa = self._wcs_data[self._wcs_sel] self._m.move_to_machine_pos(ox, oy, oz, oa) @@ -1040,7 +1038,7 @@ def _draw_move_dialog(self, stdscr: curses.window, rows: int, cols: int) -> None if self._move_sel >= len(targets): self._move_sel = len(targets) - 1 - dh = min(6 + len(targets), rows - 2) + dh = min(5 + len(targets), rows - 2) dw = min(48, cols - 2) dy = (rows - dh) // 2 dx = (cols - dw) // 2 @@ -1053,13 +1051,12 @@ def _draw_move_dialog(self, stdscr: curses.window, rows: int, cols: int) -> None win.erase() win.box() - wcs_lbl = 'MCS' if self._m.active_wcs == 0 else f'WCS{self._m.active_wcs}' + wcs_lbl = _machine.wcs_label(self._m.active_wcs) title = f' Move To (using {wcs_lbl})' win.addstr(0, 2, title[:dw - 4], CP(_CP_HEADER) | BOLD) - win.addstr(1, 1, '─' * (dw - 2), CP(_CP_LABEL)) for i, (label, _action) in enumerate(targets): - row = 2 + i + row = 1 + i if row >= dh - 3: break is_sel = (i == self._move_sel) @@ -1095,7 +1092,7 @@ def _coord_entry_dialog(self, stdscr: curses.window) -> None: CP = curses.color_pair BOLD = curses.A_BOLD - wcs_lbl = 'MCS' if self._m.active_wcs == 0 else f'WCS{self._m.active_wcs}' + wcs_lbl = _machine.wcs_label(self._m.active_wcs) win.erase() win.box() diff --git a/tests/test_wcs_dialog_move.py b/tests/test_wcs_dialog_move.py new file mode 100644 index 0000000..6d9a4ba --- /dev/null +++ b/tests/test_wcs_dialog_move.py @@ -0,0 +1,64 @@ +"""'M' in the coordinates dialog moves to the selected coordinate system's origin, MCS included.""" +import struct + +import pytest + +from rollingmill.machine import MDX40A +from rollingmill.tui import TUI, _LogBuffer +from rollingmill.usb import MdxMockUSB + +DOWN = 258 # curses.KEY_DOWN + + +class _Recording(MdxMockUSB): + def __init__(self, responses=None): + super().__init__(responses=responses) + self.sent = [] + + def vend_set(self, wValue, data=b""): + self.sent.append((wValue, data)) + super().vend_set(wValue, data) + + def moves(self): + return [d for w, d in self.sent if w == 0x04f7] + + +def _dialog(responses=None): + link = _Recording(responses) + tui = TUI(MDX40A(link), _LogBuffer()) + tui._wcs_open_dialog() + link.sent.clear() + return tui, link + + +def _move_payload(x, y, z, a, speed=1800): + return struct.pack(">HH4i", speed, 0xFFFF, round(x * 1000), round(y * 1000), round(z * 1000), round(a * 1000)) + + +def test_m_on_mcs_sends_a_move_to_machine_zero(): + tui, link = _dialog() + assert tui._wcs_sel == 0 # MCS is the first row + tui._handle_key(ord("m")) + assert link.moves() == [_move_payload(0, 0, 0, 0)] + + +def test_m_on_mcs_is_bracketed_like_every_other_move(): + tui, link = _dialog() + tui._handle_key(ord("M")) + assert [w for w, _ in link.sent] == [0x1109, 0x04f7, 0x03f2, 0x1109] + assert link.sent[0][1] == b"\x00" and link.sent[-1][1] == b"\xff" + + +def test_m_on_a_stored_origin_moves_to_it(): + tui, link = _dialog({0x3203: struct.pack(">4i", 1000, 2000, -3000, 4000)}) # G54 (slot 3) + for _ in range(3): + tui._handle_key(DOWN) + tui._handle_key(ord("m")) + assert link.moves() == [_move_payload(1, 2, -3, 4)] + + +@pytest.mark.parametrize("key", [ord("x"), ord("y"), ord("z"), ord("a"), ord("0")]) +def test_the_overwrite_keys_still_refuse_mcs(key): + tui, link = _dialog() + tui._handle_key(key) + assert link.sent == [] # MCS origin cannot be overwritten diff --git a/tests/test_wcs_titles.py b/tests/test_wcs_titles.py new file mode 100644 index 0000000..503748c --- /dev/null +++ b/tests/test_wcs_titles.py @@ -0,0 +1,81 @@ +"""Dialog titles name the active coordinate system the same way as the header (wcs_label).""" +import curses + +import pytest + +from rollingmill import machine as M +from rollingmill.machine import MDX40A +from rollingmill.tui import TUI, _LogBuffer +from rollingmill.usb import MdxMockUSB + + +class _FakeWin: + """Just enough of a curses window to record what a dialog draws.""" + + def __init__(self, h=24, w=80): + self.h, self.w = h, w + self.drawn = [] + + def getmaxyx(self): + return self.h, self.w + + def addstr(self, y, x, text, attr=0): + self.drawn.append(text) + + def erase(self): + pass + + def box(self): + pass + + def refresh(self): + pass + + def keypad(self, flag): + pass + + def nodelay(self, flag): + pass + + def move(self, y, x): + pass + + def getch(self): + return 27 # Esc: leave modal dialogs at once + + +@pytest.fixture +def fake_curses(monkeypatch): + win = _FakeWin() + monkeypatch.setattr(curses, "newwin", lambda *a, **k: win) + monkeypatch.setattr(curses, "color_pair", lambda n: 0) + monkeypatch.setattr(curses, "curs_set", lambda n: 0) + monkeypatch.setattr(curses, "noecho", lambda: None) + return win + + +@pytest.mark.parametrize("slot", range(0, 9)) +def test_move_to_title_uses_the_slot_names(slot, fake_curses): + m = MDX40A(MdxMockUSB()) + m.set_active_wcs(slot) + TUI(m, _LogBuffer())._draw_move_dialog(_FakeWin(), 30, 100) + title = next(t for t in fake_curses.drawn if "Move To" in t) + assert title.strip() == f"Move To (using {M.WCS_SLOT_NAMES[slot]})" + + +def test_move_to_title_for_user_and_g54_reads_naturally(fake_curses): + m = MDX40A(MdxMockUSB()) + for slot, name in ((1, "User (RML-1)"), (3, "WCS1 (G54)")): + fake_curses.drawn.clear() + m.set_active_wcs(slot) + TUI(m, _LogBuffer())._draw_move_dialog(_FakeWin(), 30, 100) + assert any(name in t for t in fake_curses.drawn) + + +@pytest.mark.parametrize("slot", range(0, 9)) +def test_move_to_position_title_uses_the_slot_names(slot, fake_curses): + m = MDX40A(MdxMockUSB()) + m.set_active_wcs(slot) + TUI(m, _LogBuffer())._coord_entry_dialog(_FakeWin()) + title = next(t for t in fake_curses.drawn if "Move to position" in t) + assert title.strip() == f"Move to position ({M.WCS_SLOT_NAMES[slot]})"