From f07faac4223d3d15d36b486e1fce18a84ea55fb0 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Fri, 4 Aug 2023 13:24:06 +0200 Subject: [PATCH] Add copy, paste, cut functionality CURA-7913 --- cura/CuraActions.py | 70 +++++++++++++++++++++++++++-- resources/qml/Actions.qml | 33 +++++++++++++- resources/qml/Menus/ContextMenu.qml | 2 + 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 193803325f..aef26c7082 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -1,15 +1,20 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2023 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. -from PyQt6.QtCore import QObject, QUrl -from PyQt6.QtGui import QDesktopServices from typing import List, cast +from PyQt6.QtCore import QObject, QUrl, QMimeData +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import QApplication + +import pySavitar as Savitar + from UM.Event import CallFunctionEvent from UM.FlameProfiler import pyqtSlot from UM.Math.Vector import Vector from UM.Scene.Selection import Selection from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.TranslateOperation import TranslateOperation @@ -19,6 +24,7 @@ from cura.Operations.SetParentOperation import SetParentOperation from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.ExtruderManager import ExtruderManager +from cura.Arranging.Nest2DArrange import createGroupOperationForArrange from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation @@ -181,5 +187,63 @@ class CuraActions(QObject): Selection.clear() + @pyqtSlot() + def cut(self) -> None: + self.copy() + self.deleteSelection() + + @pyqtSlot() + def copy(self) -> None: + # Convert all selected objects to a Savitar scene + savitar_scene = Savitar.Scene() + mesh_writer = cura.CuraApplication.CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter") + for scene_node in Selection.getAllSelectedObjects(): + savitar_node = mesh_writer._convertUMNodeToSavitarNode(scene_node) + savitar_scene.addSceneNode(savitar_node) + + # Convert the scene to a string + parser = Savitar.ThreeMFParser() + scene_string = parser.sceneToString(savitar_scene) + + # Copy the scene to the clipboard + QApplication.clipboard().setText(scene_string) + + @pyqtSlot() + def paste(self) -> None: + application = cura.CuraApplication.CuraApplication.getInstance() + + # Parse the scene from the clipboard + scene_string = QApplication.clipboard().text() + parser = Savitar.ThreeMFParser() + scene = parser.parse(scene_string) + + # Convert the scene to scene nodes + nodes = [] + mesh_reader = application.getMeshFileHandler().getReaderForFile(".3mf") + for savitar_node in scene.getSceneNodes(): + scene_node = mesh_reader._convertSavitarNodeToUMNode(savitar_node, "file_name") + if scene_node is None: + continue + nodes.append(scene_node) + + # Find all fixed nodes, these are the nodes that should be avoided when arranging + fixed_nodes = [] + root = application.getController().getScene().getRoot() + for node in DepthFirstIterator(root): + # Only count sliceable objects + if node.callDecoration("isSliceable"): + fixed_nodes.append(node) + # Add the new nodes to the scene, and arrange them + group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(), + fixed_nodes, factor=10000, + add_new_nodes_in_scene=True) + group_operation.push() + + # deselect currently selected nodes, and select the new nodes + for node in Selection.getAllSelectedObjects(): + Selection.remove(node) + for node in nodes: + Selection.add(node) + def _openUrl(self, url: QUrl) -> None: QDesktopServices.openUrl(url) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 6cd75b51ac..9cd72026b2 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2022 UltiMaker +// Copyright (c) 2023 UltiMaker // Cura is released under the terms of the LGPLv3 or higher. pragma Singleton @@ -71,6 +71,10 @@ Item property alias browsePackages: browsePackagesAction + property alias paste: pasteAction + property alias copy: copyAction + property alias cut: cutAction + UM.I18nCatalog{id: catalog; name: "cura"} @@ -309,6 +313,33 @@ Item onTriggered: CuraActions.centerSelection() } + Action + { + id: copyAction + text: catalog.i18nc("@action:inmenu menubar:edit", "Copy to clipboard") + onTriggered: CuraActions.copy() + enabled: UM.Controller.toolsEnabled && UM.Selection.hasSelection + shortcut: StandardKey.Copy + } + + Action + { + id: pasteAction + text: catalog.i18nc("@action:inmenu menubar:edit", "Paste from clipboard") + onTriggered: CuraActions.paste() + enabled: UM.Controller.toolsEnabled + shortcut: StandardKey.Paste + } + + Action + { + id: cutAction + text: catalog.i18nc("@action:inmenu menubar:edit", "Cut") + onTriggered: CuraActions.cut() + enabled: UM.Controller.toolsEnabled && UM.Selection.hasSelection + shortcut: StandardKey.Cut + } + Action { id: multiplySelectionAction diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index f7029939cd..d85703451f 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -19,6 +19,8 @@ Cura.Menu // Selection-related actions. Cura.MenuItem { action: Cura.Actions.centerSelection; } Cura.MenuItem { action: Cura.Actions.deleteSelection; } + Cura.MenuItem { action: Cura.Actions.copy; } + Cura.MenuItem { action: Cura.Actions.paste; } Cura.MenuItem { action: Cura.Actions.multiplySelection; } // Extruder selection - only visible if there is more than 1 extruder