mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-12 10:59:01 +08:00
Merge pull request #16383 from Ultimaker/CURA-7913
Add copy, paste, cut functionality
This commit is contained in:
commit
16ce595a15
@ -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.
|
# 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 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.Event import CallFunctionEvent
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
from UM.Operations.TranslateOperation import TranslateOperation
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
@ -19,6 +22,7 @@ from cura.Operations.SetParentOperation import SetParentOperation
|
|||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from cura.Arranging.Nest2DArrange import createGroupOperationForArrange
|
||||||
|
|
||||||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||||
|
|
||||||
@ -181,5 +185,60 @@ class CuraActions(QObject):
|
|||||||
|
|
||||||
Selection.clear()
|
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:
|
def _openUrl(self, url: QUrl) -> None:
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
@ -56,7 +56,8 @@ class ThreeMFReader(MeshReader):
|
|||||||
def emptyFileHintSet(self) -> bool:
|
def emptyFileHintSet(self) -> bool:
|
||||||
return self._empty_project
|
return self._empty_project
|
||||||
|
|
||||||
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
@staticmethod
|
||||||
|
def _createMatrixFromTransformationString(transformation: str) -> Matrix:
|
||||||
if transformation == "":
|
if transformation == "":
|
||||||
return Matrix()
|
return Matrix()
|
||||||
|
|
||||||
@ -90,7 +91,8 @@ class ThreeMFReader(MeshReader):
|
|||||||
|
|
||||||
return temp_mat
|
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.
|
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||||
|
|
||||||
:returns: Scene node.
|
:returns: Scene node.
|
||||||
@ -119,7 +121,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
pass
|
pass
|
||||||
um_node.setName(node_name)
|
um_node.setName(node_name)
|
||||||
um_node.setId(node_id)
|
um_node.setId(node_id)
|
||||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||||
um_node.setTransformation(transformation)
|
um_node.setTransformation(transformation)
|
||||||
mesh_builder = MeshBuilder()
|
mesh_builder = MeshBuilder()
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
um_node.setMeshData(mesh_data)
|
um_node.setMeshData(mesh_data)
|
||||||
|
|
||||||
for child in savitar_node.getChildren():
|
for child in savitar_node.getChildren():
|
||||||
child_node = self._convertSavitarNodeToUMNode(child)
|
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
|
||||||
if child_node:
|
if child_node:
|
||||||
um_node.addChild(child_node)
|
um_node.addChild(child_node)
|
||||||
|
|
||||||
@ -214,7 +216,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
||||||
|
|
||||||
for node in scene_3mf.getSceneNodes():
|
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:
|
if um_node is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -300,8 +302,23 @@ class ThreeMFReader(MeshReader):
|
|||||||
if unit is None:
|
if unit is None:
|
||||||
unit = "millimeter"
|
unit = "millimeter"
|
||||||
elif unit not in conversion_to_mm:
|
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"
|
unit = "millimeter"
|
||||||
|
|
||||||
scale = conversion_to_mm[unit]
|
scale = conversion_to_mm[unit]
|
||||||
return Vector(scale, scale, scale)
|
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
|
||||||
|
@ -55,11 +55,12 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
"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._archive: Optional[zipfile.ZipFile] = None
|
||||||
self._store_archive = False
|
self._store_archive = False
|
||||||
|
|
||||||
def _convertMatrixToString(self, matrix):
|
@staticmethod
|
||||||
|
def _convertMatrixToString(matrix):
|
||||||
result = ""
|
result = ""
|
||||||
result += str(matrix._data[0, 0]) + " "
|
result += str(matrix._data[0, 0]) + " "
|
||||||
result += str(matrix._data[1, 0]) + " "
|
result += str(matrix._data[1, 0]) + " "
|
||||||
@ -83,7 +84,8 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
"""
|
"""
|
||||||
self._store_archive = store_archive
|
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
|
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||||
|
|
||||||
:returns: Uranium Scene node.
|
:returns: Uranium Scene node.
|
||||||
@ -100,7 +102,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
|
|
||||||
node_matrix = um_node.getLocalTransformation()
|
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)
|
savitar_node.setTransformation(matrix_string)
|
||||||
mesh_data = um_node.getMeshData()
|
mesh_data = um_node.getMeshData()
|
||||||
@ -133,7 +135,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
# only save the nodes on the active build plate
|
# only save the nodes on the active build plate
|
||||||
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
|
||||||
continue
|
continue
|
||||||
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
|
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
|
||||||
if savitar_child_node is not None:
|
if savitar_child_node is not None:
|
||||||
savitar_node.addChild(savitar_child_node)
|
savitar_node.addChild(savitar_child_node)
|
||||||
|
|
||||||
@ -221,7 +223,7 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node == root_node:
|
if node == root_node:
|
||||||
for root_child in node.getChildren():
|
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:
|
if savitar_node:
|
||||||
savitar_scene.addSceneNode(savitar_node)
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
else:
|
else:
|
||||||
@ -303,9 +305,19 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
snapshot = Snapshot.snapshot(width = 300, height = 300)
|
snapshot = Snapshot.snapshot(width=300, height=300)
|
||||||
except:
|
except:
|
||||||
Logger.logException("w", "Failed to create snapshot image")
|
Logger.logException("w", "Failed to create snapshot image")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return snapshot
|
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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2022 UltiMaker
|
// Copyright (c) 2023 UltiMaker
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
@ -6,7 +6,7 @@ pragma Singleton
|
|||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 2.4
|
import QtQuick.Controls 2.4
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.5 as Cura
|
||||||
|
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
@ -71,6 +71,15 @@ Item
|
|||||||
|
|
||||||
property alias browsePackages: browsePackagesAction
|
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"}
|
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||||
|
|
||||||
|
|
||||||
@ -309,6 +318,33 @@ Item
|
|||||||
onTriggered: CuraActions.centerSelection()
|
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
|
Action
|
||||||
{
|
{
|
||||||
id: multiplySelectionAction
|
id: multiplySelectionAction
|
||||||
|
@ -19,6 +19,8 @@ Cura.Menu
|
|||||||
// Selection-related actions.
|
// Selection-related actions.
|
||||||
Cura.MenuItem { action: Cura.Actions.centerSelection; }
|
Cura.MenuItem { action: Cura.Actions.centerSelection; }
|
||||||
Cura.MenuItem { action: Cura.Actions.deleteSelection; }
|
Cura.MenuItem { action: Cura.Actions.deleteSelection; }
|
||||||
|
Cura.MenuItem { action: Cura.Actions.copy; }
|
||||||
|
Cura.MenuItem { action: Cura.Actions.paste; }
|
||||||
Cura.MenuItem { action: Cura.Actions.multiplySelection; }
|
Cura.MenuItem { action: Cura.Actions.multiplySelection; }
|
||||||
|
|
||||||
// Extruder selection - only visible if there is more than 1 extruder
|
// Extruder selection - only visible if there is more than 1 extruder
|
||||||
|
Loading…
x
Reference in New Issue
Block a user