diff --git a/canvas.py b/canvas.py index bf04eca..00a9597 100644 --- a/canvas.py +++ b/canvas.py @@ -4,7 +4,7 @@ import math from pathlib import Path from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal -from PySide6.QtGui import QColor, QBrush, QImageReader, QPainter, QPainterPath, QPen, QPixmap, QTransform +from PySide6.QtGui import QColor, QBrush, QImage, QImageReader, QPainter, QPainterPath, QPen, QPixmap, QTransform from PySide6.QtWidgets import ( QGraphicsEllipseItem, QGraphicsItem, @@ -1489,3 +1489,40 @@ class LedCanvas(QGraphicsView): @staticmethod def _move_scrollbar(scrollbar: QScrollBar, delta: int) -> None: scrollbar.setValue(scrollbar.value() - delta) + + def export_to_image(self, file_path: str) -> bool: + scene = self.scene() + if scene is None: + return False + + rect = scene.itemsBoundingRect() + if rect.isEmpty(): + return False + + padding = max(rect.width(), rect.height()) * 0.05 + rect.adjust(-padding, -padding, padding, padding) + + target_width = 2048 + aspect_ratio = rect.height() / rect.width() + target_height = int(target_width * aspect_ratio) + + if target_height > 4096: + target_height = 4096 + target_width = int(target_height / aspect_ratio) + + image = QImage(target_width, target_height, QImage.Format.Format_ARGB32) + + bg_brush = scene.backgroundBrush() + if bg_brush.style() != Qt.BrushStyle.NoBrush: + image.fill(bg_brush.color()) + else: + image.fill(QColor("#2b2b2b" if self._theme == "dark" else "#ffffff")) + + painter = QPainter(image) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + painter.setRenderHint(QPainter.RenderHint.TextAntialiasing) + + scene.render(painter, QRectF(0, 0, target_width, target_height), rect) + painter.end() + + return image.save(file_path) diff --git a/main_window.py b/main_window.py index c1ec983..0356ff7 100644 --- a/main_window.py +++ b/main_window.py @@ -337,6 +337,14 @@ class MainWindow(QMainWindow): add_action("open", "Открыть", self.open_file, "folder-open", QKeySequence.Open) add_action("save", "Сохранить", self.save_file, "save", QKeySequence.Save) add_action("save_as", "Сохранить как", self.save_file_as, "save-all", QKeySequence.SaveAs) + add_action( + "export_image", + "Экспортировать в изображение...", + self.export_mapping_image, + "camera", + QKeySequence("Ctrl+Shift+E"), + "Экспортировать текущую схему маппинга в изображение (PNG, JPEG)" + ) add_action("settings", "Параметры", self.open_settings, "settings", QKeySequence("Ctrl+,")) add_action( "generate_script", @@ -439,6 +447,7 @@ class MainWindow(QMainWindow): file_menu.addAction(self.actions["open"]) file_menu.addAction(self.actions["save"]) file_menu.addAction(self.actions["save_as"]) + file_menu.addAction(self.actions["export_image"]) file_menu.addSeparator() file_menu.addAction(self.actions["generate_script"]) file_menu.addSeparator() @@ -939,6 +948,38 @@ class MainWindow(QMainWindow): if path: self._save_to(Path(path)) + def export_mapping_image(self) -> None: + if not self.document.points: + self.status.setText("Схема пуста. Экспорт отменён.") + self._refresh_ui_state() + return + + default_name = "mapping.png" + if self.document.path is not None: + default_name = self.document.path.stem + ".png" + + self._disable_shortcuts() + try: + file_path, _ = QFileDialog.getSaveFileName( + self, + "Экспортировать маппинг в изображение", + default_name, + "PNG Image (*.png);;JPEG Image (*.jpg *.jpeg)" + ) + finally: + self._enable_shortcuts() + + if not file_path: + return + + success = self.canvas.export_to_image(file_path) + if success: + self.status.setText(f"Схема успешно экспортирована в: {file_path}") + else: + show_warning(self, "Ошибка экспорта", "Не удалось экспортировать схему в изображение.") + self.status.setText("Ошибка экспорта изображения.") + self._refresh_ui_state() + def generate_script(self, output_dir: str | Path | None = None) -> None: if isinstance(output_dir, bool): output_dir = None @@ -2137,6 +2178,7 @@ class MainWindow(QMainWindow): state = { "save": has_points and self._dirty, "save_as": has_points, + "export_image": has_points, "generate_script": has_points, "undo": bool(self._undo_stack), "redo": bool(self._redo_stack), diff --git a/qa_smoke.py b/qa_smoke.py index 1778876..1937f05 100644 --- a/qa_smoke.py +++ b/qa_smoke.py @@ -121,6 +121,7 @@ def main() -> None: test_panel_mapping_generation_with_strategies, test_block_renaming_and_naming_strategy, test_line_renaming_and_tree_shortcuts, + test_export_mapping_to_image, test_save_preserves_unknown_source_fields, test_ordered_json_preserves_document_and_remaps_uids, test_background_load_save_roundtrip, @@ -1132,6 +1133,26 @@ def test_line_renaming_and_tree_shortcuts() -> None: window.close() +def test_export_mapping_to_image() -> None: + if MainWindow is None or QApplication is None: + return + app = QApplication.instance() or QApplication([]) + window = MainWindow() + + window.document.add_manual_point(100.0, 100.0) + window.canvas.set_document(window.document) + window._refresh_ui_state() + + with tempfile.TemporaryDirectory() as temp_dir: + image_path = Path(temp_dir) / "output.png" + success = window.canvas.export_to_image(str(image_path)) + assert success + assert image_path.exists() + assert image_path.stat().st_size > 0 + + window.close() + + def test_save_preserves_unknown_source_fields() -> None: with tempfile.TemporaryDirectory() as temp_dir: source = Path(temp_dir) / "custom.json"