Browse Source

Merge branch 'main' into CURA-10475_engineplugin

c.lamboo 1 year ago
parent
commit
0667230055

+ 2 - 1
README.md

@@ -12,7 +12,7 @@
 
 [![Badge Test]][Test]   
 [![Badge Conan]][Conan]   
-
+![Badge Downloads]
 <br>
 <br>
 
@@ -84,6 +84,7 @@
 [Badge Conan]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/conan-package?style=for-the-badge&logoColor=white&labelColor=6185aa&color=4c6987&logo=Conan&label=Conan%20Package
 [Badge Test]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/unit-test?style=for-the-badge&logoColor=white&labelColor=4a999d&color=346c6e&logo=Codacy&label=Unit%20Test
 [Badge Size]: https://img.shields.io/github/repo-size/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=715a97&color=584674&logo=GoogleAnalytics
+[Badge Downloads]: https://img.shields.io/github/downloads-pre/Ultimaker/Cura/latest/total?style=for-the-badge
 
 
 <!---------------------------------[ Buttons ]--------------------------------->

+ 1 - 1
conanfile.py

@@ -468,6 +468,7 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
         save(self, os.path.join(self._script_dir, f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
 
         self._generate_cura_version(os.path.join(self._site_packages, "cura"))
+        self._generate_about_versions(str(self._share_dir.joinpath("cura", "resources", "qml", "Dialogs")))
 
         entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements"))
         self._generate_pyinstaller_spec(location = self._base_dir,
@@ -475,7 +476,6 @@ echo "CURA_APP_NAME={{ cura_app_name }}" >> ${{ env_prefix }}GITHUB_ENV
                                         icon_path = "'{}'".format(os.path.join(self.package_folder, self.cpp_info.resdirs[2], self.conan_data["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
                                         entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
 
-        self._generate_about_versions(os.path.join(self.source_folder, "resources", "qml", "Dialogs"))
 
     def package(self):
         copy(self, "cura_app.py", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0]))

+ 62 - 3
cura/CuraActions.py

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

+ 23 - 6
plugins/3MFReader/ThreeMFReader.py

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

+ 19 - 7
plugins/3MFWriter/ThreeMFWriter.py

@@ -57,11 +57,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]) + " "
@@ -85,7 +86,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.
@@ -102,7 +104,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()
@@ -135,7 +137,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)
 
@@ -225,7 +227,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:
@@ -369,9 +371,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

+ 29 - 0
resources/definitions/anycubic_kobra_plus.def.json

@@ -0,0 +1,29 @@
+{
+    "version": 2,
+    "name": "Anycubic Kobra Plus",
+    "inherits": "fdmprinter",
+    "metadata":
+    {
+        "visible": true,
+        "author": "Jordon Brooks",
+        "manufacturer": "Anycubic",
+        "file_formats": "text/x-gcode",
+        "has_machine_quality": true,
+        "has_materials": true,
+        "machine_extruder_trains": { "0": "anycubic_kobra_plus_extruder_0" },
+        "preferred_material": "generic_pla",
+        "preferred_quality_type": "normal",
+        "quality_definition": "anycubic_kobra_plus"
+    },
+    "overrides":
+    {
+        "machine_depth": { "default_value": 302 },
+        "machine_end_gcode": { "default_value": "M104 S0\nM140 S0\n;Retract the filament\nG92 E1\nG1 E-1 F300\nG28 X0 Y0\nM84\nM355 S0; led off" },
+        "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
+        "machine_heated_bed": { "default_value": true },
+        "machine_height": { "default_value": 352 },
+        "machine_name": { "default_value": "Anycubic Kobra Plus" },
+        "machine_start_gcode": { "default_value": "G28 ;Home\nG1 Z15.0 F6000 ;Move the platform down 15mm\n;Prime the extruder\nG92 E0\nM355 S1; Turn LED on\n; Add Custom purge lines\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X1.0 Y30 Z0.3 F5000.0 ; Move to start position\nG1 X1.0 Y100.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X1.3 Y100.0 Z0.3 F5000.0 ; Move to side a little\nG1 X1.3 Y30 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 E-2 F500 ; Retract a little \nG1 X50 F500 ; wipe away from the filament line\nG1 X100 F9000 ; Quickly wipe away from the filament line\nG1 Z5.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\n; End custom purge lines" },
+        "machine_width": { "default_value": 302 }
+    }
+}

+ 1 - 1
resources/definitions/fdmprinter.def.json

@@ -5921,7 +5921,7 @@
                     "maximum_value_warning": "skirt_brim_line_width",
                     "enabled": "resolveOrValue('adhesion_type') == 'brim'",
                     "limit_to_extruder": "skirt_brim_extruder_nr",
-                    "settable_per_mesh": true,
+                    "settable_per_mesh": false,
                     "settable_per_extruder": true
                 },
                 "brim_replaces_support":

+ 16 - 0
resources/extruders/anycubic_kobra_plus_extruder_0.def.json

@@ -0,0 +1,16 @@
+{
+    "version": 2,
+    "name": "Extruder 1",
+    "inherits": "fdmextruder",
+    "metadata":
+    {
+        "machine": "anycubic_kobra_plus",
+        "position": "0"
+    },
+    "overrides":
+    {
+        "extruder_nr": { "default_value": 0 },
+        "machine_nozzle_size": { "default_value": 0.4 },
+        "material_diameter": { "default_value": 1.75 }
+    }
+}

+ 38 - 2
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
@@ -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

+ 2 - 0
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

Some files were not shown because too many files changed in this diff