Browse Source

Now using ThreeMFWriter to save PCB files

CURA-11561
Erwan MATHIEU 1 year ago
parent
commit
b931029f1c

+ 0 - 0
plugins/PCBWriter/PCBDialog.py → plugins/3MFWriter/PCBDialog.py


+ 0 - 1
plugins/PCBWriter/PCBDialog.qml → plugins/3MFWriter/PCBDialog.qml

@@ -8,7 +8,6 @@ import QtQuick.Window 2.2
 
 import UM 1.5 as UM
 import Cura 1.1 as Cura
-import PCBWriter 1.0 as PCBWriter
 
 UM.Dialog
 {

+ 0 - 0
plugins/PCBWriter/SettingExport.py → plugins/3MFWriter/SettingExport.py


+ 1 - 1
plugins/PCBWriter/SettingSelection.qml → plugins/3MFWriter/SettingSelection.qml

@@ -32,7 +32,7 @@ RowLayout
         UM.I18nCatalog { id: catalog; name: "cura" }
 
         text: catalog.i18nc("@tooltip",
-                            "This setting can't be exported because it depends too much on the used printer capacities")
+                            "This setting can't be exported because it depends on the used printer capacities")
         visible: !modelData.selectable
     }
 }

+ 0 - 0
plugins/PCBWriter/SettingsExportGroup.py → plugins/3MFWriter/SettingsExportGroup.py


+ 0 - 0
plugins/PCBWriter/SettingsExportModel.py → plugins/3MFWriter/SettingsExportModel.py


+ 4 - 4
plugins/PCBWriter/SettingsSelectionGroup.qml → plugins/3MFWriter/SettingsSelectionGroup.qml

@@ -8,7 +8,7 @@ import QtQuick.Window 2.2
 
 import UM 1.5 as UM
 import Cura 1.1 as Cura
-import PCBWriter 1.0 as PCBWriter
+import ThreeMFWriter 1.0 as ThreeMFWriter
 
 ColumnLayout
 {
@@ -34,9 +34,9 @@ ColumnLayout
                 {
                     switch(modelData.category)
                     {
-                        case PCBWriter.SettingsExportGroup.Global:
+                        case ThreeMFWriter.SettingsExportGroup.Global:
                             return UM.Theme.getIcon("Sliders")
-                        case PCBWriter.SettingsExportGroup.Model:
+                        case ThreeMFWriter.SettingsExportGroup.Model:
                             return UM.Theme.getIcon("View3D")
                         default:
                             return ""
@@ -50,7 +50,7 @@ ColumnLayout
             {
                 id: settingsExtruderIcon
                 anchors.fill: parent
-                visible: modelData.category === PCBWriter.SettingsExportGroup.Extruder
+                visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder
                 text: (modelData.extruder_index + 1).toString()
                 font: UM.Theme.getFont("tiny_emphasis")
                 materialColor: modelData.extruder_color

+ 109 - 19
plugins/3MFWriter/ThreeMFWorkspaceWriter.py

@@ -3,7 +3,9 @@
 
 import configparser
 from io import StringIO
+from threading import Lock
 import zipfile
+from typing import Dict, Any
 
 from UM.Application import Application
 from UM.Logger import Logger
@@ -13,15 +15,50 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
 
-from cura.Utils.Threading import call_on_qt_thread
+from .PCBDialog import PCBDialog
+from .ThreeMFWriter import ThreeMFWriter
+from .SettingsExportModel import SettingsExportModel
+from .SettingsExportGroup import SettingsExportGroup
+
+USER_SETTINGS_PATH = "Cura/user-settings.json"
 
 
 class ThreeMFWorkspaceWriter(WorkspaceWriter):
     def __init__(self):
         super().__init__()
+        self._main_thread_lock = Lock()
+        self._success = False
+        self._export_model = None
+        self._stream = None
+        self._nodes = None
+        self._mode = None
+        self._config_dialog = None
 
-    @call_on_qt_thread
-    def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
+    def _preWrite(self):
+        is_pcb = False
+        if hasattr(self._stream, 'name'):
+            # This only works with local file, but we don't want remote PCB files yet
+            is_pcb = self._stream.name.endswith('.pcb')
+
+        if is_pcb:
+            self._config_dialog = PCBDialog()
+            self._config_dialog.finished.connect(self._onPCBConfigFinished)
+            self._config_dialog.show()
+        else:
+            self._doWrite()
+
+    def _onPCBConfigFinished(self, accepted: bool):
+        if accepted:
+            self._export_model = self._config_dialog.getModel()
+            self._doWrite()
+        else:
+            self._main_thread_lock.release()
+
+    def _doWrite(self):
+        self._write()
+        self._main_thread_lock.release()
+
+    def _write(self):
         application = Application.getInstance()
         machine_manager = application.getMachineManager()
 
@@ -30,24 +67,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
         if not mesh_writer:  # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
             self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
             Logger.error("3MF Writer class is unavailable. Can't write workspace.")
-            return False
+            return
 
         global_stack = machine_manager.activeMachine
         if global_stack is None:
-            self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
+            self.setInformation(
+                catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
             Logger.error("Tried to write a 3MF workspace before there was a global stack.")
-            return False
+            return
 
         # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
         mesh_writer.setStoreArchive(True)
-        if not mesh_writer.write(stream, nodes, mode):
+        if not mesh_writer.write(self._stream, self._nodes, self._mode, self._export_model):
             self.setInformation(mesh_writer.getInformation())
-            return False
+            return
 
         archive = mesh_writer.getArchive()
         if archive is None:  # This happens if there was no mesh data to write.
-            archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
-
+            archive = zipfile.ZipFile(self._stream, "w", compression=zipfile.ZIP_DEFLATED)
 
         try:
             # Add global container stack data to the archive.
@@ -62,15 +99,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
                 self._writeContainerToArchive(extruder_stack, archive)
                 for container in extruder_stack.getContainers():
                     self._writeContainerToArchive(container, archive)
+
+            # Write user settings data
+            if self._export_model is not None:
+                user_settings_data = self._getUserSettings(self._export_model)
+                ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
         except PermissionError:
             self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
             Logger.error("No permission to write workspace to this stream.")
-            return False
+            return
 
         # Write preferences to archive
-        original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
+        original_preferences = Application.getInstance().getPreferences()  # Copy only the preferences that we use to the workspace.
         temp_preferences = Preferences()
-        for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}:
+        for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
+                           "metadata/setting_version"}:
             temp_preferences.addPreference(preference, None)
             temp_preferences.setValue(preference, original_preferences.getValue(preference))
         preferences_string = StringIO()
@@ -81,7 +124,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
 
             # Save Cura version
             version_file = zipfile.ZipInfo("Cura/version.ini")
-            version_config_parser = configparser.ConfigParser(interpolation = None)
+            version_config_parser = configparser.ConfigParser(interpolation=None)
             version_config_parser.add_section("versions")
             version_config_parser.set("versions", "cura_version", application.getVersion())
             version_config_parser.set("versions", "build_type", application.getBuildType())
@@ -98,13 +141,37 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
         except PermissionError:
             self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
             Logger.error("No permission to write workspace to this stream.")
-            return False
+            return
         except EnvironmentError as e:
             self.setInformation(catalog.i18nc("@error:zip", str(e)))
-            Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
-            return False
+            Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
+            return
         mesh_writer.setStoreArchive(False)
-        return True
+
+        self._success = True
+
+    def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
+        self._success = False
+        self._export_model = None
+        self._stream = stream
+        self._nodes = nodes
+        self._mode = mode
+        self._config_dialog = None
+
+        self._main_thread_lock.acquire()
+        # Export is done in main thread because it may require a few asynchronous configuration steps
+        Application.getInstance().callLater(self._preWrite)
+        self._main_thread_lock.acquire()  # Block until lock has been released, meaning the config+write is over
+
+        self._main_thread_lock.release()
+
+        self._export_model = None
+        self._stream = None
+        self._nodes = None
+        self._mode = None
+        self._config_dialog = None
+
+        return self._success
 
     @staticmethod
     def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
@@ -165,4 +232,27 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
             archive.writestr(file_in_archive, serialized_data)
         except (FileNotFoundError, EnvironmentError):
             Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
-            return
+            return
+
+    @staticmethod
+    def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
+        user_settings = {}
+
+        for group in model.settingsGroups:
+            category = ''
+            if group.category == SettingsExportGroup.Category.Global:
+                category = 'global'
+            elif group.category == SettingsExportGroup.Category.Extruder:
+                category = f"extruder_{group.extruder_index}"
+
+            if len(category) > 0:
+                settings_values = {}
+                stack = group.stack
+
+                for setting in group.settings:
+                    if setting.selected:
+                        settings_values[setting.id] = stack.getProperty(setting.id, "value")
+
+                user_settings[category] = settings_values
+
+        return user_settings

+ 48 - 11
plugins/3MFWriter/ThreeMFWriter.py

@@ -40,6 +40,9 @@ except ImportError:
 import zipfile
 import UM.Application
 
+from .SettingsExportModel import SettingsExportModel
+from .SettingsExportGroup import SettingsExportGroup
+
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
 
@@ -87,7 +90,9 @@ class ThreeMFWriter(MeshWriter):
         self._store_archive = store_archive
 
     @staticmethod
-    def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
+    def _convertUMNodeToSavitarNode(um_node,
+                                    transformation = Matrix(),
+                                    exported_settings: Optional[Dict[str, Set[str]]] = None):
         """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
 
         :returns: Uranium Scene node.
@@ -129,13 +134,22 @@ class ThreeMFWriter(MeshWriter):
         if stack is not None:
             changed_setting_keys = stack.getTop().getAllKeys()
 
-            # Ensure that we save the extruder used for this object in a multi-extrusion setup
-            if stack.getProperty("machine_extruder_count", "value") > 1:
-                changed_setting_keys.add("extruder_nr")
+            if exported_settings is None:
+                # Ensure that we save the extruder used for this object in a multi-extrusion setup
+                if stack.getProperty("machine_extruder_count", "value") > 1:
+                    changed_setting_keys.add("extruder_nr")
+
+                # Get values for all changed settings & save them.
+                for key in changed_setting_keys:
+                    savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
+            else:
+                 # We want to export only the specified settings
+                if um_node.getName() in exported_settings:
+                    model_exported_settings = exported_settings[um_node.getName()]
 
-            # Get values for all changed settings & save them.
-            for key in changed_setting_keys:
-                savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
+                    # Get values for all exported settings & save them.
+                    for key in model_exported_settings:
+                        savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
 
         # Store the metadata.
         for key, value in um_node.metadata.items():
@@ -145,7 +159,8 @@ 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 = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
+            savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
+                                                                           exported_settings = exported_settings)
             if savitar_child_node is not None:
                 savitar_node.addChild(savitar_child_node)
 
@@ -154,7 +169,7 @@ class ThreeMFWriter(MeshWriter):
     def getArchive(self):
         return self._archive
 
-    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
+    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool:
         self._archive = None # Reset archive
         archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
         try:
@@ -232,14 +247,19 @@ class ThreeMFWriter(MeshWriter):
                 transformation_matrix.preMultiply(translation_matrix)
 
             root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
+            exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model)
             for node in nodes:
                 if node == root_node:
                     for root_child in node.getChildren():
-                        savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
+                        savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
+                                                                                 transformation_matrix,
+                                                                                 exported_model_settings)
                         if savitar_node:
                             savitar_scene.addSceneNode(savitar_node)
                 else:
-                    savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
+                    savitar_node = self._convertUMNodeToSavitarNode(node,
+                                                                    transformation_matrix,
+                                                                    exported_model_settings)
                     if savitar_node:
                         savitar_scene.addSceneNode(savitar_node)
 
@@ -395,3 +415,20 @@ class ThreeMFWriter(MeshWriter):
         parser = Savitar.ThreeMFParser()
         scene_string = parser.sceneToString(savitar_scene)
         return scene_string
+
+    @staticmethod
+    def _extractModelExportedSettings(model: Optional[SettingsExportModel]) -> Dict[str, Set[str]]:
+        extra_settings = {}
+
+        if model is not None:
+            for group in model.settingsGroups:
+                if group.category == SettingsExportGroup.Category.Model:
+                    exported_model_settings = set()
+
+                    for exported_setting in group.settings:
+                        if exported_setting.selected:
+                            exported_model_settings.add(exported_setting.id)
+
+                    extra_settings[group.category_details] = exported_model_settings
+
+        return extra_settings

+ 33 - 12
plugins/3MFWriter/__init__.py

@@ -2,9 +2,12 @@
 # Uranium is released under the terms of the LGPLv3 or higher.
 import sys
 
+from PyQt6.QtQml import qmlRegisterType
+
 from UM.Logger import Logger
 try:
     from . import ThreeMFWriter
+    from .SettingsExportGroup import SettingsExportGroup
     threemf_writer_was_imported = True
 except ImportError:
     Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
@@ -23,20 +26,36 @@ def getMetaData():
 
     if threemf_writer_was_imported:
         metaData["mesh_writer"] = {
-            "output": [{
-                "extension": "3mf",
-                "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
-                "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
-                "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
-            }]
+            "output": [
+                {
+                    "extension": "3mf",
+                    "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
+                    "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
+                    "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
+                },
+                {
+                    "extension": "pcb",
+                    "description": i18n_catalog.i18nc("@item:inlistbox", "PCB file"),
+                    "mime_type": "application/x-pcb",
+                    "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
+                }
+            ]
         }
         metaData["workspace_writer"] = {
-            "output": [{
-                "extension": workspace_extension,
-                "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
-                "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
-                "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
-            }]
+            "output": [
+                {
+                    "extension": workspace_extension,
+                    "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
+                    "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
+                    "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
+                },
+                {
+                    "extension": "pcb",
+                    "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"),
+                    "mime_type": "application/x-pcb",
+                    "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
+                }
+            ]
         }
 
     return metaData
@@ -44,6 +63,8 @@ def getMetaData():
 
 def register(app):
     if "3MFWriter.ThreeMFWriter" in sys.modules:
+        qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup")
+
         return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), 
                 "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
     else:

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