Browse Source

Merge branch 'main' into fix-ratrig-mesh-offsets

Saumya Jain 1 year ago
parent
commit
5faf164024

+ 2 - 2
cura/BuildVolume.py

@@ -880,7 +880,7 @@ class BuildVolume(SceneNode):
             result[extruder.getId()] = []
 
         # Currently, the only normally printed object is the prime tower.
-        if self._global_container_stack.getProperty("prime_tower_mode", "value") != 'none':
+        if self._global_container_stack.getProperty("prime_tower_enable", "value"):
             prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
             machine_width = self._global_container_stack.getProperty("machine_width", "value")
             machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
@@ -1208,7 +1208,7 @@ class BuildVolume(SceneNode):
     _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_layers", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
     _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
     _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
-    _tower_settings = ["prime_tower_mode", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable", "prime_tower_base_size", "prime_tower_base_height"]
+    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable", "prime_tower_base_size", "prime_tower_base_height"]
     _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
     _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
     _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.

+ 4 - 0
cura/CuraActions.py

@@ -273,7 +273,11 @@ class CuraActions(QObject):
         # deselect currently selected nodes, and select the new nodes
         for node in Selection.getAllSelectedObjects():
             Selection.remove(node)
+
+        numberOfFixedNodes = len(fixed_nodes)
         for node in nodes:
+            numberOfFixedNodes += 1
+            node.printOrder = numberOfFixedNodes
             Selection.add(node)
 
     def _openUrl(self, url: QUrl) -> None:

+ 16 - 3
cura/CuraApplication.py

@@ -601,7 +601,9 @@ class CuraApplication(QtApplication):
         preferences.addPreference("mesh/scale_to_fit", False)
         preferences.addPreference("mesh/scale_tiny_meshes", True)
         preferences.addPreference("cura/dialog_on_project_save", True)
+        preferences.addPreference("cura/dialog_on_ucp_project_save", True)
         preferences.addPreference("cura/asked_dialog_on_project_save", False)
+        preferences.addPreference("cura/asked_dialog_on_ucp_project_save", False)
         preferences.addPreference("cura/choice_on_profile_override", "always_ask")
         preferences.addPreference("cura/choice_on_open_project", "always_ask")
         preferences.addPreference("cura/use_multi_build_plate", False)
@@ -617,6 +619,7 @@ class CuraApplication(QtApplication):
 
         preferences.addPreference("view/invert_zoom", False)
         preferences.addPreference("view/filter_current_build_plate", False)
+        preferences.addPreference("view/navigation_style", "cura")
         preferences.addPreference("cura/sidebar_collapsed", False)
 
         preferences.addPreference("cura/favorite_materials", "")
@@ -1082,9 +1085,9 @@ class CuraApplication(QtApplication):
     def getTextManager(self, *args) -> "TextManager":
         return self._text_manager
 
-    @pyqtSlot(bool)
-    def getWorkplaceDropToBuildplate(self, drop_to_build_plate: bool) ->None:
-        return self._physics.setAppPerModelDropDown(drop_to_build_plate)
+    @pyqtSlot()
+    def setWorkplaceDropToBuildplate(self):
+        return self._physics.setAppAllModelDropDown()
 
     def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
         if self._cura_formula_functions is None:
@@ -1141,6 +1144,16 @@ class CuraApplication(QtApplication):
             self._build_plate_model = BuildPlateModel(self)
         return self._build_plate_model
 
+    @pyqtSlot()
+    def exportUcp(self):
+        writer = self.getMeshFileHandler().getWriter("3MFWriter")
+
+        if writer is None:
+            Logger.warning("3mf writer is not enabled")
+            return
+
+        writer.exportUcp()
+
     def getCuraSceneController(self, *args) -> CuraSceneController:
         if self._cura_scene_controller is None:
             self._cura_scene_controller = CuraSceneController.createCuraSceneController()

+ 8 - 14
cura/PlatformPhysics.py

@@ -38,14 +38,12 @@ class PlatformPhysics:
         self._minimum_gap = 2  # It is a minimum distance (in mm) between two models, applicable for small models
 
         Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
-        Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", False)
-        self._app_per_model_drop = Application.getInstance().getPreferences().getValue("physics/automatic_drop_down")
+        Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
+        self._app_all_model_drop = False
 
-    def getAppPerModelDropDown(self):
-        return self._app_per_model_drop
-
-    def setAppPerModelDropDown(self, drop_to_buildplate):
-        self._app_per_model_drop = drop_to_buildplate
+    def setAppAllModelDropDown(self):
+        self._app_all_model_drop = True
+        self._onChangeTimerFinished()
 
     def _onSceneChanged(self, source):
         if not source.callDecoration("isSliceable"):
@@ -78,7 +76,6 @@ class PlatformPhysics:
         # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
         # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
         random.shuffle(nodes)
-
         for node in nodes:
             if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
                 continue
@@ -88,12 +85,9 @@ class PlatformPhysics:
             # Move it downwards if bottom is above platform
             move_vector = Vector()
 
-            # if per model drop is different then app_automatic_drop, in case of 3mf loading when user changes this setting for that model
-            if (self._app_per_model_drop != app_automatic_drop_down):
-                node.setSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop)
-            if node.getSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
+            if (node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) or self._app_all_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root)  and node.isEnabled():
                 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
-                move_vector = move_vector.set(y = -bbox.bottom + z_offset)
+                move_vector = move_vector.set(y=-bbox.bottom + z_offset)
 
             # If there is no convex hull for the node, start calculating it and continue.
             if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
@@ -180,7 +174,7 @@ class PlatformPhysics:
                 op.push()
 
         # setting this drop to model same as app_automatic_drop_down
-        self._app_per_model_drop = app_automatic_drop_down
+        self._app_all_model_drop = False
         # After moving, we have to evaluate the boundary checks for nodes
         build_volume.updateNodeBoundaryCheck()
 

+ 3 - 0
cura/PrintOrderManager.py

@@ -116,6 +116,9 @@ class PrintOrderManager(QObject):
                                      ) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
         nodes = self._get_nodes()
         ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
+        for i, node in enumerate(ordered_nodes, 1):
+            node.printOrder = i
+
         selected_node = PrintOrderManager._getSingleSelectedNode()
         if selected_node and selected_node in ordered_nodes:
             selected_node_index = ordered_nodes.index(selected_node)

+ 5 - 0
cura/Scene/CuraSceneNode.py

@@ -11,6 +11,7 @@ from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNodeDecorator import SceneNodeDecorator  # To cast the deepcopy of every decorator back to SceneNodeDecorator.
 
 import cura.CuraApplication  # To get the build plate.
+from UM.Scene.SceneNodeSettings import SceneNodeSettings
 from cura.Settings.ExtruderStack import ExtruderStack  # For typing.
 from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator  # For per-object settings.
 
@@ -41,6 +42,10 @@ class CuraSceneNode(SceneNode):
     def isOutsideBuildArea(self) -> bool:
         return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
 
+    @property
+    def isDropDownEnabled(self) ->bool:
+        return self.getSetting(SceneNodeSettings.AutoDropDown, Application.getInstance().getPreferences().getValue("physics/automatic_drop_down"))
+
     def isVisible(self) -> bool:
         return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
 

+ 38 - 0
plugins/3MFReader/SpecificSettingsModel.py

@@ -0,0 +1,38 @@
+# Copyright (c) 2024 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt6.QtCore import Qt
+
+from UM.Settings.SettingDefinition import SettingDefinition
+from UM.Qt.ListModel import ListModel
+
+
+class SpecificSettingsModel(ListModel):
+    CategoryRole = Qt.ItemDataRole.UserRole + 1
+    LabelRole = Qt.ItemDataRole.UserRole + 2
+    ValueRole = Qt.ItemDataRole.UserRole + 3
+
+    def __init__(self, parent = None):
+        super().__init__(parent = parent)
+        self.addRoleName(self.CategoryRole, "category")
+        self.addRoleName(self.LabelRole, "label")
+        self.addRoleName(self.ValueRole, "value")
+
+        self._i18n_catalog = None
+
+    def addSettingsFromStack(self, stack, category, settings):
+        for setting, value in settings.items():
+            unit = stack.getProperty(setting, "unit")
+
+            setting_type = stack.getProperty(setting, "type")
+            if setting_type is not None:
+                # This is not very good looking, but will do for now
+                value = SettingDefinition.settingValueToString(setting_type, value) + " " + unit
+            else:
+                value = str(value)
+
+            self.appendItem({
+                "category": category,
+                "label": stack.getProperty(setting, "label"),
+                "value": value
+            })

+ 5 - 1
plugins/3MFReader/ThreeMFReader.py

@@ -16,6 +16,7 @@ from UM.Mesh.MeshReader import MeshReader
 from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
 from UM.Scene.GroupDecorator import GroupDecorator
 from UM.Scene.SceneNode import SceneNode  # For typing.
+from UM.Scene.SceneNodeSettings import SceneNodeSettings
 from cura.CuraApplication import CuraApplication
 from cura.Machines.ContainerTree import ContainerTree
 from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
@@ -41,7 +42,7 @@ class ThreeMFReader(MeshReader):
 
         MimeTypeDatabase.addMimeType(
             MimeType(
-                name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
+                name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
                 comment="3MF",
                 suffixes=["3mf"]
             )
@@ -180,6 +181,9 @@ class ThreeMFReader(MeshReader):
                 if key == "print_order":
                     um_node.printOrder = int(setting_value)
                     continue
+                if key =="drop_to_buildplate":
+                    um_node.setSetting(SceneNodeSettings.AutoDropDown, eval(setting_value))
+                    continue
                 if key in known_setting_keys:
                     setting_container.setProperty(key, "value", setting_value)
                 else:

+ 163 - 83
plugins/3MFReader/ThreeMFWorkspaceReader.py

@@ -5,6 +5,7 @@ from configparser import ConfigParser
 import zipfile
 import os
 import json
+import re
 from typing import cast, Dict, List, Optional, Tuple, Any, Set
 
 import xml.etree.ElementTree as ET
@@ -57,6 +58,7 @@ _ignored_machine_network_metadata: Set[str] = {
     "is_abstract_machine"
 }
 
+USER_SETTINGS_PATH = "Cura/user-settings.json"
 
 class ContainerInfo:
     def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
@@ -141,10 +143,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._old_new_materials: Dict[str, str] = {}
         self._machine_info = None
 
+        self._load_profile = False
+        self._user_settings: Dict[str, Dict[str, Any]] = {}
+
     def _clearState(self):
         self._id_mapping = {}
         self._old_new_materials = {}
         self._machine_info = None
+        self._load_profile = False
+        self._user_settings = {}
 
     def getNewId(self, old_id: str):
         """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
@@ -228,11 +235,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._resolve_strategies = {k: None for k in resolve_strategy_keys}
         containers_found_dict = {k: False for k in resolve_strategy_keys}
 
+        # Check whether the file is a UCP, which changes some import options
+        is_ucp = USER_SETTINGS_PATH in cura_file_names
+
         #
         # Read definition containers
         #
         machine_definition_id = None
-        updatable_machines = []
+        updatable_machines = None if is_ucp else []
         machine_definition_container_count = 0
         extruder_definition_container_count = 0
         definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
@@ -250,7 +260,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             if definition_container_type == "machine":
                 machine_definition_id = container_id
                 machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
-                if machine_definition_containers:
+                if machine_definition_containers and updatable_machines is not None:
                     updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
                 machine_type = definition_container["name"]
                 variant_type_name = definition_container.get("variants_name", variant_type_name)
@@ -597,6 +607,37 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         package_metadata = self._parse_packages_metadata(archive)
         missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
 
+        # Load the user specifically exported settings
+        self._dialog.exportedSettingModel.clear()
+        if is_ucp:
+            try:
+                self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8"))
+                any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
+                actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack()
+
+                for stack_name, settings in self._user_settings.items():
+                    if stack_name == 'global':
+                        self._dialog.exportedSettingModel.addSettingsFromStack(actual_global_stack, i18n_catalog.i18nc("@label", "Global"), settings)
+                    else:
+                        extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
+                        if extruder_match is not None:
+                            extruder_nr = int(extruder_match.group(1))
+                            self._dialog.exportedSettingModel.addSettingsFromStack(any_extruder_stack,
+                                                                                   i18n_catalog.i18nc("@label",
+                                                                                                      "Extruder {0}", extruder_nr + 1),
+                                                                                   settings)
+            except KeyError as e:
+                # If there is no user settings file, it's not a UCP, so notify user of failure.
+                Logger.log("w", "File %s is not a valid UCP.", file_name)
+                message = Message(
+                    i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
+                                       "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
+                                       file_name, str(e)),
+                    title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                    message_type=Message.MessageType.ERROR)
+                message.show()
+                return WorkspaceReader.PreReadResult.failed
+
         # Show the dialog, informing the user what is about to happen.
         self._dialog.setMachineConflict(machine_conflict)
         self._dialog.setIsPrinterGroup(is_printer_group)
@@ -617,8 +658,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._dialog.setVariantType(variant_type_name)
         self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
         self._dialog.setMissingPackagesMetadata(missing_package_metadata)
+        self._dialog.setHasVisibleSelectSameProfileChanged(is_ucp)
+        self._dialog.setAllowCreatemachine(not is_ucp)
         self._dialog.show()
 
+
         # Choosing the initially selected printer in MachineSelector
         is_networked_machine = False
         is_abstract_machine = False
@@ -648,6 +692,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         self._dialog.setIsNetworkedMachine(is_networked_machine)
         self._dialog.setIsAbstractMachine(is_abstract_machine)
         self._dialog.setMachineName(machine_name)
+        self._dialog.updateCompatibleMachine()
+        self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine)
 
         # Block until the dialog is closed.
         self._dialog.waitForClose()
@@ -655,6 +701,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         if self._dialog.getResult() == {}:
             return WorkspaceReader.PreReadResult.cancelled
 
+        self._load_profile = not is_ucp or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine)
+
         self._resolve_strategies = self._dialog.getResult()
         #
         # There can be 3 resolve strategies coming from the dialog:
@@ -690,16 +738,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
         except EnvironmentError as e:
             message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
                                                  "Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
-                                                 title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
-                                                 message_type = Message.MessageType.ERROR)
+                              title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                              message_type = Message.MessageType.ERROR)
             message.show()
             self.setWorkspaceName("")
             return [], {}
         except zipfile.BadZipFile as e:
             message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
                                                  "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
-                                                 title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
-                                                 message_type = Message.MessageType.ERROR)
+                              title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
+                              message_type = Message.MessageType.ERROR)
             message.show()
             self.setWorkspaceName("")
             return [], {}
@@ -761,9 +809,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             # Find the machine which will be overridden
             global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
             if not global_stacks:
-                message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!", 
+                message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
                                                      "Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
-                                                     message_type = Message.MessageType.ERROR)
+                                  message_type = Message.MessageType.ERROR)
                 message.show()
                 self.setWorkspaceName("")
                 return [], {}
@@ -777,84 +825,89 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             for stack in extruder_stacks:
                 stack.setNextStack(global_stack, connect_signals = False)
 
-        Logger.log("d", "Workspace loading is checking definitions...")
-        # Get all the definition files & check if they exist. If not, add them.
-        definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
-        for definition_container_file in definition_container_files:
-            container_id = self._stripFileToId(definition_container_file)
+        if self._load_profile:
+            Logger.log("d", "Workspace loading is checking definitions...")
+            # Get all the definition files & check if they exist. If not, add them.
+            definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
+            for definition_container_file in definition_container_files:
+                container_id = self._stripFileToId(definition_container_file)
 
-            definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
-            if not definitions:
-                definition_container = DefinitionContainer(container_id)
-                try:
-                    definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
-                                                     file_name = definition_container_file)
-                except ContainerFormatError:
-                    # We cannot just skip the definition file because everything else later will just break if the
-                    # machine definition cannot be found.
-                    Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
-                                        definition_container_file, file_name)
-                    definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
-                self._container_registry.addContainer(definition_container)
-            Job.yieldThread()
-            QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
-
-        Logger.log("d", "Workspace loading is checking materials...")
-        # Get all the material files and check if they exist. If not, add them.
-        xml_material_profile = self._getXmlProfileClass()
-        if self._material_container_suffix is None:
-            self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
-        if xml_material_profile:
-            material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
-            for material_container_file in material_container_files:
-                to_deserialize_material = False
-                container_id = self._stripFileToId(material_container_file)
-                need_new_name = False
-                materials = self._container_registry.findInstanceContainers(id = container_id)
-
-                if not materials:
-                    # No material found, deserialize this material later and add it
-                    to_deserialize_material = True
-                else:
-                    material_container = materials[0]
-                    old_material_root_id = material_container.getMetaDataEntry("base_file")
-                    if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id):  # Only create new materials if they are not read only.
-                        to_deserialize_material = True
-
-                        if self._resolve_strategies["material"] == "override":
-                            # Remove the old materials and then deserialize the one from the project
-                            root_material_id = material_container.getMetaDataEntry("base_file")
-                            application.getContainerRegistry().removeContainer(root_material_id)
-                        elif self._resolve_strategies["material"] == "new":
-                            # Note that we *must* deserialize it with a new ID, as multiple containers will be
-                            # auto created & added.
-                            container_id = self.getNewId(container_id)
-                            self._old_new_materials[old_material_root_id] = container_id
-                            need_new_name = True
-
-                if to_deserialize_material:
-                    material_container = xml_material_profile(container_id)
+                definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
+                if not definitions:
+                    definition_container = DefinitionContainer(container_id)
                     try:
-                        material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
-                                                       file_name = container_id + "." + self._material_container_suffix)
+                        definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
+                                                         file_name = definition_container_file)
                     except ContainerFormatError:
-                        Logger.logException("e", "Failed to deserialize material file %s in project file %s",
-                                            material_container_file, file_name)
-                        continue
-                    if need_new_name:
-                        new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
-                        material_container.setName(new_name)
-                    material_container.setDirty(True)
-                    self._container_registry.addContainer(material_container)
+                        # We cannot just skip the definition file because everything else later will just break if the
+                        # machine definition cannot be found.
+                        Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
+                                            definition_container_file, file_name)
+                        definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
+                    self._container_registry.addContainer(definition_container)
                 Job.yieldThread()
                 QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
 
+            Logger.log("d", "Workspace loading is checking materials...")
+            # Get all the material files and check if they exist. If not, add them.
+            xml_material_profile = self._getXmlProfileClass()
+            if self._material_container_suffix is None:
+                self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
+            if xml_material_profile:
+                material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
+                for material_container_file in material_container_files:
+                    to_deserialize_material = False
+                    container_id = self._stripFileToId(material_container_file)
+                    need_new_name = False
+                    materials = self._container_registry.findInstanceContainers(id = container_id)
+
+                    if not materials:
+                        # No material found, deserialize this material later and add it
+                        to_deserialize_material = True
+                    else:
+                        material_container = materials[0]
+                        old_material_root_id = material_container.getMetaDataEntry("base_file")
+                        if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id):  # Only create new materials if they are not read only.
+                            to_deserialize_material = True
+
+                            if self._resolve_strategies["material"] == "override":
+                                # Remove the old materials and then deserialize the one from the project
+                                root_material_id = material_container.getMetaDataEntry("base_file")
+                                application.getContainerRegistry().removeContainer(root_material_id)
+                            elif self._resolve_strategies["material"] == "new":
+                                # Note that we *must* deserialize it with a new ID, as multiple containers will be
+                                # auto created & added.
+                                container_id = self.getNewId(container_id)
+                                self._old_new_materials[old_material_root_id] = container_id
+                                need_new_name = True
+
+                    if to_deserialize_material:
+                        material_container = xml_material_profile(container_id)
+                        try:
+                            material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
+                                                           file_name = container_id + "." + self._material_container_suffix)
+                        except ContainerFormatError:
+                            Logger.logException("e", "Failed to deserialize material file %s in project file %s",
+                                                material_container_file, file_name)
+                            continue
+                        if need_new_name:
+                            new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
+                            material_container.setName(new_name)
+                        material_container.setDirty(True)
+                        self._container_registry.addContainer(material_container)
+                    Job.yieldThread()
+                    QCoreApplication.processEvents()  # Ensure that the GUI does not freeze.
+
         if global_stack:
-            # Handle quality changes if any
-            self._processQualityChanges(global_stack)
+            if self._load_profile:
+                # Handle quality changes if any
+                self._processQualityChanges(global_stack)
 
-            # Prepare the machine
-            self._applyChangesToMachine(global_stack, extruder_stack_dict)
+                # Prepare the machine
+                self._applyChangesToMachine(global_stack, extruder_stack_dict)
+            else:
+                # Just clear the settings now, so that we can change the active machine without conflicts
+                self._clearMachineSettings(global_stack, extruder_stack_dict)
 
             Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
             # Actually change the active machine.
@@ -866,6 +919,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             # To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
             self._updateActiveMachine(global_stack)
 
+            if not self._load_profile:
+                # Now we have switched, apply the user settings
+                self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings)
+
         # Load all the nodes / mesh data of the workspace
         nodes = self._3mf_mesh_reader.read(file_name)
         if nodes is None:
@@ -1177,21 +1234,44 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
             material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
             extruder_stack.material = material_node.container
 
-    def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
-        # Clear all first
+    def _clearMachineSettings(self, global_stack, extruder_stack_dict):
         self._clearStack(global_stack)
         for extruder_stack in extruder_stack_dict.values():
             self._clearStack(extruder_stack)
 
+        self._quality_changes_to_apply = None
+        self._quality_type_to_apply = None
+        self._intent_category_to_apply = None
+        self._user_settings_to_apply = None
+
+    def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
+        for stack_name, settings in user_settings.items():
+            if stack_name == 'global':
+                ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
+            else:
+                extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
+                if extruder_match is not None:
+                    extruder_nr = extruder_match.group(1)
+                    if extruder_nr in extruder_stack_dict:
+                        ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
+
+    @staticmethod
+    def _applyUserSettingsOnStack(stack, user_settings):
+        user_settings_container = stack.userChanges
+
+        for setting_to_import, setting_value in user_settings.items():
+            user_settings_container.setProperty(setting_to_import, 'value', setting_value)
+
+    def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
+        # Clear all first
+        self._clearMachineSettings(global_stack, extruder_stack_dict)
+
         self._applyDefinitionChanges(global_stack, extruder_stack_dict)
         self._applyUserChanges(global_stack, extruder_stack_dict)
         self._applyVariants(global_stack, extruder_stack_dict)
         self._applyMaterials(global_stack, extruder_stack_dict)
 
         # prepare the quality to select
-        self._quality_changes_to_apply = None
-        self._quality_type_to_apply = None
-        self._intent_category_to_apply = None
         if self._machine_info.quality_changes_info is not None:
             self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
         else:

+ 54 - 5
plugins/3MFReader/WorkspaceDialog.py

@@ -22,6 +22,8 @@ import time
 
 from cura.CuraApplication import CuraApplication
 
+from .SpecificSettingsModel import SpecificSettingsModel
+
 i18n_catalog = i18nCatalog("cura")
 
 
@@ -71,6 +73,11 @@ class WorkspaceDialog(QObject):
         self._install_missing_package_dialog: Optional[QObject] = None
         self._is_abstract_machine = False
         self._is_networked_machine = False
+        self._is_compatible_machine = False
+        self._has_visible_select_same_profile = False
+        self._select_same_profile_checked = True
+        self._allow_create_machine = True
+        self._exported_settings_model = SpecificSettingsModel()
 
     machineConflictChanged = pyqtSignal()
     qualityChangesConflictChanged = pyqtSignal()
@@ -94,6 +101,9 @@ class WorkspaceDialog(QObject):
     extrudersChanged = pyqtSignal()
     isPrinterGroupChanged = pyqtSignal()
     missingPackagesChanged = pyqtSignal()
+    isCompatibleMachineChanged = pyqtSignal()
+    hasVisibleSelectSameProfileChanged = pyqtSignal()
+    selectSameProfileCheckedChanged = pyqtSignal()
 
     @pyqtProperty(bool, notify = isPrinterGroupChanged)
     def isPrinterGroup(self) -> bool:
@@ -292,6 +302,50 @@ class WorkspaceDialog(QObject):
     @pyqtSlot(str)
     def setMachineToOverride(self, machine_name: str) -> None:
         self._override_machine = machine_name
+        self.updateCompatibleMachine()
+
+    def updateCompatibleMachine(self):
+        registry = ContainerRegistry.getInstance()
+        containers_expected = registry.findDefinitionContainers(name=self._machine_type)
+        containers_selected = registry.findContainerStacks(id=self._override_machine)
+        if len(containers_expected) == 1 and len(containers_selected) == 1:
+            new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
+            if new_compatible_machine != self._is_compatible_machine:
+                self._is_compatible_machine = new_compatible_machine
+                self.isCompatibleMachineChanged.emit()
+
+    @pyqtProperty(bool, notify = isCompatibleMachineChanged)
+    def isCompatibleMachine(self) -> bool:
+        return self._is_compatible_machine
+
+    def setHasVisibleSelectSameProfileChanged(self, has_visible_select_same_profile):
+        if has_visible_select_same_profile != self._has_visible_select_same_profile:
+            self._has_visible_select_same_profile = has_visible_select_same_profile
+            self.hasVisibleSelectSameProfileChanged.emit()
+
+    @pyqtProperty(bool, notify = hasVisibleSelectSameProfileChanged)
+    def hasVisibleSelectSameProfile(self):
+        return self._has_visible_select_same_profile
+
+    def setSelectSameProfileChecked(self, select_same_profile_checked):
+        if select_same_profile_checked != self._select_same_profile_checked:
+            self._select_same_profile_checked = select_same_profile_checked
+            self.selectSameProfileCheckedChanged.emit()
+
+    @pyqtProperty(bool, notify = selectSameProfileCheckedChanged, fset = setSelectSameProfileChecked)
+    def selectSameProfileChecked(self):
+        return self._select_same_profile_checked
+
+    def setAllowCreatemachine(self, allow_create_machine):
+        self._allow_create_machine = allow_create_machine
+
+    @pyqtProperty(bool, constant = True)
+    def allowCreateMachine(self):
+        return self._allow_create_machine
+
+    @pyqtProperty(QObject, constant = True)
+    def exportedSettingModel(self):
+        return self._exported_settings_model
 
     @pyqtSlot()
     def closeBackend(self) -> None:
@@ -299,11 +353,6 @@ class WorkspaceDialog(QObject):
 
         Application.getInstance().getBackend().close()
 
-    @pyqtSlot(bool)
-    def setDropToBuildPlateForModel(self, drop_to_buildplate: bool) -> None:
-        CuraApplication.getInstance().getWorkplaceDropToBuildplate(drop_to_buildplate)
-
-
     def setMaterialConflict(self, material_conflict: bool) -> None:
         if self._has_material_conflict != material_conflict:
             self._has_material_conflict = material_conflict

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