Merge pull request #16383 from Ultimaker/CURA-7913

Add copy, paste, cut functionality
This commit is contained in:
Jelle Spijker 2023-08-09 14:37:12 +02:00 committed by GitHub
commit 16ce595a15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 18 deletions

View File

@ -1,15 +1,18 @@
# 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
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 +22,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 +185,60 @@ class CuraActions(QObject):
Selection.clear()
@pyqtSlot()
def cut(self) -> None:
self.copy()
self.deleteSelection()
@pyqtSlot()
def copy(self) -> None:
mesh_writer = cura.CuraApplication.CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer:
Logger.log("e", "No 3MF writer found, unable to copy.")
return
# Get the selected nodes
selected_objects = Selection.getAllSelectedObjects()
# Serialize the nodes to a string
scene_string = mesh_writer.sceneNodesToString(selected_objects)
# Put the string on the clipboard
QApplication.clipboard().setText(scene_string)
@pyqtSlot()
def paste(self) -> None:
application = cura.CuraApplication.CuraApplication.getInstance()
mesh_reader = application.getMeshFileHandler().getReaderForFile(".3mf")
if not mesh_reader:
Logger.log("e", "No 3MF reader found, unable to paste.")
return
# Parse the scene from the clipboard
scene_string = QApplication.clipboard().text()
nodes = mesh_reader.stringToSceneNodes(scene_string)
if not nodes:
# Nothing to paste
return
# 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)

View File

@ -56,7 +56,8 @@ class ThreeMFReader(MeshReader):
def emptyFileHintSet(self) -> bool:
return self._empty_project
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
@staticmethod
def _createMatrixFromTransformationString(transformation: str) -> Matrix:
if transformation == "":
return Matrix()
@ -90,7 +91,8 @@ class ThreeMFReader(MeshReader):
return temp_mat
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
@staticmethod
def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
:returns: Scene node.
@ -119,7 +121,7 @@ class ThreeMFReader(MeshReader):
pass
um_node.setName(node_name)
um_node.setId(node_id)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
@ -138,7 +140,7 @@ class ThreeMFReader(MeshReader):
um_node.setMeshData(mesh_data)
for child in savitar_node.getChildren():
child_node = self._convertSavitarNodeToUMNode(child)
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
if child_node:
um_node.addChild(child_node)
@ -214,7 +216,7 @@ class ThreeMFReader(MeshReader):
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
for node in scene_3mf.getSceneNodes():
um_node = self._convertSavitarNodeToUMNode(node, file_name)
um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name)
if um_node is None:
continue
@ -300,8 +302,23 @@ class ThreeMFReader(MeshReader):
if unit is None:
unit = "millimeter"
elif unit not in conversion_to_mm:
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit=unit))
unit = "millimeter"
scale = conversion_to_mm[unit]
return Vector(scale, scale, scale)
@staticmethod
def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
parser = Savitar.ThreeMFParser()
scene = parser.parse(scene_string)
# Convert the scene to scene nodes
nodes = []
for savitar_node in scene.getSceneNodes():
scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
if scene_node is None:
continue
nodes.append(scene_node)
return nodes

View File

@ -55,11 +55,12 @@ class ThreeMFWriter(MeshWriter):
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
self._archive: Optional[zipfile.ZipFile] = None
self._store_archive = False
def _convertMatrixToString(self, matrix):
@staticmethod
def _convertMatrixToString(matrix):
result = ""
result += str(matrix._data[0, 0]) + " "
result += str(matrix._data[1, 0]) + " "
@ -83,7 +84,8 @@ class ThreeMFWriter(MeshWriter):
"""
self._store_archive = store_archive
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
@staticmethod
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
:returns: Uranium Scene node.
@ -100,7 +102,7 @@ class ThreeMFWriter(MeshWriter):
node_matrix = um_node.getLocalTransformation()
matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
savitar_node.setTransformation(matrix_string)
mesh_data = um_node.getMeshData()
@ -133,7 +135,7 @@ class ThreeMFWriter(MeshWriter):
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)
@ -221,7 +223,7 @@ class ThreeMFWriter(MeshWriter):
for node in nodes:
if node == root_node:
for root_child in node.getChildren():
savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
if savitar_node:
savitar_scene.addSceneNode(savitar_node)
else:
@ -303,9 +305,19 @@ class ThreeMFWriter(MeshWriter):
Logger.log("w", "Can't create snapshot when renderer not initialized.")
return None
try:
snapshot = Snapshot.snapshot(width = 300, height = 300)
snapshot = Snapshot.snapshot(width=300, height=300)
except:
Logger.logException("w", "Failed to create snapshot image")
return None
return snapshot
@staticmethod
def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
savitar_scene = Savitar.Scene()
for scene_node in scene_nodes:
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node)
savitar_scene.addSceneNode(savitar_node)
parser = Savitar.ThreeMFParser()
scene_string = parser.sceneToString(savitar_scene)
return scene_string

View File

@ -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
@ -6,7 +6,7 @@ pragma Singleton
import QtQuick 2.10
import QtQuick.Controls 2.4
import UM 1.1 as UM
import Cura 1.0 as Cura
import Cura 1.5 as Cura
Item
{
@ -71,6 +71,15 @@ Item
property alias browsePackages: browsePackagesAction
property alias paste: pasteAction
property alias copy: copyAction
property alias cut: cutAction
readonly property bool copy_paste_enabled: {
const all_enabled_packages = CuraApplication.getPackageManager().allEnabledPackages;
return all_enabled_packages.includes("3MFReader") && all_enabled_packages.includes("3MFWriter");
}
UM.I18nCatalog{id: catalog; name: "cura"}
@ -309,6 +318,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 && copy_paste_enabled
shortcut: StandardKey.Copy
}
Action
{
id: pasteAction
text: catalog.i18nc("@action:inmenu menubar:edit", "Paste from clipboard")
onTriggered: CuraActions.paste()
enabled: UM.Controller.toolsEnabled && copy_paste_enabled
shortcut: StandardKey.Paste
}
Action
{
id: cutAction
text: catalog.i18nc("@action:inmenu menubar:edit", "Cut")
onTriggered: CuraActions.cut()
enabled: UM.Controller.toolsEnabled && UM.Selection.hasSelection && copy_paste_enabled
shortcut: StandardKey.Cut
}
Action
{
id: multiplySelectionAction

View File

@ -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