feat: Add counterclockwise routing and LED editing
- Добавлено автоматическое построение блока против часовой стрелки. - Добавлено редактирование координат LED через контекстное меню. - Добавлено перемещение светодиодов через Alt + ЛКМ. - Убран лишний столбец "Источник" из таблицы выбранных LED. - Индексы LED сделаны визуальной подсказкой, которая не мешает кликам и перетаскиванию точек. - Добавлены проверки для новых сценариев редактирования, маршрутизации, таблицы и сборочного имени.
This commit is contained in:
parent
207d6ca86c
commit
2832914343
83
canvas.py
83
canvas.py
@ -56,7 +56,10 @@ class LedCanvas(QGraphicsView):
|
||||
selectionChanged = Signal(list)
|
||||
deleteSelectionRequested = Signal()
|
||||
deletePointsRequested = Signal()
|
||||
editPointRequested = Signal(int)
|
||||
pointMoveFinished = Signal(int, float, float)
|
||||
autoBlockFromPointRequested = Signal(int)
|
||||
counterclockwiseBlockFromPointRequested = Signal(int)
|
||||
brushStrokeStarted = Signal(bool)
|
||||
brushPointTouched = Signal(int)
|
||||
brushStrokeFinished = Signal()
|
||||
@ -108,6 +111,7 @@ class LedCanvas(QGraphicsView):
|
||||
self._is_panning = False
|
||||
self._last_pan_pos = None
|
||||
self._last_mouse_pos = None
|
||||
self._point_drag: dict | None = None
|
||||
self._brush_enabled = False
|
||||
self._add_led_enabled = False
|
||||
self._is_brush_stroke = False
|
||||
@ -621,6 +625,9 @@ class LedCanvas(QGraphicsView):
|
||||
self._start_pan(event.pos())
|
||||
event.accept()
|
||||
return
|
||||
if event.button() == Qt.LeftButton and event.modifiers() & Qt.AltModifier and self._start_point_drag(event.pos()):
|
||||
event.accept()
|
||||
return
|
||||
if self._background_edit_mode and event.button() == Qt.LeftButton and self._start_background_edit(event.pos()):
|
||||
event.accept()
|
||||
return
|
||||
@ -661,6 +668,10 @@ class LedCanvas(QGraphicsView):
|
||||
self._move_background_edit(event.pos())
|
||||
event.accept()
|
||||
return
|
||||
if self._point_drag is not None:
|
||||
self._move_point_drag(event.pos())
|
||||
event.accept()
|
||||
return
|
||||
if self._brush_enabled and self._is_brush_stroke:
|
||||
self._update_brush_zone(event.pos())
|
||||
self._touch_brush_point(event.pos())
|
||||
@ -685,6 +696,10 @@ class LedCanvas(QGraphicsView):
|
||||
self._finish_background_edit()
|
||||
event.accept()
|
||||
return
|
||||
if event.button() == Qt.LeftButton and self._point_drag is not None:
|
||||
self._finish_point_drag(event.pos())
|
||||
event.accept()
|
||||
return
|
||||
if self._brush_enabled and event.button() == Qt.LeftButton and self._is_brush_stroke:
|
||||
self._update_brush_zone(event.pos())
|
||||
self._touch_brush_point(event.pos())
|
||||
@ -695,6 +710,51 @@ class LedCanvas(QGraphicsView):
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def _start_point_drag(self, viewport_pos) -> bool: # noqa: ANN001
|
||||
if self.document is None:
|
||||
return False
|
||||
item = self._led_item_at(viewport_pos)
|
||||
if item is None:
|
||||
return False
|
||||
scene_pos = self.mapToScene(viewport_pos)
|
||||
self._point_drag = {
|
||||
"point_id": item.point_id,
|
||||
"item": item,
|
||||
"offset": item.pos() - scene_pos,
|
||||
"start_pos": item.pos(),
|
||||
}
|
||||
self.select_points([item.point_id])
|
||||
self.setDragMode(QGraphicsView.NoDrag)
|
||||
self.viewport().setCursor(Qt.ClosedHandCursor)
|
||||
return True
|
||||
|
||||
def _move_point_drag(self, viewport_pos) -> None: # noqa: ANN001
|
||||
if self._point_drag is None:
|
||||
return
|
||||
item = self._point_drag.get("item")
|
||||
if item is None or not isValid(item):
|
||||
return
|
||||
offset = self._point_drag.get("offset", QPointF(0.0, 0.0))
|
||||
item.setPos(self.mapToScene(viewport_pos) + offset)
|
||||
self._position_line_index_labels()
|
||||
|
||||
def _finish_point_drag(self, viewport_pos) -> None: # noqa: ANN001
|
||||
if self._point_drag is None:
|
||||
return
|
||||
self._move_point_drag(viewport_pos)
|
||||
point_id = int(self._point_drag.get("point_id", -1))
|
||||
item = self._point_drag.get("item")
|
||||
start_pos = self._point_drag.get("start_pos")
|
||||
self._point_drag = None
|
||||
self.setDragMode(QGraphicsView.NoDrag if self._brush_enabled else QGraphicsView.RubberBandDrag)
|
||||
self._apply_default_cursor()
|
||||
if item is None or not isValid(item) or point_id < 0:
|
||||
return
|
||||
if isinstance(start_pos, QPointF) and (item.pos() - start_pos).manhattanLength() < 0.001:
|
||||
return
|
||||
document_point = self._document_point_from_scene(item.pos())
|
||||
self.pointMoveFinished.emit(point_id, document_point.x(), document_point.y())
|
||||
|
||||
def _start_background_edit(self, viewport_pos) -> bool: # noqa: ANN001
|
||||
if self.document is None or self._background_item is None or not isValid(self._background_item):
|
||||
return False
|
||||
@ -815,6 +875,8 @@ class LedCanvas(QGraphicsView):
|
||||
cursor_shape = Qt.ClosedHandCursor
|
||||
elif self._background_drag is not None:
|
||||
cursor_shape = Qt.ClosedHandCursor if self._background_drag.get("mode") == "move" else Qt.SizeAllCursor
|
||||
elif self._point_drag is not None:
|
||||
cursor_shape = Qt.ClosedHandCursor
|
||||
elif self._space_pressed and self._can_pan_with_space():
|
||||
cursor_shape = Qt.OpenHandCursor
|
||||
elif self._brush_enabled or self._add_led_enabled:
|
||||
@ -852,10 +914,12 @@ class LedCanvas(QGraphicsView):
|
||||
self._apply_default_cursor()
|
||||
|
||||
def contextMenuEvent(self, event) -> None: # noqa: ANN001
|
||||
item = self.itemAt(event.pos())
|
||||
point_id = item.point_id if isinstance(item, LedItem) and isValid(item) else self._single_selected_point_id()
|
||||
item = self._led_item_at(event.pos())
|
||||
point_id = item.point_id if item is not None else self._single_selected_point_id()
|
||||
|
||||
menu = QMenu(self)
|
||||
edit_action = menu.addAction("Редактировать координаты")
|
||||
edit_action.setEnabled(point_id is not None)
|
||||
delete_action = menu.addAction("Удалить фиксацию выбранных LED")
|
||||
delete_action.setEnabled(self._selection_has_assignment())
|
||||
delete_points_action = menu.addAction("Удалить выбранные точки")
|
||||
@ -864,10 +928,14 @@ class LedCanvas(QGraphicsView):
|
||||
clear_selection_action.setEnabled(bool(self.selected_point_ids()))
|
||||
auto_block_action = menu.addAction("Автоматически добавить блок")
|
||||
auto_block_action.setEnabled(point_id is not None)
|
||||
counterclockwise_block_action = menu.addAction("Автоматически добавить блок против часовой")
|
||||
counterclockwise_block_action.setEnabled(point_id is not None)
|
||||
fit_action = menu.addAction("Показать все точки")
|
||||
fit_action.setEnabled(self.document is not None and bool(self.document.points))
|
||||
chosen = menu.exec(event.globalPos())
|
||||
if chosen == delete_action:
|
||||
if chosen == edit_action and point_id is not None:
|
||||
self.editPointRequested.emit(point_id)
|
||||
elif chosen == delete_action:
|
||||
self.deleteSelectionRequested.emit()
|
||||
elif chosen == delete_points_action:
|
||||
self.deletePointsRequested.emit()
|
||||
@ -875,9 +943,17 @@ class LedCanvas(QGraphicsView):
|
||||
self.select_points([])
|
||||
elif chosen == auto_block_action and point_id is not None:
|
||||
self.autoBlockFromPointRequested.emit(point_id)
|
||||
elif chosen == counterclockwise_block_action and point_id is not None:
|
||||
self.counterclockwiseBlockFromPointRequested.emit(point_id)
|
||||
elif chosen == fit_action:
|
||||
self.fit_to_content()
|
||||
|
||||
def _led_item_at(self, viewport_pos) -> LedItem | None: # noqa: ANN001
|
||||
for item in self.items(viewport_pos):
|
||||
if isinstance(item, LedItem) and isValid(item):
|
||||
return item
|
||||
return None
|
||||
|
||||
def _emit_selection(self) -> None:
|
||||
selected_ids = {
|
||||
item.point_id
|
||||
@ -922,6 +998,7 @@ class LedCanvas(QGraphicsView):
|
||||
label.setZValue(40)
|
||||
label.setFlag(QGraphicsItem.ItemIgnoresTransformations, True)
|
||||
label.setAcceptedMouseButtons(Qt.NoButton)
|
||||
label.setEnabled(False)
|
||||
self.scene().addItem(label)
|
||||
self.line_index_label_items[point_id] = label
|
||||
self._position_line_index_labels()
|
||||
|
||||
1
icons.py
1
icons.py
@ -23,6 +23,7 @@ LUCIDE_PATHS = {
|
||||
"paintbrush": '<path d="m14.62 17.9-1.48 1.48a2.12 2.12 0 1 1-3-3l1.48-1.48"/><path d="m16.1 16.42 5.15-5.15a4.95 4.95 0 0 0-7-7L9.1 9.42"/><path d="m7.5 11 5.5 5.5"/><path d="m9 9-6 6v4h4l6-6"/>',
|
||||
"plus": '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||
"redo-2": '<path d="m15 14 5-5-5-5"/><path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5 5.5 5.5 0 0 0 9.5 20H13"/>',
|
||||
"rotate-ccw": '<path d="M3 12a9 9 0 1 0 2.64-6.36L3 8"/><path d="M3 3v5h5"/>',
|
||||
"rotate-cw": '<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8"/><path d="M21 3v5h-5"/>',
|
||||
"save": '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/>',
|
||||
"save-all": '<path d="M10 2v3a1 1 0 0 0 1 1h5"/><path d="M18 18v-6a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6"/><path d="M18 22H6a2 2 0 0 1-2-2V6"/><path d="M8 2h8l4 4v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2"/>',
|
||||
|
||||
132
main_window.py
132
main_window.py
@ -9,7 +9,11 @@ from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QAbstractScrollArea,
|
||||
QApplication,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QDoubleSpinBox,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
@ -37,7 +41,14 @@ try:
|
||||
from .background_dialog import BackgroundDialog
|
||||
from .canvas import LedCanvas
|
||||
from .icons import lucide_icon
|
||||
from .localization import ask_question, localize_message_box, show_critical, show_information, show_warning
|
||||
from .localization import (
|
||||
ask_question,
|
||||
localize_dialog_buttons,
|
||||
localize_message_box,
|
||||
show_critical,
|
||||
show_information,
|
||||
show_warning,
|
||||
)
|
||||
from .models import LedBlock, LedLine, MappingDocument
|
||||
from .panel_mapping_generator import generate_panel_mapping_from_document
|
||||
from .settings import DEFAULT_SHORTCUTS, AppSettings
|
||||
@ -48,7 +59,14 @@ except ImportError:
|
||||
from background_dialog import BackgroundDialog
|
||||
from canvas import LedCanvas
|
||||
from icons import lucide_icon
|
||||
from localization import ask_question, localize_message_box, show_critical, show_information, show_warning
|
||||
from localization import (
|
||||
ask_question,
|
||||
localize_dialog_buttons,
|
||||
localize_message_box,
|
||||
show_critical,
|
||||
show_information,
|
||||
show_warning,
|
||||
)
|
||||
from models import LedBlock, LedLine, MappingDocument
|
||||
from panel_mapping_generator import generate_panel_mapping_from_document
|
||||
from settings import DEFAULT_SHORTCUTS, AppSettings
|
||||
@ -67,12 +85,11 @@ MAX_WIDGET_WIDTH = 16777215
|
||||
ID_COLUMN = 0
|
||||
X_COLUMN = 1
|
||||
Y_COLUMN = 2
|
||||
SOURCE_COLUMN = 3
|
||||
ROW_COLUMN = 4
|
||||
COL_COLUMN = 5
|
||||
LINE_COLUMN = 6
|
||||
BLOCK_COLUMN = 7
|
||||
UID_COLUMN = 8
|
||||
ROW_COLUMN = 3
|
||||
COL_COLUMN = 4
|
||||
LINE_COLUMN = 5
|
||||
BLOCK_COLUMN = 6
|
||||
UID_COLUMN = 7
|
||||
_NATIVE_VIRTUAL_KEY_NAMES = {
|
||||
0x08: "Backspace",
|
||||
0x09: "Tab",
|
||||
@ -170,7 +187,10 @@ class MainWindow(QMainWindow):
|
||||
self.canvas.selectionChanged.connect(self._on_selection_changed)
|
||||
self.canvas.deleteSelectionRequested.connect(self.clear_selected_point_assignments)
|
||||
self.canvas.deletePointsRequested.connect(self.delete_selected_points)
|
||||
self.canvas.editPointRequested.connect(self.edit_point_coordinates)
|
||||
self.canvas.pointMoveFinished.connect(self.move_point_from_canvas)
|
||||
self.canvas.autoBlockFromPointRequested.connect(self.auto_block_from_point)
|
||||
self.canvas.counterclockwiseBlockFromPointRequested.connect(self.auto_block_from_point_counterclockwise)
|
||||
self.canvas.addLedRequested.connect(self.add_manual_led)
|
||||
self.canvas.brushStrokeStarted.connect(self.start_brush_stroke)
|
||||
self.canvas.brushPointTouched.connect(self.add_brush_point_to_block)
|
||||
@ -185,14 +205,14 @@ class MainWindow(QMainWindow):
|
||||
self.tree.itemSelectionChanged.connect(self._on_tree_selection)
|
||||
self.tree.customContextMenuRequested.connect(self._show_tree_menu)
|
||||
|
||||
self.table = QTableWidget(0, 9)
|
||||
self.table = QTableWidget(0, 8)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
["ID", "X", "Y", "Источник", "Ряд", "Кол.", "Линия", "Блок", "UID"]
|
||||
["ID", "X", "Y", "Ряд", "Кол.", "Линия", "Блок", "UID"]
|
||||
)
|
||||
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
|
||||
self.table.horizontalHeader().setMinimumSectionSize(24)
|
||||
self.table.horizontalHeader().setStretchLastSection(False)
|
||||
for column, width in enumerate((48, 72, 72, 72, 54, 54, 58, 58, 54)):
|
||||
for column, width in enumerate((48, 72, 72, 54, 54, 58, 58, 54)):
|
||||
self.table.setColumnWidth(column, width)
|
||||
self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
self.table.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
||||
@ -271,6 +291,14 @@ class MainWindow(QMainWindow):
|
||||
add_action("add_block", "Блок из выбора", self.add_block_from_selection, "plus", None, "Создать блок в активной линии")
|
||||
add_action("auto_blocks", "Автоблоки", self.auto_blocks_from_selection, "layers", QKeySequence("Ctrl+Shift+B"), "Разбить выделение на связанные блоки")
|
||||
add_action("clockwise", "По часовой", self.auto_connect_active_line, "rotate-cw", None, "Пересчитать порядок LED по часовой стрелке")
|
||||
add_action(
|
||||
"counterclockwise",
|
||||
"Против часовой",
|
||||
self.auto_block_counterclockwise_from_selection_seed,
|
||||
"rotate-ccw",
|
||||
None,
|
||||
"Автоматически добавить блок от выбранной LED против часовой стрелки",
|
||||
)
|
||||
add_action("rebuild_route", "Пересобрать", self.rebuild_active_line_blocks, "redo-2", None, "Пересобрать блоки активной линии по правилу 34 мм")
|
||||
brush_action = add_action("brush", "Кисть", self.toggle_brush_mode, "paintbrush", QKeySequence("B"), "Рисовать выбранный блок по точкам на схеме")
|
||||
brush_action.setCheckable(True)
|
||||
@ -385,6 +413,7 @@ class MainWindow(QMainWindow):
|
||||
tools_menu.addAction(self.actions["generate_script"])
|
||||
tools_menu.addSeparator()
|
||||
tools_menu.addAction(self.actions["auto_blocks"])
|
||||
tools_menu.addAction(self.actions["counterclockwise"])
|
||||
tools_menu.addSeparator()
|
||||
tools_menu.addAction(self.actions["brush"])
|
||||
|
||||
@ -997,6 +1026,61 @@ class MainWindow(QMainWindow):
|
||||
self.status.setText(f"Добавлен свободный LED ID {point.source_id}: X={point.x:.3f}, Y={point.y:.3f}.")
|
||||
self._refresh_ui_state()
|
||||
|
||||
def edit_point_coordinates(self, point_id: int) -> None:
|
||||
if not 0 <= point_id < len(self.document.points):
|
||||
return
|
||||
point = self.document.get_point(point_id)
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle(f"Редактировать LED ID {point.source_id}")
|
||||
layout = QVBoxLayout(dialog)
|
||||
form = QFormLayout()
|
||||
x_spin = self._coordinate_spin_box(point.x)
|
||||
y_spin = self._coordinate_spin_box(point.y)
|
||||
form.addRow("X", x_spin)
|
||||
form.addRow("Y", y_spin)
|
||||
layout.addLayout(form)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
localize_dialog_buttons(buttons)
|
||||
buttons.accepted.connect(dialog.accept)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
layout.addWidget(buttons)
|
||||
if dialog.exec() != QDialog.Accepted:
|
||||
return
|
||||
self.update_point_coordinates(point_id, x_spin.value(), y_spin.value(), source="dialog")
|
||||
|
||||
def move_point_from_canvas(self, point_id: int, x: float, y: float) -> None:
|
||||
self.update_point_coordinates(point_id, x, y, source="drag")
|
||||
|
||||
def update_point_coordinates(self, point_id: int, x: float, y: float, *, source: str = "edit") -> bool:
|
||||
if not 0 <= point_id < len(self.document.points):
|
||||
return False
|
||||
point = self.document.get_point(point_id)
|
||||
if abs(point.x - x) < 1e-9 and abs(point.y - y) < 1e-9:
|
||||
self.canvas.set_document(self.document, reset_view=False)
|
||||
self.canvas.select_points([point_id])
|
||||
self._on_selection_changed([point_id])
|
||||
return False
|
||||
self._push_undo()
|
||||
self.document.update_point_position(point.uid, x=x, y=y)
|
||||
self._dirty = True
|
||||
self.canvas.set_document(self.document, reset_view=False)
|
||||
self.canvas.select_points([point.uid])
|
||||
self.refresh_tree()
|
||||
self._on_selection_changed([point.uid])
|
||||
action_text = "перемещен" if source == "drag" else "обновлен"
|
||||
self.status.setText(f"LED ID {point.source_id}: координаты {action_text}.")
|
||||
self._refresh_ui_state([point.uid])
|
||||
return True
|
||||
|
||||
def _coordinate_spin_box(self, value: float) -> QDoubleSpinBox:
|
||||
spin = QDoubleSpinBox()
|
||||
spin.setRange(-1_000_000_000.0, 1_000_000_000.0)
|
||||
spin.setDecimals(6)
|
||||
spin.setSingleStep(1.0)
|
||||
spin.setValue(float(value))
|
||||
spin.setKeyboardTracking(False)
|
||||
return spin
|
||||
|
||||
def add_block_from_selection(self) -> None:
|
||||
line_id = self._require_active_line()
|
||||
if line_id is None:
|
||||
@ -1034,6 +1118,19 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
|
||||
def auto_block_from_point(self, point_id: int) -> None:
|
||||
self._auto_block_from_point(point_id, direction="clockwise")
|
||||
|
||||
def auto_block_from_point_counterclockwise(self, point_id: int) -> None:
|
||||
self._auto_block_from_point(point_id, direction="counterclockwise")
|
||||
|
||||
def auto_block_counterclockwise_from_selection_seed(self) -> None:
|
||||
point_ids = self.canvas.selected_point_ids()
|
||||
if len(point_ids) != 1:
|
||||
show_information(self, "Нет стартовой точки", "Выберите одну LED-точку для построения блока против часовой.")
|
||||
return
|
||||
self.auto_block_from_point_counterclockwise(point_ids[0])
|
||||
|
||||
def _auto_block_from_point(self, point_id: int, *, direction: str = "clockwise") -> None:
|
||||
line_id = self.active_line_id
|
||||
needs_new_line = line_id is None and not self.document.lines
|
||||
if line_id is None and self.document.lines:
|
||||
@ -1046,7 +1143,12 @@ class MainWindow(QMainWindow):
|
||||
f"Новая модель поддерживает максимум {MAX_LINES} линии. Удалите лишнюю линию, чтобы создать новую.",
|
||||
)
|
||||
return
|
||||
preview = self.document.clockwise_block_from_seed_preview(point_id)
|
||||
is_counterclockwise = direction == "counterclockwise"
|
||||
preview = (
|
||||
self.document.counterclockwise_block_from_seed_preview(point_id)
|
||||
if is_counterclockwise
|
||||
else self.document.clockwise_block_from_seed_preview(point_id)
|
||||
)
|
||||
if not preview:
|
||||
show_information(self, "Нет блока", "Не удалось найти связанный блок от выбранной LED.")
|
||||
return
|
||||
@ -1060,8 +1162,10 @@ class MainWindow(QMainWindow):
|
||||
block = self.document.add_clockwise_block_from_group(line_id, point_id, preview)
|
||||
self._dirty = True
|
||||
self.canvas.select_points(block.point_ids)
|
||||
direction_text = "против часовой" if is_counterclockwise else "по часовой"
|
||||
self._after_mapping_change(
|
||||
f"Автоматически добавлен {block.name}: {len(block.point_ids)} LED, стартовая точка {point_id}. "
|
||||
f"Автоматически добавлен {block.name} {direction_text}: {len(block.point_ids)} LED, "
|
||||
f"стартовая точка {point_id}. "
|
||||
f"{self._unfolding_summary(line_id)}",
|
||||
selection_already_updated=True,
|
||||
)
|
||||
@ -1611,7 +1715,6 @@ class MainWindow(QMainWindow):
|
||||
"-" if point.line_index is None else point.line_index,
|
||||
self._format_coordinate(point.x),
|
||||
self._format_coordinate(point.y),
|
||||
point.source_id,
|
||||
point.row,
|
||||
point.col,
|
||||
"-" if point.line_id is None else point.line_id,
|
||||
@ -1863,6 +1966,7 @@ class MainWindow(QMainWindow):
|
||||
"add_block": has_points and has_active_line,
|
||||
"auto_blocks": has_points and has_active_line and has_selection,
|
||||
"clockwise": has_active_line,
|
||||
"counterclockwise": has_points and len(point_ids) == 1,
|
||||
"rebuild_route": has_active_line,
|
||||
"brush": has_points,
|
||||
"delete": (has_selection and selected_has_assignment) or self._tree_has_focus_with_item(),
|
||||
|
||||
66
models.py
66
models.py
@ -822,6 +822,12 @@ class MappingDocument:
|
||||
group_ids = self.clockwise_block_from_seed_preview(start_point_id)
|
||||
return self.add_clockwise_block_from_group(line_id, start_point_id, group_ids)
|
||||
|
||||
def add_counterclockwise_block_from_seed(self, line_id: int, start_point_id: int) -> LedBlock:
|
||||
if not 0 <= start_point_id < len(self.points):
|
||||
raise ValueError(f"Unknown LED point id: {start_point_id}")
|
||||
group_ids = self.counterclockwise_block_from_seed_preview(start_point_id)
|
||||
return self.add_clockwise_block_from_group(line_id, start_point_id, group_ids)
|
||||
|
||||
def add_clockwise_block_from_group(self, line_id: int, start_point_id: int, group_ids: list[int]) -> LedBlock:
|
||||
if not 0 <= start_point_id < len(self.points):
|
||||
raise ValueError(f"Unknown LED point id: {start_point_id}")
|
||||
@ -831,6 +837,12 @@ class MappingDocument:
|
||||
return block
|
||||
|
||||
def clockwise_block_from_seed_preview(self, start_point_id: int) -> list[int]:
|
||||
return self.route_block_from_seed_preview(start_point_id, direction="clockwise")
|
||||
|
||||
def counterclockwise_block_from_seed_preview(self, start_point_id: int) -> list[int]:
|
||||
return self.route_block_from_seed_preview(start_point_id, direction="counterclockwise")
|
||||
|
||||
def route_block_from_seed_preview(self, start_point_id: int, *, direction: str = "clockwise") -> list[int]:
|
||||
if not 0 <= start_point_id < len(self.points):
|
||||
return []
|
||||
candidate_points = [
|
||||
@ -846,11 +858,12 @@ class MappingDocument:
|
||||
adjacency=adjacency,
|
||||
allowed_ids={point.uid for point in candidate_points},
|
||||
min_neighbor_distance=threshold,
|
||||
direction=direction,
|
||||
)
|
||||
if not route:
|
||||
return [start_point_id]
|
||||
return rotate_ids_to_start(
|
||||
[point.uid for point in normalize_route_clockwise(route, threshold)],
|
||||
[point.uid for point in normalize_route_direction(route, threshold, direction=direction)],
|
||||
start_point_id,
|
||||
)
|
||||
|
||||
@ -917,11 +930,23 @@ def rotate_ids_to_start(values: list[int], start_value: int) -> list[int]:
|
||||
|
||||
|
||||
def normalize_route_clockwise(route: list[LedPoint], min_neighbor_distance: float) -> list[LedPoint]:
|
||||
return normalize_route_direction(route, min_neighbor_distance, direction="clockwise")
|
||||
|
||||
|
||||
def normalize_route_direction(
|
||||
route: list[LedPoint],
|
||||
min_neighbor_distance: float,
|
||||
*,
|
||||
direction: str = "clockwise",
|
||||
) -> list[LedPoint]:
|
||||
if len(route) < 3:
|
||||
return route[:]
|
||||
if not is_valid_route_neighbor(route[-1], route[0], min_neighbor_distance):
|
||||
return route[:]
|
||||
return list(reversed(route)) if route_signed_area(route) > 0 else route[:]
|
||||
signed_area = route_signed_area(route)
|
||||
if direction == "counterclockwise":
|
||||
return list(reversed(route)) if signed_area < 0 else route[:]
|
||||
return list(reversed(route)) if signed_area > 0 else route[:]
|
||||
|
||||
|
||||
def route_signed_area(route: list[LedPoint]) -> float:
|
||||
@ -1185,6 +1210,7 @@ def trace_single_route_like_grid_svg(
|
||||
adjacency: dict[int, list[LedPoint]] | None = None,
|
||||
allowed_ids: set[int] | None = None,
|
||||
min_neighbor_distance: float | None = None,
|
||||
direction: str = "clockwise",
|
||||
) -> list[LedPoint]:
|
||||
if not points:
|
||||
return []
|
||||
@ -1216,7 +1242,7 @@ def trace_single_route_like_grid_svg(
|
||||
)
|
||||
if not neighbors:
|
||||
break
|
||||
next_point = choose_next_route_point(route, neighbors, min_neighbor_distance)
|
||||
next_point = choose_next_route_point(route, neighbors, min_neighbor_distance, direction=direction)
|
||||
route.append(next_point)
|
||||
remaining.remove(next_point)
|
||||
remaining_ids.remove(next_point.uid)
|
||||
@ -1251,9 +1277,11 @@ def choose_next_route_point(
|
||||
route: list[LedPoint],
|
||||
neighbors: list[LedPoint],
|
||||
min_neighbor_distance: float,
|
||||
*,
|
||||
direction: str = "clockwise",
|
||||
) -> LedPoint:
|
||||
if len(route) < 2:
|
||||
return min(neighbors, key=lambda point: route_path_preference(route, point, min_neighbor_distance))
|
||||
return min(neighbors, key=lambda point: route_path_preference(route, point, min_neighbor_distance, direction=direction))
|
||||
|
||||
neighbors = nearest_neighbor_band(route[-1], neighbors, min_neighbor_distance)
|
||||
previous = route[-2]
|
||||
@ -1264,7 +1292,10 @@ def choose_next_route_point(
|
||||
if same_direction_vector(previous, current, point)
|
||||
]
|
||||
if same_vector_direction:
|
||||
return min(same_vector_direction, key=lambda point: route_path_preference(route, point, min_neighbor_distance))
|
||||
return min(
|
||||
same_vector_direction,
|
||||
key=lambda point: route_path_preference(route, point, min_neighbor_distance, direction=direction),
|
||||
)
|
||||
|
||||
forward_direction = [
|
||||
point
|
||||
@ -1272,9 +1303,12 @@ def choose_next_route_point(
|
||||
if forward_direction_vector(previous, current, point)
|
||||
]
|
||||
if forward_direction:
|
||||
return min(forward_direction, key=lambda point: route_path_preference(route, point, min_neighbor_distance))
|
||||
return min(
|
||||
forward_direction,
|
||||
key=lambda point: route_path_preference(route, point, min_neighbor_distance, direction=direction),
|
||||
)
|
||||
|
||||
return min(neighbors, key=lambda point: route_path_preference(route, point, min_neighbor_distance))
|
||||
return min(neighbors, key=lambda point: route_path_preference(route, point, min_neighbor_distance, direction=direction))
|
||||
|
||||
|
||||
def nearest_neighbor_band(
|
||||
@ -1538,6 +1572,8 @@ def route_path_preference(
|
||||
route: list[LedPoint],
|
||||
candidate: LedPoint,
|
||||
min_distance: float,
|
||||
*,
|
||||
direction: str = "clockwise",
|
||||
) -> tuple[int, float, int, int, float, tuple[int, int, float, float, int]]:
|
||||
current = route[-1]
|
||||
if len(route) < 2:
|
||||
@ -1552,7 +1588,7 @@ def route_path_preference(
|
||||
)
|
||||
|
||||
previous = route[-2]
|
||||
turn_class, turn_rank = route_clockwise_turn_preference(previous, current, candidate)
|
||||
turn_class, turn_rank = route_turn_preference(previous, current, candidate, direction=direction)
|
||||
return (
|
||||
turn_class,
|
||||
turn_rank,
|
||||
@ -1565,6 +1601,16 @@ def route_path_preference(
|
||||
|
||||
|
||||
def route_clockwise_turn_preference(previous: LedPoint, current: LedPoint, candidate: LedPoint) -> tuple[int, float]:
|
||||
return route_turn_preference(previous, current, candidate, direction="clockwise")
|
||||
|
||||
|
||||
def route_turn_preference(
|
||||
previous: LedPoint,
|
||||
current: LedPoint,
|
||||
candidate: LedPoint,
|
||||
*,
|
||||
direction: str = "clockwise",
|
||||
) -> tuple[int, float]:
|
||||
incoming_x = current.x - previous.x
|
||||
incoming_y = current.y - previous.y
|
||||
outgoing_x = candidate.x - current.x
|
||||
@ -1576,6 +1622,10 @@ def route_clockwise_turn_preference(previous: LedPoint, current: LedPoint, candi
|
||||
return 0, 0.0
|
||||
if angle < math.radians(35):
|
||||
return 0, angle
|
||||
if direction == "counterclockwise":
|
||||
if cross > epsilon:
|
||||
return 1, -angle
|
||||
return 2, angle
|
||||
if cross < -epsilon:
|
||||
return 1, -angle
|
||||
return 2, angle
|
||||
|
||||
241
qa_smoke.py
241
qa_smoke.py
@ -26,6 +26,7 @@ from app.models import (
|
||||
from app.panel_mapping_generator import generate_panel_mapping_from_document
|
||||
from app.unfolding import MAX_LINES
|
||||
try:
|
||||
import app.canvas as canvas_module
|
||||
from app.canvas import LedCanvas
|
||||
from app.background_dialog import BackgroundDialog
|
||||
from app.icons import LUCIDE_PATHS
|
||||
@ -38,6 +39,7 @@ try:
|
||||
from PySide6.QtGui import QColor, QImage, QKeySequence
|
||||
from PySide6.QtWidgets import QApplication, QDialogButtonBox, QGraphicsItem, QMessageBox, QToolBar
|
||||
except ImportError:
|
||||
canvas_module = None
|
||||
LedCanvas = None
|
||||
BackgroundDialog = None
|
||||
LUCIDE_PATHS = {}
|
||||
@ -55,7 +57,7 @@ except ImportError:
|
||||
light_stylesheet = None
|
||||
SIDEBAR_COLLAPSED_WIDTH = 34
|
||||
SIDEBAR_MIN_WIDTH = 180
|
||||
UID_COLUMN = 8
|
||||
UID_COLUMN = 7
|
||||
QPoint = None
|
||||
QPointF = None
|
||||
QRectF = None
|
||||
@ -108,6 +110,8 @@ def main() -> None:
|
||||
test_clockwise_block_from_seed_starts_at_selected_point,
|
||||
test_clockwise_block_from_seed_reverses_counterclockwise_corner,
|
||||
test_clockwise_block_from_seed_uses_direction_priority,
|
||||
test_counterclockwise_block_from_seed_reverses_clockwise_route,
|
||||
test_counterclockwise_block_from_point_handler,
|
||||
test_far_neighbor_is_not_valid_route_neighbor,
|
||||
test_diagonal_grid_neighbor_is_valid_route_neighbor,
|
||||
test_rebuild_line_blocks_splits_far_segments,
|
||||
@ -164,6 +168,11 @@ def main() -> None:
|
||||
test_sidebar_action_toggle_updates_state_and_constraints,
|
||||
test_sidebar_collapsed_tab_click_and_drag_expand,
|
||||
test_canvas_select_points_skips_duplicate_selection_emit,
|
||||
test_canvas_context_menu_can_request_edit_and_counterclockwise_block,
|
||||
test_canvas_alt_drag_emits_point_move,
|
||||
test_canvas_alt_drag_ignores_line_index_label_hit,
|
||||
test_main_window_updates_point_from_canvas_drag,
|
||||
test_main_window_edit_point_dialog_imports_localized_buttons,
|
||||
test_canvas_pan_mode_helpers_follow_settings,
|
||||
test_canvas_brush_cursor_survives_pan_cursor_reset,
|
||||
test_canvas_brush_zone_is_visible_and_collects_points,
|
||||
@ -851,6 +860,62 @@ def test_clockwise_block_from_seed_uses_direction_priority() -> None:
|
||||
assert doc.points[492].line_index == 0
|
||||
|
||||
|
||||
def test_counterclockwise_block_from_seed_reverses_clockwise_route() -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "loop.json"
|
||||
points = [
|
||||
{"id": 0, "x": 0.0, "y": 0.0, "row": 0, "col": 0},
|
||||
{"id": 1, "x": 34.0, "y": 0.0, "row": 0, "col": 1},
|
||||
{"id": 2, "x": 68.0, "y": 0.0, "row": 0, "col": 2},
|
||||
{"id": 3, "x": 68.0, "y": 34.0, "row": 1, "col": 2},
|
||||
{"id": 4, "x": 68.0, "y": 68.0, "row": 2, "col": 2},
|
||||
{"id": 5, "x": 34.0, "y": 68.0, "row": 2, "col": 1},
|
||||
{"id": 6, "x": 0.0, "y": 68.0, "row": 2, "col": 0},
|
||||
{"id": 7, "x": 0.0, "y": 34.0, "row": 1, "col": 0},
|
||||
]
|
||||
path.write_text(json.dumps({"circles": points}), encoding="utf-8")
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
line = doc.add_line()
|
||||
|
||||
clockwise = doc.add_clockwise_block_from_seed(line.id, 0).point_ids
|
||||
doc.clear_all_assignments()
|
||||
line = doc.add_line()
|
||||
counterclockwise = doc.add_counterclockwise_block_from_seed(line.id, 0).point_ids
|
||||
|
||||
assert clockwise == [0, 7, 6, 5, 4, 3, 2, 1]
|
||||
assert counterclockwise == [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
|
||||
def test_counterclockwise_block_from_point_handler() -> None:
|
||||
if MainWindow is None or QApplication is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
window = MainWindow()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "counter-handler.json"
|
||||
points = [
|
||||
{"id": 0, "x": 0.0, "y": 0.0, "row": 0, "col": 0},
|
||||
{"id": 1, "x": 34.0, "y": 0.0, "row": 0, "col": 1},
|
||||
{"id": 2, "x": 34.0, "y": 34.0, "row": 1, "col": 1},
|
||||
{"id": 3, "x": 0.0, "y": 34.0, "row": 1, "col": 0},
|
||||
]
|
||||
path.write_text(json.dumps({"circles": points}), encoding="utf-8")
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
line = doc.add_line()
|
||||
window.document = doc
|
||||
window.active_line_id = line.id
|
||||
window.canvas.set_document(doc)
|
||||
window.canvas.set_active_line(line.id)
|
||||
window._confirm_reassignment = lambda _point_ids: True
|
||||
|
||||
window.auto_block_from_point_counterclockwise(0)
|
||||
|
||||
assert doc.lines[0].blocks[0].point_ids == [0, 1, 2, 3]
|
||||
window.close()
|
||||
|
||||
|
||||
def test_far_neighbor_is_not_valid_route_neighbor() -> None:
|
||||
current = LedPoint(0, 0, 0.0, 0.0, 0, 0)
|
||||
far = LedPoint(1, 1, 100.0, 0.0, 1, 0)
|
||||
@ -1683,7 +1748,7 @@ def test_classic_menu_contains_expected_actions() -> None:
|
||||
"Подложка",
|
||||
"Скрыть боковую панель",
|
||||
],
|
||||
"Инструменты": ["Новая линия", "Новый светодиод", "Сгенерировать скрипт", "Автоблоки", "Кисть"],
|
||||
"Инструменты": ["Новая линия", "Новый светодиод", "Сгенерировать скрипт", "Автоблоки", "Против часовой", "Кисть"],
|
||||
}
|
||||
for top_action in list(window.menuBar().actions()):
|
||||
menu = top_action.menu()
|
||||
@ -1780,6 +1845,7 @@ def test_main_window_actions_use_lucide_icons() -> None:
|
||||
"add_block",
|
||||
"auto_blocks",
|
||||
"clockwise",
|
||||
"counterclockwise",
|
||||
"rebuild_route",
|
||||
"brush",
|
||||
"undo",
|
||||
@ -1832,6 +1898,7 @@ def test_main_window_action_shortcuts_are_assigned() -> None:
|
||||
"add_block": "Ctrl+Shift+L",
|
||||
"auto_blocks": "Ctrl+Shift+B",
|
||||
"clockwise": "Ctrl+Alt+R",
|
||||
"counterclockwise": "Ctrl+Alt+Shift+R",
|
||||
"rebuild_route": "Ctrl+Shift+R",
|
||||
"brush": "B",
|
||||
}
|
||||
@ -2017,6 +2084,7 @@ def test_lucide_icon_names_are_declared() -> None:
|
||||
"paintbrush",
|
||||
"plus",
|
||||
"redo-2",
|
||||
"rotate-ccw",
|
||||
"rotate-cw",
|
||||
"save",
|
||||
"save-all",
|
||||
@ -2036,7 +2104,6 @@ def test_table_uses_single_line_index_column() -> None:
|
||||
"ID",
|
||||
"X",
|
||||
"Y",
|
||||
"Источник",
|
||||
"Ряд",
|
||||
"Кол.",
|
||||
"Линия",
|
||||
@ -2056,7 +2123,7 @@ def test_table_uses_single_line_index_column() -> None:
|
||||
app.processEvents()
|
||||
assert window.table.item(0, 0).text() == "0"
|
||||
assert window.table.item(1, 0).text() == "1"
|
||||
assert window.table.item(0, 8).text() == "0"
|
||||
assert window.table.item(0, UID_COLUMN).text() == "0"
|
||||
|
||||
window._on_selection_changed([0, 4])
|
||||
app.processEvents()
|
||||
@ -2364,6 +2431,172 @@ def test_canvas_select_points_skips_duplicate_selection_emit() -> None:
|
||||
assert emitted == [[0, 1]]
|
||||
|
||||
|
||||
def test_canvas_context_menu_can_request_edit_and_counterclockwise_block() -> None:
|
||||
if LedCanvas is None or QApplication is None or canvas_module is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
class FakeAction:
|
||||
def __init__(self, text: str) -> None:
|
||||
self.text = text
|
||||
self.enabled = True
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
self.enabled = enabled
|
||||
|
||||
class FakeMenu:
|
||||
next_choice = ""
|
||||
|
||||
def __init__(self, _parent=None) -> None:
|
||||
self.actions: list[FakeAction] = []
|
||||
|
||||
def addAction(self, text: str) -> FakeAction:
|
||||
action = FakeAction(text)
|
||||
self.actions.append(action)
|
||||
return action
|
||||
|
||||
def exec(self, _global_pos): # noqa: ANN001
|
||||
return next(action for action in self.actions if action.text == self.next_choice)
|
||||
|
||||
class FakeContextEvent:
|
||||
def pos(self) -> QPoint:
|
||||
return QPoint(0, 0)
|
||||
|
||||
def globalPos(self) -> QPoint:
|
||||
return QPoint(0, 0)
|
||||
|
||||
with temp_document_path() as path:
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
canvas = LedCanvas()
|
||||
canvas.set_document(doc)
|
||||
canvas.select_points([0])
|
||||
edited: list[int] = []
|
||||
counterclockwise: list[int] = []
|
||||
canvas.editPointRequested.connect(lambda point_id: edited.append(point_id))
|
||||
canvas.counterclockwiseBlockFromPointRequested.connect(lambda point_id: counterclockwise.append(point_id))
|
||||
original_menu = canvas_module.QMenu
|
||||
canvas_module.QMenu = FakeMenu
|
||||
try:
|
||||
FakeMenu.next_choice = "Редактировать координаты"
|
||||
canvas.contextMenuEvent(FakeContextEvent())
|
||||
FakeMenu.next_choice = "Автоматически добавить блок против часовой"
|
||||
canvas.contextMenuEvent(FakeContextEvent())
|
||||
finally:
|
||||
canvas_module.QMenu = original_menu
|
||||
canvas.close()
|
||||
|
||||
assert edited == [0]
|
||||
assert counterclockwise == [0]
|
||||
|
||||
|
||||
def test_canvas_alt_drag_emits_point_move() -> None:
|
||||
if LedCanvas is None or QApplication is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "single-point.json"
|
||||
path.write_text(
|
||||
json.dumps({"circles": [{"id": 1, "x": 100.0, "y": 200.0, "row": 0, "col": 0}]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
canvas = LedCanvas()
|
||||
canvas.resize(320, 240)
|
||||
canvas.set_document(doc)
|
||||
canvas.show()
|
||||
app.processEvents()
|
||||
start_pos = canvas.mapFromScene(canvas.items_by_id[0].pos())
|
||||
moved: list[tuple[int, float, float]] = []
|
||||
canvas.pointMoveFinished.connect(lambda point_id, x, y: moved.append((point_id, x, y)))
|
||||
|
||||
assert canvas._start_point_drag(start_pos)
|
||||
canvas._finish_point_drag(start_pos + QPoint(30, -20))
|
||||
|
||||
assert moved
|
||||
assert moved[0][0] == 0
|
||||
assert abs(moved[0][1] - doc.points[0].x) > 1e-6 or abs(moved[0][2] - doc.points[0].y) > 1e-6
|
||||
canvas.close()
|
||||
|
||||
|
||||
def test_canvas_alt_drag_ignores_line_index_label_hit() -> None:
|
||||
if LedCanvas is None or QApplication is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "labeled-point.json"
|
||||
path.write_text(
|
||||
json.dumps({"circles": [{"id": 1, "x": 100.0, "y": 200.0, "row": 0, "col": 0}]}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
line = doc.add_line()
|
||||
doc.add_block(line.id, [0])
|
||||
canvas = LedCanvas()
|
||||
canvas.resize(320, 240)
|
||||
canvas.set_document(doc)
|
||||
canvas.set_active_line(line.id)
|
||||
canvas.select_points([0])
|
||||
canvas.show()
|
||||
app.processEvents()
|
||||
|
||||
label = canvas.line_index_label_items[0]
|
||||
assert not label.isEnabled()
|
||||
assert label.acceptedMouseButtons() == Qt.NoButton
|
||||
|
||||
label_pos = canvas.mapFromScene(canvas.items_by_id[0].pos())
|
||||
assert canvas._led_item_at(label_pos) is canvas.items_by_id[0]
|
||||
assert canvas._start_point_drag(label_pos)
|
||||
assert canvas._point_drag["point_id"] == 0
|
||||
canvas._finish_point_drag(label_pos + QPoint(15, 0))
|
||||
canvas.close()
|
||||
|
||||
|
||||
def test_main_window_updates_point_from_canvas_drag() -> None:
|
||||
if MainWindow is None or QApplication is None:
|
||||
return
|
||||
app = QApplication.instance() or QApplication([])
|
||||
window = MainWindow()
|
||||
with temp_document_path() as path:
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
window.document = doc
|
||||
window.canvas.set_document(doc)
|
||||
source_id = doc.points[0].source_id
|
||||
|
||||
assert window.move_point_from_canvas(0, doc.points[0].x + 12.5, doc.points[0].y + 7.25) is None
|
||||
|
||||
assert doc.points[0].source_id == source_id
|
||||
assert abs(doc.points[0].x - (fixture_points()[0]["x"] + 12.5)) < 1e-6
|
||||
assert abs(doc.points[0].y - (fixture_points()[0]["y"] + 7.25)) < 1e-6
|
||||
assert window._dirty
|
||||
assert window._undo_stack
|
||||
window.close()
|
||||
|
||||
|
||||
def test_main_window_edit_point_dialog_imports_localized_buttons() -> None:
|
||||
if MainWindow is None or QApplication is None:
|
||||
return
|
||||
import app.main_window as main_window_module
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
window = MainWindow()
|
||||
with temp_document_path() as path:
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
window.document = doc
|
||||
|
||||
original_exec = main_window_module.QDialog.exec
|
||||
main_window_module.QDialog.exec = lambda _dialog: main_window_module.QDialog.Rejected
|
||||
try:
|
||||
window.edit_point_coordinates(0)
|
||||
finally:
|
||||
main_window_module.QDialog.exec = original_exec
|
||||
window.close()
|
||||
|
||||
|
||||
def test_canvas_pan_mode_helpers_follow_settings() -> None:
|
||||
if LedCanvas is None or QApplication is None:
|
||||
return
|
||||
|
||||
@ -35,6 +35,7 @@ SHORTCUT_DEFINITIONS: list[tuple[str, str, str]] = [
|
||||
("add_block", "Блок из выбора", "Ctrl+Shift+L"),
|
||||
("auto_blocks", "Автоблоки", "Ctrl+Shift+B"),
|
||||
("clockwise", "По часовой", "Ctrl+Alt+R"),
|
||||
("counterclockwise", "Против часовой", "Ctrl+Alt+Shift+R"),
|
||||
("rebuild_route", "Пересобрать", "Ctrl+Shift+R"),
|
||||
("brush", "Кисть", "B"),
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user