From efc9719ff08708af98d9275b77f198bf6a9a2fb0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 10:00:00 +0100 Subject: [PATCH 01/50] Added stub workspace reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 12 ++++++++++++ plugins/3MFReader/__init__.py | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 plugins/3MFReader/ThreeMFWorkspaceReader.py diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py new file mode 100644 index 0000000000..c1c78037dd --- /dev/null +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -0,0 +1,12 @@ +from UM.Workspace.WorkspaceReader import WorkspaceReader + + +## Base implementation for reading 3MF workspace files. +class ThreeMFWorkspaceReader(WorkspaceReader): + def __init__(self): + super().__init__() + + def preRead(self, file_name): + return WorkspaceReader.PreReadResult.accepted + # TODO: Find 3MFFileReader so we can load SceneNodes + # TODO: Ask user if it's okay for the scene to be cleared diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 42b1794160..a2af30211d 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -2,10 +2,11 @@ # Cura is released under the terms of the AGPLv3 or higher. from . import ThreeMFReader - +from . import ThreeMFWorkspaceReader from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + def getMetaData(): return { "plugin": { @@ -20,8 +21,17 @@ def getMetaData(): "extension": "3mf", "description": catalog.i18nc("@item:inlistbox", "3MF File") } + ], + "workspace_reader": + [ + { + "extension": "3mf", + "description": catalog.i18nc("@item:inlistbox", "3MF File") + } ] } + def register(app): - return { "mesh_reader": ThreeMFReader.ThreeMFReader() } + return {"mesh_reader": ThreeMFReader.ThreeMFReader(), + "workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()} From a859c9883c404cef61583b1d1dd383bad90a2116 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 15:33:31 +0100 Subject: [PATCH 02/50] Added loadWorkspace option to menu CURA-1263 --- resources/qml/Actions.qml | 7 +++++++ resources/qml/Cura.qml | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index e88b7e77ea..2719d09cbc 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -11,6 +11,7 @@ import Cura 1.0 as Cura Item { property alias open: openAction; + property alias loadWorkspace: loadWorkspaceAction; property alias quit: quitAction; property alias undo: undoAction; @@ -286,6 +287,12 @@ Item shortcut: StandardKey.Open; } + Action + { + id: loadWorkspaceAction + text: catalog.i18nc("@action:inmenu menubar:file","&Load Workspace..."); + } + Action { id: showEngineLogAction; diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 021178e6db..94e81190a6 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -67,9 +67,14 @@ UM.MainWindow id: fileMenu title: catalog.i18nc("@title:menu menubar:toplevel","&File"); - MenuItem { + MenuItem + { action: Cura.Actions.open; } + MenuItem + { + action: Cura.Actions.loadWorkspace + } RecentFilesMenu { } @@ -712,6 +717,38 @@ UM.MainWindow onTriggered: openDialog.open() } + FileDialog + { + id: openWorkspaceDialog; + + //: File open dialog title + title: catalog.i18nc("@title:window","Open workspace") + modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal; + selectMultiple: false + nameFilters: UM.WorkspaceFileHandler.supportedReadFileTypes; + folder: CuraApplication.getDefaultPath("dialog_load_path") + onAccepted: + { + //Because several implementations of the file dialog only update the folder + //when it is explicitly set. + var f = folder; + folder = f; + + CuraApplication.setDefaultPath("dialog_load_path", folder); + + for(var i in fileUrls) + { + UM.WorkspaceFileHandler.readLocalFile(fileUrls[i]) + } + } + } + + Connections + { + target: Cura.Actions.loadWorkspace + onTriggered:openWorkspaceDialog.open() + } + EngineLog { id: engineLog; From 164f378dd4cdd1e335a64c37f746ded91d287bda Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 17:26:01 +0100 Subject: [PATCH 03/50] Added supported Extensions to workspace reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c1c78037dd..3250706b97 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -5,6 +5,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): super().__init__() + self._supported_extensions = [".3mf"] def preRead(self, file_name): return WorkspaceReader.PreReadResult.accepted From e30038435cf24395ac38c730b61330006f456c7e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 8 Nov 2016 17:36:41 +0100 Subject: [PATCH 04/50] Added pre-read check for 3mf Reader CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3250706b97..c46b83bd1f 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -1,5 +1,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader +from UM.Application import Application +from UM.Logger import Logger ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -7,7 +9,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): super().__init__() self._supported_extensions = [".3mf"] + self._3mf_mesh_reader = None + def preRead(self, file_name): - return WorkspaceReader.PreReadResult.accepted - # TODO: Find 3MFFileReader so we can load SceneNodes + self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) + if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: + pass + else: + Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace") + return WorkspaceReader.PreReadResult.failed # TODO: Ask user if it's okay for the scene to be cleared + return WorkspaceReader.PreReadResult.accepted + + def read(self, file_name): + pass From 304696c8090a10b14b3f7e8160b5569d37432b9c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 9 Nov 2016 14:03:57 +0100 Subject: [PATCH 05/50] OutputDevices now take file_handler into account CURA-1263 --- cura/PrinterOutputDevice.py | 2 +- .../RemovableDriveOutputDevice.py | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index f90566c30b..efabeae641 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -49,7 +49,7 @@ class PrinterOutputDevice(QObject, OutputDevice): self._printer_state = "" self._printer_type = "unknown" - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): raise NotImplementedError("requestWrite needs to be implemented") ## Signals diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index 3fdd6b3e3e..b6505e7e6b 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -6,7 +6,7 @@ import os.path from UM.Application import Application from UM.Logger import Logger from UM.Message import Message -from UM.Mesh.WriteMeshJob import WriteMeshJob +from UM.FileHandler.WriteFileJob import WriteFileJob from UM.Mesh.MeshWriter import MeshWriter from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.OutputDevice.OutputDevice import OutputDevice @@ -37,13 +37,17 @@ class RemovableDriveOutputDevice(OutputDevice): # meshes. # \param limit_mimetypes Should we limit the available MIME types to the # MIME types available to the currently active machine? - def requestWrite(self, nodes, file_name = None, filter_by_machine = False): + def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None): filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() # Formats supported by this application (File types that we can actually write) - file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() + if file_handler: + file_formats = file_handler.getSupportedFileTypesWrite() + else: + file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() + if filter_by_machine: container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"}) @@ -58,7 +62,11 @@ class RemovableDriveOutputDevice(OutputDevice): raise OutputDeviceError.WriteRequestFailedError() # Just take the first file format available. - writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + if file_handler is not None: + writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"]) + else: + writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + extension = file_formats[0]["extension"] if file_name is None: @@ -72,7 +80,7 @@ class RemovableDriveOutputDevice(OutputDevice): Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") - job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) + job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) From f57a17577f6f950140c927febd0ddf42d1ab78bf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 9 Nov 2016 14:14:22 +0100 Subject: [PATCH 06/50] Added workspace save option to menu CURA-1263 --- resources/qml/Cura.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 94e81190a6..0c0eff2f66 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -107,6 +107,12 @@ UM.MainWindow onObjectRemoved: saveAllMenu.removeItem(object) } } + MenuItem + { + id: saveWorkspaceMenu + text: catalog.i18nc("@title:menu menubar:file","Save Workspace") + onTriggered: UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "file_type": "workspace" }); + } MenuItem { action: Cura.Actions.reloadAll; } From 54040d4c992aa2b1630b48ce111db93243fd0015 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 10:39:20 +0100 Subject: [PATCH 07/50] Moved 3mf writer here from Uranium CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 49 +++++ plugins/3MFWriter/ThreeMFWriter.py | 205 ++++++++++++++++++++ plugins/3MFWriter/__init__.py | 38 ++++ plugins/__init__.py | 0 4 files changed, 292 insertions(+) create mode 100644 plugins/3MFWriter/ThreeMFWorkspaceWriter.py create mode 100644 plugins/3MFWriter/ThreeMFWriter.py create mode 100644 plugins/3MFWriter/__init__.py create mode 100644 plugins/__init__.py diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py new file mode 100644 index 0000000000..cfc3e18eb1 --- /dev/null +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -0,0 +1,49 @@ +from UM.Workspace.WorkspaceWriter import WorkspaceWriter +from UM.Application import Application +from UM.Preferences import Preferences +import zipfile +from io import StringIO + + +class ThreeMFWorkspaceWriter(WorkspaceWriter): + def __init__(self): + super().__init__() + + def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter") + + if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace + return False + + # 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) + mesh_writer.write(stream, nodes, mode) + archive = mesh_writer.getArchive() + + # Add global container stack data to the archive. + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_stack_file = zipfile.ZipInfo("Cura/%s.stack.cfg" % global_container_stack.getId()) + global_stack_file.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_stack_file, global_container_stack.serialize()) + + # Write user changes to the archive. + global_user_instance_container = global_container_stack.getTop() + global_user_instance_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_user_instance_container.getId()) + global_user_instance_container.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_user_instance_file, global_user_instance_container.serialize()) + + # Write quality changes to the archive. + global_quality_changes = global_container_stack.findContainer({"type": "quality_changes"}) + global_quality_changes_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_quality_changes.getId()) + global_quality_changes.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(global_quality_changes_file, global_quality_changes.serialize()) + + # Write preferences to archive + preferences_file = zipfile.ZipInfo("Cura/preferences.cfg") + preferences_string = StringIO() + Preferences.getInstance().writeToFile(preferences_string) + archive.writestr(preferences_file, preferences_string.getvalue()) + # Close the archive & reset states. + archive.close() + mesh_writer.setStoreArchive(False) + return True diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py new file mode 100644 index 0000000000..acf1421655 --- /dev/null +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -0,0 +1,205 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +from UM.Mesh.MeshWriter import MeshWriter +from UM.Math.Vector import Vector +from UM.Logger import Logger +from UM.Math.Matrix import Matrix +from UM.Settings.SettingRelation import RelationType + +try: + import xml.etree.cElementTree as ET +except ImportError: + Logger.log("w", "Unable to load cElementTree, switching to slower version") + import xml.etree.ElementTree as ET + +import zipfile +import UM.Application + + +class ThreeMFWriter(MeshWriter): + def __init__(self): + super().__init__() + self._namespaces = { + "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", + "content-types": "http://schemas.openxmlformats.org/package/2006/content-types", + "relationships": "http://schemas.openxmlformats.org/package/2006/relationships", + "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10" + } + + self._unit_matrix_string = self._convertMatrixToString(Matrix()) + self._archive = None + self._store_archive = False + + def _convertMatrixToString(self, matrix): + result = "" + result += str(matrix._data[0,0]) + " " + result += str(matrix._data[1,0]) + " " + result += str(matrix._data[2,0]) + " " + result += str(matrix._data[0,1]) + " " + result += str(matrix._data[1,1]) + " " + result += str(matrix._data[2,1]) + " " + result += str(matrix._data[0,2]) + " " + result += str(matrix._data[1,2]) + " " + result += str(matrix._data[2,2]) + " " + result += str(matrix._data[0,3]) + " " + result += str(matrix._data[1,3]) + " " + result += str(matrix._data[2,3]) + " " + return result + + ## Should we store the archive + # Note that if this is true, the archive will not be closed. + # The object that set this parameter is then responsible for closing it correctly! + def setStoreArchive(self, store_archive): + self._store_archive = store_archive + + def getArchive(self): + return self._archive + + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): + try: + MeshWriter._meshNodes(nodes).__next__() + except StopIteration: + return False #Don't write anything if there is no mesh data. + self._archive = None # Reset archive + archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) + try: + model_file = zipfile.ZipInfo("3D/3dmodel.model") + # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. + model_file.compress_type = zipfile.ZIP_DEFLATED + + # Create content types file + content_types_file = zipfile.ZipInfo("[Content_Types].xml") + content_types_file.compress_type = zipfile.ZIP_DEFLATED + content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) + rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") + model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") + + # Create _rels/.rels file + relations_file = zipfile.ZipInfo("_rels/.rels") + relations_file.compress_type = zipfile.ZIP_DEFLATED + relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) + model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") + + model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) + resources = ET.SubElement(model, "resources") + build = ET.SubElement(model, "build") + + added_nodes = [] + + # Write all nodes with meshData to the file as objects inside the resource tag + for index, n in enumerate(MeshWriter._meshNodes(nodes)): + added_nodes.append(n) # Save the nodes that have mesh data + object = ET.SubElement(resources, "object", id = str(index+1), type = "model") + mesh = ET.SubElement(object, "mesh") + + mesh_data = n.getMeshData() + vertices = ET.SubElement(mesh, "vertices") + verts = mesh_data.getVertices() + + if verts is None: + Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.") + continue # No mesh data, nothing to do. + if mesh_data.hasIndices(): + for face in mesh_data.getIndices(): + v1 = verts[face[0]] + v2 = verts[face[1]] + v3 = verts[face[2]] + xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2])) + xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2])) + xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2])) + + triangles = ET.SubElement(mesh, "triangles") + for face in mesh_data.getIndices(): + triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2])) + else: + triangles = ET.SubElement(mesh, "triangles") + for idx, vert in enumerate(verts): + xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2])) + + # If we have no faces defined, assume that every three subsequent vertices form a face. + if idx % 3 == 0: + triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2)) + + # Handle per object settings + stack = n.callDecoration("getStack") + if stack is not None: + changed_setting_keys = set(stack.getTop().getAllKeys()) + + # Ensure that we save the extruder used for this object. + if stack.getProperty("machine_extruder_count", "value") > 1: + changed_setting_keys.add("extruder_nr") + + settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"]) + + # Get values for all changed settings & save them. + for key in changed_setting_keys: + setting_xml = ET.SubElement(settings_xml, "setting", key = key) + setting_xml.text = str(stack.getProperty(key, "value")) + + # Add one to the index as we haven't incremented the last iteration. + index += 1 + nodes_to_add = set() + + for node in added_nodes: + # Check the parents of the nodes with mesh_data and ensure that they are also added. + parent_node = node.getParent() + while parent_node is not None: + if parent_node.callDecoration("isGroup"): + nodes_to_add.add(parent_node) + parent_node = parent_node.getParent() + else: + parent_node = None + + # Sort all the nodes by depth (so nodes with the highest depth are done first) + sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True) + + # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene + for node in sorted_nodes_to_add: + object = ET.SubElement(resources, "object", id=str(index + 1), type="model") + components = ET.SubElement(object, "components") + for child in node.getChildren(): + if child in added_nodes: + component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation())) + index += 1 + added_nodes.append(node) + + # Create a transformation Matrix to convert from our worldspace into 3MF. + # First step: flip the y and z axis. + transformation_matrix = Matrix() + transformation_matrix._data[1, 1] = 0 + transformation_matrix._data[1, 2] = -1 + transformation_matrix._data[2, 1] = 1 + transformation_matrix._data[2, 2] = 0 + + global_container_stack = UM.Application.getInstance().getGlobalContainerStack() + # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the + # build volume. + if global_container_stack: + translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, + y=global_container_stack.getProperty("machine_depth", "value") / 2, + z=0) + translation_matrix = Matrix() + translation_matrix.setByTranslation(translation_vector) + transformation_matrix.preMultiply(translation_matrix) + + # Find out what the final build items are and add them. + for node in added_nodes: + if node.getParent().callDecoration("isGroup") is None: + node_matrix = node.getLocalTransformation() + + ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix))) + + archive.writestr(model_file, b' \n' + ET.tostring(model)) + archive.writestr(content_types_file, b' \n' + ET.tostring(content_types)) + archive.writestr(relations_file, b' \n' + ET.tostring(relations_element)) + except Exception as e: + Logger.logException("e", "Error writing zip file") + return False + finally: + if not self._store_archive: + archive.close() + else: + self._archive = archive + + return True diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py new file mode 100644 index 0000000000..1dbc0bf281 --- /dev/null +++ b/plugins/3MFWriter/__init__.py @@ -0,0 +1,38 @@ +# Copyright (c) 2015 Ultimaker B.V. +# Uranium is released under the terms of the AGPLv3 or higher. + +from UM.i18n import i18nCatalog +from . import ThreeMFWorkspaceWriter +from . import ThreeMFWriter + +i18n_catalog = i18nCatalog("uranium") + +def getMetaData(): + return { + "plugin": { + "name": i18n_catalog.i18nc("@label", "3MF Writer"), + "author": "Ultimaker", + "version": "1.0", + "description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."), + "api": 3 + }, + "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 + }] + }, + "workspace_writer": { + "output": [{ + "extension": "3mf", + "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), + "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode + }] + } + } + +def register(app): + return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()} diff --git a/plugins/__init__.py b/plugins/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 61d1199abfb97b0187eeee02d5f0ae52524f05ad Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:27:25 +0100 Subject: [PATCH 08/50] The entire machine is now saved to 3mf file when saving workspace CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 50 +++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index cfc3e18eb1..1b00451e92 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -1,6 +1,8 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.Application import Application from UM.Preferences import Preferences +from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.Settings.ExtruderManager import ExtruderManager import zipfile from io import StringIO @@ -20,30 +22,50 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): mesh_writer.write(stream, nodes, mode) archive = mesh_writer.getArchive() - # Add global container stack data to the archive. global_container_stack = Application.getInstance().getGlobalContainerStack() - global_stack_file = zipfile.ZipInfo("Cura/%s.stack.cfg" % global_container_stack.getId()) - global_stack_file.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_stack_file, global_container_stack.serialize()) - # Write user changes to the archive. - global_user_instance_container = global_container_stack.getTop() - global_user_instance_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_user_instance_container.getId()) - global_user_instance_container.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_user_instance_file, global_user_instance_container.serialize()) + # Add global container stack data to the archive. + self._writeContainerToArchive(global_container_stack, archive) - # Write quality changes to the archive. - global_quality_changes = global_container_stack.findContainer({"type": "quality_changes"}) - global_quality_changes_file = zipfile.ZipInfo("Cura/%s.inst.cfg" % global_quality_changes.getId()) - global_quality_changes.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(global_quality_changes_file, global_quality_changes.serialize()) + # Also write all containers in the stack to the file + for container in global_container_stack.getContainers(): + self._writeContainerToArchive(container, archive) + + # Check if the machine has extruders and save all that data as well. + for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()): + self._writeContainerToArchive(extruder_stack, archive) + for container in extruder_stack.getContainers(): + self._writeContainerToArchive(container, archive) # Write preferences to archive preferences_file = zipfile.ZipInfo("Cura/preferences.cfg") preferences_string = StringIO() Preferences.getInstance().writeToFile(preferences_string) archive.writestr(preferences_file, preferences_string.getvalue()) + # Close the archive & reset states. archive.close() mesh_writer.setStoreArchive(False) return True + + @staticmethod + def _writeContainerToArchive(container, archive): + if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): + return # Empty file, do nothing. + + file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] + + # Some containers have a base file, which should then be the file to use. + base_file = container.getMetaDataEntry("base_file", None) + if base_file: + container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] + + file_name = "Cura/%s.%s" % (container.getId(), file_suffix) + + if file_name in archive.namelist(): + return # File was already saved, no need to do it again. + + file_in_archive = zipfile.ZipInfo(file_name) + file_in_archive.compress_type = zipfile.ZIP_DEFLATED + + archive.writestr(file_in_archive, container.serialize()) From 3ab283bfed657280f954d46393ed785ecfc34d75 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:29:47 +0100 Subject: [PATCH 09/50] Saving workspace now works when there are no meshes to save CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 1b00451e92..437b9188ae 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -20,7 +20,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # 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) mesh_writer.write(stream, nodes, mode) + 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) global_container_stack = Application.getInstance().getGlobalContainerStack() From d477630ccec0699a69f3b60d1a49610900c19001 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:34:12 +0100 Subject: [PATCH 10/50] Updated documentation CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 437b9188ae..afa3f53bf0 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -51,6 +51,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): mesh_writer.setStoreArchive(False) return True + ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. + # \param container That follows the \type{ContainerInterface} to archive. + # \param archive The archive to write to. @staticmethod def _writeContainerToArchive(container, archive): if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): @@ -66,9 +69,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): file_name = "Cura/%s.%s" % (container.getId(), file_suffix) if file_name in archive.namelist(): - return # File was already saved, no need to do it again. + return # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold. file_in_archive = zipfile.ZipInfo(file_name) + # For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive) file_in_archive.compress_type = zipfile.ZIP_DEFLATED archive.writestr(file_in_archive, container.serialize()) From 1f21957cb486e9dcaf0ba1abddf88ff7b4965dc3 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 11:46:13 +0100 Subject: [PATCH 11/50] Fixed issue with per-object settings in 3mf reader CURA-1263 and CURA-382 --- plugins/3MFReader/ThreeMFReader.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 2aa719018d..3fee1adcb8 100644 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -84,20 +84,20 @@ class ThreeMFReader(MeshReader): definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom()) node.callDecoration("getStack").getTop().setDefinition(definition) - setting_container = node.callDecoration("getStack").getTop() - for setting in xml_settings: - setting_key = setting.get("key") - setting_value = setting.text + setting_container = node.callDecoration("getStack").getTop() + for setting in xml_settings: + setting_key = setting.get("key") + setting_value = setting.text - # Extruder_nr is a special case. - if setting_key == "extruder_nr": - extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) - if extruder_stack: - node.callDecoration("setActiveExtruder", extruder_stack.getId()) - else: - Logger.log("w", "Unable to find extruder in position %s", setting_value) - continue - setting_container.setProperty(setting_key,"value", setting_value) + # Extruder_nr is a special case. + if setting_key == "extruder_nr": + extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value)) + if extruder_stack: + node.callDecoration("setActiveExtruder", extruder_stack.getId()) + else: + Logger.log("w", "Unable to find extruder in position %s", setting_value) + continue + setting_container.setProperty(setting_key,"value", setting_value) if len(node.getChildren()) > 0: group_decorator = GroupDecorator() From 611572c324e3a1a065e49b5e71692e9987a241e6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 14:11:33 +0100 Subject: [PATCH 12/50] Extruder stack is now saved (instead of the material being saved as the stack) CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index afa3f53bf0..b6c9d884af 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -62,8 +62,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] # Some containers have a base file, which should then be the file to use. - base_file = container.getMetaDataEntry("base_file", None) - if base_file: + if "base_file" in container.getMetaData(): + base_file = container.getMetaDataEntry("base_file") container = ContainerRegistry.getInstance().findContainers(id = base_file)[0] file_name = "Cura/%s.%s" % (container.getId(), file_suffix) From b92ca508bbe521f1fb1383361c3bbb6f140e6c97 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 10 Nov 2016 15:42:20 +0100 Subject: [PATCH 13/50] Stacks are now loaded from workspace file CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 36 ++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c46b83bd1f..9046c5e6f3 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -2,6 +2,9 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Application import Application from UM.Logger import Logger +from UM.Settings.ContainerStack import ContainerStack + +import zipfile ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -22,4 +25,35 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted def read(self, file_name): - pass + # Load all the nodes / meshdata of the workspace + nodes = self._3mf_mesh_reader.read(file_name) + if nodes is None: + nodes = [] + + + archive = zipfile.ZipFile(file_name, "r") + + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + + # Get the stack(s) saved in the workspace. + container_stack_files = [name for name in cura_file_names if name.endswith(".stack.cfg")] + global_stack = None + extruder_stacks = [] + for container_stack_file in container_stack_files: + container_id = container_stack_file.replace("Cura/", "") + container_id = container_id.replace(".stack.cfg", "") + stack = ContainerStack(container_id) + + # Serialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + + if stack.getMetaDataEntry("type") == "extruder_train": + extruder_stacks.append(stack) + else: + global_stack = stack + + # Check if the right machine type is active now + #Application.getInstance().getGlobalContainerStack().getBottom().getId() == + + + return nodes From 4a2f07c3632a119e35100c6c3df5c0b62ad3f6a2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 10:39:54 +0100 Subject: [PATCH 14/50] Definitions & materials are now loaded from workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 82 ++++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 9046c5e6f3..c18e823658 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -3,9 +3,13 @@ from UM.Application import Application from UM.Logger import Logger from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.DefinitionContainer import DefinitionContainer +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.ContainerRegistry import ContainerRegistry import zipfile + ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): @@ -30,30 +34,88 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if nodes is None: nodes = [] - + container_registry = ContainerRegistry.getInstance() archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few + # TODO: cases that the container loaded is the same (most notable in materials & definitions). + # TODO: It might be possible that we need to add smarter checking in the future. + + # Get all the definition files & check if they exist. If not, add them. + definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + definition_container_files = [name for name in cura_file_names if name.endswith(definition_container_suffix)] + for definition_container_file in definition_container_files: + container_id = definition_container_file.replace("Cura/", "") + container_id = container_id.replace(".%s" % definition_container_suffix, "") + definitions = container_registry.findDefinitionContainers(id=container_id) + if not definitions: + definition_container = DefinitionContainer(container_id) + definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) + container_registry.addContainer(definition_container) + + # Get all the material files and check if they exist. If not, add them. + xml_material_profile = None + for type_name, container_type in container_registry.getContainerTypes(): + if type_name == "XmlMaterialProfile": + xml_material_profile = container_type + break + + if xml_material_profile: + material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + material_container_files = [name for name in cura_file_names if name.endswith(material_container_suffix)] + for material_container_file in material_container_files: + container_id = material_container_file.replace("Cura/", "") + container_id = container_id.replace(".%s" % material_container_suffix, "") + materials = container_registry.findInstanceContainers(id=container_id) + if not materials: + material_container = xml_material_profile(container_id) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + container_registry.addContainer(material_container) + + # Get quality_changes and user profiles saved in the workspace + instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] + instance_container_files = [name for name in cura_file_names if name.endswith(instance_container_suffix)] + user_instance_containers = [] + quality_changes_instance_containers = [] + for instance_container_file in instance_container_files: + container_id = instance_container_file.replace("Cura/", "") + container_id = instance_container_file.replace(".%s" % instance_container_suffix, "") + instance_container = InstanceContainer(container_id) + + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "user": + user_instance_containers.append(instance_container) + elif container_type == "quality_changes": + quality_changes_instance_containers.append(instance_container) + else: + continue + # Get the stack(s) saved in the workspace. - container_stack_files = [name for name in cura_file_names if name.endswith(".stack.cfg")] + '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] global_stack = None extruder_stacks = [] for container_stack_file in container_stack_files: container_id = container_stack_file.replace("Cura/", "") - container_id = container_id.replace(".stack.cfg", "") - stack = ContainerStack(container_id) + container_id = container_id.replace(".%s" % container_stack_suffix, "") - # Serialize stack by converting read data from bytes to string + # Check if a stack by this ID already exists; + container_stacks = container_registry.findContainerStacks(id = container_id) + if container_stacks: + print("CONTAINER ALREADY EXISTSSS") + + #stack = ContainerStack(container_id) + + # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: - global_stack = stack - - # Check if the right machine type is active now - #Application.getInstance().getGlobalContainerStack().getBottom().getId() == - + global_stack = stack''' return nodes From 413d788c0ca5c4161736464cbc347ff47bd6a4a0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 10:46:54 +0100 Subject: [PATCH 15/50] Fixed copypaste mistake CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c18e823658..500a5cdc7c 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -81,7 +81,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_instance_containers = [] for instance_container_file in instance_container_files: container_id = instance_container_file.replace("Cura/", "") - container_id = instance_container_file.replace(".%s" % instance_container_suffix, "") + container_id = container_id.replace(".%s" % instance_container_suffix, "") instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string From 99e753d3be061537e24cd8ea48bf881f3c49c39c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 13:31:36 +0100 Subject: [PATCH 16/50] Added loading of preferences form workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 500a5cdc7c..6f31115c24 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -7,8 +7,10 @@ from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry -import zipfile +from UM.Preferences import Preferences +import zipfile +import io ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): @@ -39,6 +41,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its + # parsing code. + temp_preferences = Preferences() + temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks. + + # Copy a number of settings from the temp preferences to the global + global_preferences = Preferences.getInstance() + global_preferences.setValue("general/visible_settings", temp_preferences.getValue("general/visible_settings")) + global_preferences.setValue("cura/categories_expanded", temp_preferences.getValue("cura/categories_expanded")) + Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. @@ -90,10 +103,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if container_type == "user": user_instance_containers.append(instance_container) elif container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = container_registry.findInstanceContainers(id = container_id) + if not quality_changes: + container_registry.addContainer(instance_container) quality_changes_instance_containers.append(instance_container) else: continue + # Get the stack(s) saved in the workspace. '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] From cbcc48ff3342f63d9f2e175e3227923adf095114 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 11 Nov 2016 17:17:23 +0100 Subject: [PATCH 17/50] Pre-read now checks for conflicts and asks the user what strategy for resolvement to use CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 131 ++++++++++++++------ plugins/3MFReader/WorkspaceDialog.py | 79 ++++++++++++ plugins/3MFReader/WorkspaceDialog.qml | 52 ++++++++ 3 files changed, 221 insertions(+), 41 deletions(-) create mode 100644 plugins/3MFReader/WorkspaceDialog.py create mode 100644 plugins/3MFReader/WorkspaceDialog.qml diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 6f31115c24..e4a2c574ac 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -2,23 +2,34 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Application import Application from UM.Logger import Logger +from UM.i18n import i18nCatalog from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Preferences import Preferences - +from .WorkspaceDialog import WorkspaceDialog import zipfile import io +i18n_catalog = i18nCatalog("cura") + + ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): super().__init__() self._supported_extensions = [".3mf"] - + self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None + self._container_registry = ContainerRegistry.getInstance() + self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it + self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] + self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + + self._resolvement_strategy = None def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -27,7 +38,45 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace") return WorkspaceReader.PreReadResult.failed - # TODO: Ask user if it's okay for the scene to be cleared + + # Check if there are any conflicts, so we can ask the user. + archive = zipfile.ZipFile(file_name, "r") + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] + + conflict = False + for container_stack_file in container_stack_files: + container_id = self._stripFileToId(container_stack_file) + stacks = self._container_registry.findContainerStacks(id=container_id) + if stacks: + conflict = True + break + + # Check if any quality_changes instance container is in conflict. + if not conflict: + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] + for instance_container_file in instance_container_files: + container_id = self._stripFileToId(instance_container_file) + instance_container = InstanceContainer(container_id) + + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = self._container_registry.findInstanceContainers(id = container_id) + if quality_changes: + conflict = True + if conflict: + # There is a conflict; User should choose to either update the existing data, add everything as new data or abort + self._resolvement_strategy = None + self._dialog.show() + self._dialog.waitForClose() + if self._dialog.getResult() == "cancel": + return WorkspaceReader.PreReadResult.cancelled + + self._resolvement_strategy = self._dialog.getResult() + pass return WorkspaceReader.PreReadResult.accepted def read(self, file_name): @@ -36,7 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if nodes is None: nodes = [] - container_registry = ContainerRegistry.getInstance() archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] @@ -57,83 +105,84 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # TODO: It might be possible that we need to add smarter checking in the future. # Get all the definition files & check if they exist. If not, add them. - definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] - definition_container_files = [name for name in cura_file_names if name.endswith(definition_container_suffix)] + 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 = definition_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % definition_container_suffix, "") - definitions = container_registry.findDefinitionContainers(id=container_id) + container_id = self._stripFileToId(definition_container_file) + definitions = self._container_registry.findDefinitionContainers(id=container_id) if not definitions: definition_container = DefinitionContainer(container_id) definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) - container_registry.addContainer(definition_container) + self._container_registry.addContainer(definition_container) # Get all the material files and check if they exist. If not, add them. - xml_material_profile = None - for type_name, container_type in container_registry.getContainerTypes(): - if type_name == "XmlMaterialProfile": - xml_material_profile = container_type - break - + 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_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] - material_container_files = [name for name in cura_file_names if name.endswith(material_container_suffix)] + 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: - container_id = material_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % material_container_suffix, "") - materials = container_registry.findInstanceContainers(id=container_id) + container_id = self._stripFileToId(material_container_file) + materials = self._container_registry.findInstanceContainers(id=container_id) if not materials: material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - container_registry.addContainer(material_container) + self._container_registry.addContainer(material_container) # Get quality_changes and user profiles saved in the workspace - instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] - instance_container_files = [name for name in cura_file_names if name.endswith(instance_container_suffix)] + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] quality_changes_instance_containers = [] for instance_container_file in instance_container_files: - container_id = instance_container_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % instance_container_suffix, "") + container_id = self._stripFileToId(instance_container_file) instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) container_type = instance_container.getMetaDataEntry("type") if container_type == "user": + # Check if quality changes already exists. + user_containers = self._container_registry.findInstanceContainers(id=container_id) + if not user_containers: + self._container_registry.addContainer(instance_container) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. - quality_changes = container_registry.findInstanceContainers(id = container_id) + quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: - container_registry.addContainer(instance_container) + self._container_registry.addContainer(instance_container) quality_changes_instance_containers.append(instance_container) else: continue - # Get the stack(s) saved in the workspace. - '''container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - container_stack_files = [name for name in cura_file_names if name.endswith(container_stack_suffix)] + container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] + for container_stack_file in container_stack_files: - container_id = container_stack_file.replace("Cura/", "") - container_id = container_id.replace(".%s" % container_stack_suffix, "") - - # Check if a stack by this ID already exists; - container_stacks = container_registry.findContainerStacks(id = container_id) - if container_stacks: - print("CONTAINER ALREADY EXISTSSS") - - #stack = ContainerStack(container_id) + container_id = self._stripFileToId(container_stack_file) + stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Check if a stack by this ID already exists; + container_stacks = self._container_registry.findContainerStacks(id = container_id) + if container_stacks: + print("CONTAINER ALREADY EXISTSSS") + if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: - global_stack = stack''' + global_stack = stack return nodes + + def _stripFileToId(self, file): + return file.replace("Cura/", "").split(".")[0] + + def _getXmlProfileClass(self): + for type_name, container_type in self._container_registry.getContainerTypes(): + print(type_name, container_type) + if type_name == "XmlMaterialProfile": + return container_type diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py new file mode 100644 index 0000000000..ae1280b4bd --- /dev/null +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -0,0 +1,79 @@ +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtQml import QQmlComponent, QQmlContext +from UM.PluginRegistry import PluginRegistry +from UM.Application import Application + +import os +import threading + +class WorkspaceDialog(QObject): + showDialogSignal = pyqtSignal() + + def __init__(self, parent = None): + super().__init__(parent) + self._component = None + self._context = None + self._view = None + self._qml_url = "WorkspaceDialog.qml" + self._lock = threading.Lock() + self._result = None # What option did the user pick? + self._visible = False + self.showDialogSignal.connect(self.__show) + + def getResult(self): + return self._result + + def _createViewFromQML(self): + path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)) + self._component = QQmlComponent(Application.getInstance()._engine, path) + self._context = QQmlContext(Application.getInstance()._engine.rootContext()) + self._context.setContextProperty("manager", self) + self._view = self._component.create(self._context) + + def show(self): + # Emit signal so the right thread actually shows the view. + self._lock.acquire() + self._result = None + self._visible = True + self.showDialogSignal.emit() + + @pyqtSlot() + ## Used to notify the dialog so the lock can be released. + def notifyClosed(self): + if self._result is None: + self._result = "cancel" + self._lock.release() + + def hide(self): + self._visible = False + self._lock.release() + self._view.hide() + + @pyqtSlot() + def onOverrideButtonClicked(self): + self._view.hide() + self.hide() + self._result = "override" + + @pyqtSlot() + def onNewButtonClicked(self): + self._view.hide() + self.hide() + self._result = "new" + + @pyqtSlot() + def onCancelButtonClicked(self): + self._view.hide() + self.hide() + self._result = "cancel" + + ## Block thread until the dialog is closed. + def waitForClose(self): + if self._visible: + self._lock.acquire() + self._lock.release() + + def __show(self): + if self._view is None: + self._createViewFromQML() + self._view.show() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml new file mode 100644 index 0000000000..0c56dbcb6c --- /dev/null +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -0,0 +1,52 @@ +// Copyright (c) 2016 Ultimaker B.V. +// Cura is released under the terms of the AGPLv3 or higher. + +import QtQuick 2.1 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Window 2.1 + +import UM 1.1 as UM + +UM.Dialog +{ + title: catalog.i18nc("@title:window", "Conflict") + + width: 350 * Screen.devicePixelRatio; + minimumWidth: 350 * Screen.devicePixelRatio; + maximumWidth: 350 * Screen.devicePixelRatio; + + height: 250 * Screen.devicePixelRatio; + minimumHeight: 250 * Screen.devicePixelRatio; + maximumHeight: 250 * Screen.devicePixelRatio; + + onClosing: manager.notifyClosed() + + Item + { + UM.I18nCatalog { id: catalog; name: "cura"; } + } + rightButtons: [ + Button + { + id: override_button + text: catalog.i18nc("@action:button","Override"); + onClicked: { manager.onOverrideButtonClicked() } + enabled: true + }, + Button + { + id: create_new + text: catalog.i18nc("@action:button","Create new"); + onClicked: { manager.onNewButtonClicked() } + enabled: true + }, + Button + { + id: cancel_button + text: catalog.i18nc("@action:button","Cancel"); + onClicked: { manager.onCancelButtonClicked() } + enabled: true + } + ] +} \ No newline at end of file From d5229992df425f4ed9cfd13ef2beefdf6db78f8a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 12:37:26 +0100 Subject: [PATCH 18/50] Override resolve strategy is now handled CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 24 ++++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index e4a2c574ac..89260193b2 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -29,7 +29,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - self._resolvement_strategy = None + self._resolve_strategy = None def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -69,13 +69,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): conflict = True if conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolvement_strategy = None + self._resolve_strategy = None self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled - self._resolvement_strategy = self._dialog.getResult() + self._resolve_strategy = self._dialog.getResult() pass return WorkspaceReader.PreReadResult.accepted @@ -144,12 +144,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader): user_containers = self._container_registry.findInstanceContainers(id=container_id) if not user_containers: self._container_registry.addContainer(instance_container) + else: + if self._resolve_strategy == "override": + user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: self._container_registry.addContainer(instance_container) + else: + if self._resolve_strategy == "override": + quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -163,13 +169,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(container_stack_file) stack = ContainerStack(container_id) - # Deserialize stack by converting read data from bytes to string - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - # Check if a stack by this ID already exists; - container_stacks = self._container_registry.findContainerStacks(id = container_id) + container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: - print("CONTAINER ALREADY EXISTSSS") + if self._resolve_strategy == "override": + container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + else: + # Deserialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) From 8ae0cfd8488f1d6e1be439ed15e4d244825d2316 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 13:30:28 +0100 Subject: [PATCH 19/50] Loading workspace now activates the machine and notifies everyone that it was changed CURA-1263 --- cura/Settings/MachineManager.py | 2 +- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index fce82212cd..ed9066c4ba 100644 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -277,7 +277,7 @@ class MachineManager(QObject): def _onInstanceContainersChanged(self, container): container_type = container.getMetaDataEntry("type") - + if container_type == "material": self.activeMaterialChanged.emit() elif container_type == "variant": diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 89260193b2..8930e42e93 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -147,6 +147,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategy == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + else: + # TODO: Handle other resolve strategies + pass user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. @@ -168,22 +171,37 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) - stack = ContainerStack(container_id) + # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: + stack = container_stacks[0] if self._resolve_strategy == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + else: + # TODO: Handle other resolve strategies + pass else: + stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: global_stack = stack + # Notify everything/one that is to notify about changes. + for container in global_stack.getContainers(): + global_stack.containersChanged.emit(container) + for stack in extruder_stacks: + for container in stack.getContainers(): + stack.containersChanged.emit(container) + + # Actually change the active machine. + Application.getInstance().setGlobalContainerStack(global_stack) return nodes def _stripFileToId(self, file): From 377752397f5fc14ab082358939eafd4c8af203c6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 13:48:49 +0100 Subject: [PATCH 20/50] Made it possible for machine & quality changes to have different resolve strategies CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 8930e42e93..819fe1586b 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -29,7 +29,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] - self._resolve_strategy = None + self._resolve_strategies = {} def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -69,13 +69,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): conflict = True if conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolve_strategy = None + self._resolve_strategies = {} self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled - - self._resolve_strategy = self._dialog.getResult() + result = self._dialog.getResult() + # TODO: In the future it could be that machine & quality changes will have different resolve strategies + self._resolve_strategies = {"machine": result, "quality_changes": result} pass return WorkspaceReader.PreReadResult.accepted @@ -145,9 +146,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not user_containers: self._container_registry.addContainer(instance_container) else: - if self._resolve_strategy == "override": + if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) else: + user_containers.deserialize(archive.open(instance_container_file).read().decode("utf-8")) # TODO: Handle other resolve strategies pass user_instance_containers.append(instance_container) @@ -157,8 +159,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not quality_changes: self._container_registry.addContainer(instance_container) else: - if self._resolve_strategy == "override": + if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + else: + quality_changes.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -171,12 +175,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) - # Check if a stack by this ID already exists; container_stacks = self._container_registry.findContainerStacks(id=container_id) if container_stacks: stack = container_stacks[0] - if self._resolve_strategy == "override": + if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: # TODO: Handle other resolve strategies @@ -185,13 +188,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - self._container_registry.addContainer(stack) if stack.getMetaDataEntry("type") == "extruder_train": extruder_stacks.append(stack) else: global_stack = stack + # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) From 990736b5c62208843a3f54a856239badb28979c9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 14:51:10 +0100 Subject: [PATCH 21/50] Implemented quality_changes resolve strategy This enables the creation of a new quality_changes profile if the user chose to do this CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 819fe1586b..57a81a24f0 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -162,7 +162,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) else: - quality_changes.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -195,6 +195,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: global_stack = stack + if self._resolve_strategies["quality_changes"] == "new": + # Quality changes needs to get a new ID, added to registry and to the right stacks + for container in quality_changes_instance_containers: + old_id = container.getId() + container.setName(self._container_registry.uniqueName(container.getName())) + # We're not really supposed to change the ID in normal cases, but this is an exception. + container._id = self._container_registry.uniqueName(container.getId()) + + # The container was not added yet, as it didn't have an unique ID. It does now, so add it. + self._container_registry.addContainer(container) + + # Replace the quality changes container + old_container = global_stack.findContainer({"type": "quality_changes"}) + if old_container.getId() == old_id: + quality_changes_index = global_stack.getContainerIndex(old_container) + global_stack.replaceContainer(quality_changes_index, container) + + for stack in extruder_stacks: + old_container = stack.findContainer({"type": "quality_changes"}) + if old_container.getId() == old_id: + quality_changes_index = stack.getContainerIndex(old_container) + stack.replaceContainer(quality_changes_index, container) + # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) From 0d4f28b310c1eeda0912db6777608d11647f0bce Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:16:17 +0100 Subject: [PATCH 22/50] MachineStacks & user containers are now also renamed if so required CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 29 +++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 57a81a24f0..091528a4eb 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -148,8 +148,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) - else: - user_containers.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + elif self._resolve_strategies["machine"] == "new": + # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. + extruder_id = instance_container.getMetaDataEntry("extruder", None) + if extruder_id: + new_id = self._container_registry.uniqueName(extruder_id) + "_current_settings" + instance_container._id = new_id + instance_container.setName(new_id) + self._container_registry.addContainer(instance_container) + continue + + machine_id = instance_container.getMetaDataEntry("machine", None) + if machine_id: + new_id = self._container_registry.uniqueName(machine_id) + "_current_settings" + instance_container._id = new_id + instance_container.setName(new_id) + self._container_registry.addContainer(instance_container) # TODO: Handle other resolve strategies pass user_instance_containers.append(instance_container) @@ -182,8 +196,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: - # TODO: Handle other resolve strategies - pass + new_id = self._container_registry.uniqueName(container_id) + stack = ContainerStack(new_id) + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Ensure a unique ID and name + stack._id = new_id + stack.setName(self._container_registry.uniqueName(stack.getName())) + self._container_registry.addContainer(stack) else: stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string @@ -211,6 +230,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if old_container.getId() == old_id: quality_changes_index = global_stack.getContainerIndex(old_container) global_stack.replaceContainer(quality_changes_index, container) + continue for stack in extruder_stacks: old_container = stack.findContainer({"type": "quality_changes"}) @@ -235,6 +255,5 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _getXmlProfileClass(self): for type_name, container_type in self._container_registry.getContainerTypes(): - print(type_name, container_type) if type_name == "XmlMaterialProfile": return container_type From 4dc14a72ab557694e18b9b2c2709d9152fabbf6c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:32:08 +0100 Subject: [PATCH 23/50] Added conversion table for old to new ID's CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 27 +++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 091528a4eb..0933c1be50 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -31,6 +31,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {} + self._id_mapping = {} + + ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. + # This has nothing to do with speed, but with getting consistent new naming for instances & objects. + def getNewId(self, old_id): + if old_id not in self._id_mapping: + self._id_mapping[old_id] = self._container_registry.uniqueName(old_id) + return self._id_mapping[old_id] + def preRead(self, file_name): self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: @@ -101,10 +110,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): global_preferences.setValue("cura/categories_expanded", temp_preferences.getValue("cura/categories_expanded")) Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change + self._id_mapping = {} + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. - + 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: @@ -115,6 +126,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) self._container_registry.addContainer(definition_container) + 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: @@ -129,6 +141,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) + Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] @@ -152,7 +165,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. extruder_id = instance_container.getMetaDataEntry("extruder", None) if extruder_id: - new_id = self._container_registry.uniqueName(extruder_id) + "_current_settings" + new_id = self.getNewId(extruder_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) self._container_registry.addContainer(instance_container) @@ -160,7 +173,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: - new_id = self._container_registry.uniqueName(machine_id) + "_current_settings" + new_id = self.getNewId(machine_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) self._container_registry.addContainer(instance_container) @@ -182,10 +195,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): continue # Get the stack(s) saved in the workspace. + Logger.log("d", "Workspace loading is checking stacks containers...") container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] - for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) @@ -196,7 +209,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) else: - new_id = self._container_registry.uniqueName(container_id) + new_id = self.getNewId(container_id) stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) # Ensure a unique ID and name @@ -220,7 +233,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): old_id = container.getId() container.setName(self._container_registry.uniqueName(container.getName())) # We're not really supposed to change the ID in normal cases, but this is an exception. - container._id = self._container_registry.uniqueName(container.getId()) + container._id = self.getNewId(container.getId()) # The container was not added yet, as it didn't have an unique ID. It does now, so add it. self._container_registry.addContainer(container) @@ -238,6 +251,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): global_stack.containersChanged.emit(container) @@ -245,7 +259,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: for container in stack.getContainers(): stack.containersChanged.emit(container) - # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) return nodes From c919883178217a093f2692949306349c06397615 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:50:07 +0100 Subject: [PATCH 24/50] Extruder stacks now properly get global stack set as next CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 0933c1be50..1f9b042d91 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -188,8 +188,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) - else: - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) quality_changes_instance_containers.append(instance_container) else: continue @@ -257,8 +255,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): global_stack.containersChanged.emit(container) for stack in extruder_stacks: + stack.setNextStack(global_stack) for container in stack.getContainers(): stack.containersChanged.emit(container) + # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) return nodes From b8746aee30a088340bec865341ced6f30df16d74 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 15:59:14 +0100 Subject: [PATCH 25/50] Added hack so the new extruders are added to extruder manager CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 1f9b042d91..05b850efcd 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -10,6 +10,9 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Preferences import Preferences from .WorkspaceDialog import WorkspaceDialog + +from cura.Settings.ExtruderManager import ExtruderManager + import zipfile import io @@ -249,6 +252,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + for stack in extruder_stacks: + if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} + #TODO: This is nasty hack; this should be made way more robust (setter?) + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack + Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. for container in global_stack.getContainers(): From 2e4b430cf8c738f35ebb790653e1d9236fb8d6e0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:04:58 +0100 Subject: [PATCH 26/50] User changes are now also updated correctly when resolving to new machine in workspace loading CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 29 ++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 05b850efcd..94394ba4e1 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -171,17 +171,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): new_id = self.getNewId(extruder_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) + instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id)) self._container_registry.addContainer(instance_container) - continue machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: new_id = self.getNewId(machine_id) + "_current_settings" instance_container._id = new_id instance_container.setName(new_id) + instance_container.setMetaDataEntry("machine", self.getNewId(machine_id)) self._container_registry.addContainer(instance_container) - # TODO: Handle other resolve strategies - pass user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. @@ -209,14 +208,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = container_stacks[0] if self._resolve_strategies["machine"] == "override": container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) - else: + elif self._resolve_strategies["machine"] == "new": new_id = self.getNewId(container_id) stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the + # bound machine also needs to change. + if stack.getMetaDataEntry("machine", None): + stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) # Ensure a unique ID and name stack._id = new_id stack.setName(self._container_registry.uniqueName(stack.getName())) self._container_registry.addContainer(stack) + else: + Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: stack = ContainerStack(container_id) # Deserialize stack by converting read data from bytes to string @@ -228,6 +234,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: global_stack = stack + if self._resolve_strategies["machine"] == "new": + # A new machine was made, but it was serialized with the wrong user container. Fix that now. + for container in user_instance_containers: + extruder_id = container.getMetaDataEntry("extruder", None) + if extruder_id: + for extruder in extruder_stacks: + if extruder.getId() == extruder_id: + extruder.replaceContainer(0, container) + continue + machine_id = container.getMetaDataEntry("machine", None) + if machine_id: + if global_stack.getId() == machine_id: + global_stack.replaceContainer(0, container) + continue + if self._resolve_strategies["quality_changes"] == "new": # Quality changes needs to get a new ID, added to registry and to the right stacks for container in quality_changes_instance_containers: From 3245c2fe32d93ab77735210ac91e5c9c0c38098d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:08:43 +0100 Subject: [PATCH 27/50] Extruder stacks are no longer renamed when loading from workspace CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 94394ba4e1..08da848392 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -213,13 +213,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack = ContainerStack(new_id) stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Ensure a unique ID and name + stack._id = new_id + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the # bound machine also needs to change. if stack.getMetaDataEntry("machine", None): stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) - # Ensure a unique ID and name - stack._id = new_id - stack.setName(self._container_registry.uniqueName(stack.getName())) + + if stack.getMetaDataEntry("type") != "extruder_train": + # Only machines need a new name, stacks may be non-unique + stack.setName(self._container_registry.uniqueName(stack.getName())) self._container_registry.addContainer(stack) else: Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) From 1db2d06e0655ae34cc8030527158397e855e55a0 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:15:29 +0100 Subject: [PATCH 28/50] Fixed "new" resolve if only the machine was double. CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 43 ++++++++++++--------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 08da848392..3d5ef22f0a 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -56,30 +56,31 @@ class ThreeMFWorkspaceReader(WorkspaceReader): cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - conflict = False + machine_conflict = False + quality_changes_conflict = False for container_stack_file in container_stack_files: container_id = self._stripFileToId(container_stack_file) stacks = self._container_registry.findContainerStacks(id=container_id) if stacks: - conflict = True + machine_conflict = True break # Check if any quality_changes instance container is in conflict. - if not conflict: - instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] - for instance_container_file in instance_container_files: - container_id = self._stripFileToId(instance_container_file) - instance_container = InstanceContainer(container_id) + instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] + for instance_container_file in instance_container_files: + container_id = self._stripFileToId(instance_container_file) + instance_container = InstanceContainer(container_id) - # Deserialize InstanceContainer by converting read data from bytes to string - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) - container_type = instance_container.getMetaDataEntry("type") - if container_type == "quality_changes": - # Check if quality changes already exists. - quality_changes = self._container_registry.findInstanceContainers(id = container_id) - if quality_changes: - conflict = True - if conflict: + # Deserialize InstanceContainer by converting read data from bytes to string + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + container_type = instance_container.getMetaDataEntry("type") + if container_type == "quality_changes": + # Check if quality changes already exists. + quality_changes = self._container_registry.findInstanceContainers(id = container_id) + if quality_changes: + quality_changes_conflict = True + + if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort self._resolve_strategies = {} self._dialog.show() @@ -87,9 +88,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == "cancel": return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() - # TODO: In the future it could be that machine & quality changes will have different resolve strategies - self._resolve_strategies = {"machine": result, "quality_changes": result} - pass + + self._resolve_strategies = {"machine": None, "quality_changes": None} + if machine_conflict: + self._resolve_strategies["machine"] = result + if quality_changes_conflict: + self._resolve_strategies["quality_changes"] = result + return WorkspaceReader.PreReadResult.accepted def read(self, file_name): From 8640b2b787d78c8e54b344ceeb9e4af775fe1314 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:45:30 +0100 Subject: [PATCH 29/50] Saving an empty buildplate as a workspace is now possible CURA-1263 --- plugins/3MFWriter/ThreeMFWriter.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index acf1421655..d86b119276 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -57,10 +57,6 @@ class ThreeMFWriter(MeshWriter): return self._archive def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): - try: - MeshWriter._meshNodes(nodes).__next__() - except StopIteration: - return False #Don't write anything if there is no mesh data. self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: @@ -86,7 +82,7 @@ class ThreeMFWriter(MeshWriter): build = ET.SubElement(model, "build") added_nodes = [] - + index = 0 # Ensure index always exists (even if there are no nodes to write) # Write all nodes with meshData to the file as objects inside the resource tag for index, n in enumerate(MeshWriter._meshNodes(nodes)): added_nodes.append(n) # Save the nodes that have mesh data From bade9e1bff66983b7c50bc038bc3216aff67e7cf Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:50:54 +0100 Subject: [PATCH 30/50] Resolve strategy is now always set CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3d5ef22f0a..266217f98b 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -55,7 +55,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - + self._resolve_strategies = {"machine": None, "quality_changes": None} machine_conflict = False quality_changes_conflict = False for container_stack_file in container_stack_files: @@ -89,7 +89,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() - self._resolve_strategies = {"machine": None, "quality_changes": None} if machine_conflict: self._resolve_strategies["machine"] = result if quality_changes_conflict: From dffb54d9010288edf3693ece6f1a3af4b9c04859 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:54:38 +0100 Subject: [PATCH 31/50] Removed abundant whitespace --- cura/Settings/ExtruderManager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index d3005a78fe..81a58c3f78 100644 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -287,7 +287,6 @@ class ExtruderManager(QObject): result.append(stack.getProperty(setting_key, property)) return result - ## Removes the container stack and user profile for the extruders for a specific machine. # # \param machine_id The machine to remove the extruders for. From 47d0e95e53907e27c33ac1748fff32de6163a520 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 14 Nov 2016 17:55:30 +0100 Subject: [PATCH 32/50] Loading single extrusion machine from file no longer gives exception CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 266217f98b..42a9ad67f6 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -281,10 +281,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + # TODO: This is nasty hack; this should be made way more robust (setter?) + if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: + ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} for stack in extruder_stacks: - if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} - #TODO: This is nasty hack; this should be made way more robust (setter?) ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack Logger.log("d", "Workspace loading is notifying rest of the code of changes...") From f0eb5e0da3f5e41d43e4b90d727b9174627fbbae Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:10:10 +0100 Subject: [PATCH 33/50] User can now select what strategy to use per conflict CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 18 ++- plugins/3MFReader/WorkspaceDialog.py | 53 ++++++--- plugins/3MFReader/WorkspaceDialog.qml | 116 +++++++++++++++++--- 3 files changed, 154 insertions(+), 33 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 42a9ad67f6..31ea96d6df 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -82,17 +82,23 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort - self._resolve_strategies = {} + self._dialog.setMachineConflict(machine_conflict) + self._dialog.setQualityChangesConflict(quality_changes_conflict) self._dialog.show() self._dialog.waitForClose() - if self._dialog.getResult() == "cancel": + if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled result = self._dialog.getResult() + # If there is no conflict, ignore the data. + print("beep", result) + if not machine_conflict: + result["machine"] = None + if not quality_changes_conflict: + result["quality_changes"] = None - if machine_conflict: - self._resolve_strategies["machine"] = result - if quality_changes_conflict: - self._resolve_strategies["quality_changes"] = result + + self._resolve_strategies = result + print("STRATEGY WAS", self._resolve_strategies) return WorkspaceReader.PreReadResult.accepted diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index ae1280b4bd..c90561e52d 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -1,7 +1,8 @@ -from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty from PyQt5.QtQml import QQmlComponent, QQmlContext from UM.PluginRegistry import PluginRegistry from UM.Application import Application +from UM.Logger import Logger import os import threading @@ -16,10 +17,36 @@ class WorkspaceDialog(QObject): self._view = None self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() - self._result = None # What option did the user pick? + self._default_strategy = "override" + self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) + self._has_quality_changes_conflict = False + self._has_machine_conflict = False + + machineConflictChanged = pyqtSignal() + qualityChangesConflictChanged = pyqtSignal() + + @pyqtProperty(bool, notify = machineConflictChanged) + def machineConflict(self): + return self._has_machine_conflict + + @pyqtProperty(bool, notify=qualityChangesConflictChanged) + def qualityChangesConflict(self): + return self._has_quality_changes_conflict + + @pyqtSlot(str, str) + def setResolveStrategy(self, key, strategy): + if key in self._result: + self._result[key] = strategy + + def setMachineConflict(self, machine_conflict): + self._has_machine_conflict = machine_conflict + + def setQualityChangesConflict(self, quality_changes_conflict): + self._has_quality_changes_conflict = quality_changes_conflict + def getResult(self): return self._result @@ -29,11 +56,15 @@ class WorkspaceDialog(QObject): self._context = QQmlContext(Application.getInstance()._engine.rootContext()) self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) + if self._view is None: + Logger.log("e", "QQmlComponent status %s", self._component.status()) + Logger.log("e", "QQmlComponent errorString %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. self._lock.acquire() - self._result = None + # Reset the result + self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} self._visible = True self.showDialogSignal.emit() @@ -41,7 +72,7 @@ class WorkspaceDialog(QObject): ## Used to notify the dialog so the lock can be released. def notifyClosed(self): if self._result is None: - self._result = "cancel" + self._result = {} self._lock.release() def hide(self): @@ -50,22 +81,15 @@ class WorkspaceDialog(QObject): self._view.hide() @pyqtSlot() - def onOverrideButtonClicked(self): + def onOkButtonClicked(self): self._view.hide() self.hide() - self._result = "override" - - @pyqtSlot() - def onNewButtonClicked(self): - self._view.hide() - self.hide() - self._result = "new" @pyqtSlot() def onCancelButtonClicked(self): self._view.hide() self.hide() - self._result = "cancel" + self._result = {} ## Block thread until the dialog is closed. def waitForClose(self): @@ -76,4 +100,5 @@ class WorkspaceDialog(QObject): def __show(self): if self._view is None: self._createViewFromQML() - self._view.show() + if self._view: + self._view.show() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 0c56dbcb6c..6014739c39 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -10,7 +10,7 @@ import UM 1.1 as UM UM.Dialog { - title: catalog.i18nc("@title:window", "Conflict") + title: catalog.i18nc("@title:window", "Import workspace conflict") width: 350 * Screen.devicePixelRatio; minimumWidth: 350 * Screen.devicePixelRatio; @@ -21,24 +21,114 @@ UM.Dialog maximumHeight: 250 * Screen.devicePixelRatio; onClosing: manager.notifyClosed() - + onVisibleChanged: + { + if(visible) + { + machineResolveComboBox.currentIndex = 0 + qualityChangesResolveComboBox.currentIndex = 0 + } + } Item { - UM.I18nCatalog { id: catalog; name: "cura"; } + anchors.fill: parent + + UM.I18nCatalog + { + id: catalog; + name: "cura"; + } + + ListModel + { + id: resolveStrategiesModel + // Instead of directly adding the list elements, we add them afterwards. + // This is because it's impossible to use setting function results to be bound to listElement properties directly. + // See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties + Component.onCompleted: + { + append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Override existing")}); + append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")}); + } + } + + Column + { + anchors.fill: parent + Label + { + id: infoLabel + width: parent.width + text: catalog.i18nc("@action:label", "Cura detected a number of conflicts while importing the workspace. How would you like to resolve these?") + wrapMode: Text.Wrap + height: 50 + } + UM.TooltipArea + { + id: machineResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?") + visible: manager.machineConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Machine") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: machineResolveComboBox + onActivated: + { + manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + } + } + } + } + UM.TooltipArea + { + id: qualityChangesResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?") + visible: manager.qualityChangesConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Profile") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: qualityChangesResolveComboBox + onActivated: + { + manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + } + } + } + } + } } rightButtons: [ Button { - id: override_button - text: catalog.i18nc("@action:button","Override"); - onClicked: { manager.onOverrideButtonClicked() } - enabled: true - }, - Button - { - id: create_new - text: catalog.i18nc("@action:button","Create new"); - onClicked: { manager.onNewButtonClicked() } + id: ok_button + text: catalog.i18nc("@action:button","OK"); + onClicked: { manager.onOkButtonClicked() } enabled: true }, Button From bbf5c73dae3e394e2d223c3ecaf12f4445d3f7e2 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:11:32 +0100 Subject: [PATCH 34/50] Quality_changes user option is now set correctly CURA-1263 --- plugins/3MFReader/WorkspaceDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 6014739c39..1726b7abaa 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -116,7 +116,7 @@ UM.Dialog id: qualityChangesResolveComboBox onActivated: { - manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key) + manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key) } } } From fcd4fb86f54d59a362c39bb47e32abef5c6d3a1e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 15 Nov 2016 15:12:04 +0100 Subject: [PATCH 35/50] removed debug prints CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 31ea96d6df..f2cb4c72a7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -88,17 +88,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.waitForClose() if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled + result = self._dialog.getResult() # If there is no conflict, ignore the data. - print("beep", result) if not machine_conflict: result["machine"] = None if not quality_changes_conflict: result["quality_changes"] = None - - self._resolve_strategies = result - print("STRATEGY WAS", self._resolve_strategies) return WorkspaceReader.PreReadResult.accepted From 100e1f4f40e5fe6c9a8587ed08f5f09a1e38e962 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 13:10:42 +0100 Subject: [PATCH 36/50] Conflict in quality changes is now handled less naive Instead of only checking ID, we also check values of the QC CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 ++++- plugins/3MFReader/WorkspaceDialog.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index f2cb4c72a7..998fee5ccb 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -78,7 +78,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if quality_changes: - quality_changes_conflict = True + # Check if there really is a conflict by comparing the values + if quality_changes[0] != instance_container: + quality_changes_conflict = True + break if machine_conflict or quality_changes_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index c90561e52d..bbac4fb557 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -43,9 +43,11 @@ class WorkspaceDialog(QObject): def setMachineConflict(self, machine_conflict): self._has_machine_conflict = machine_conflict + self.machineConflictChanged.emit() def setQualityChangesConflict(self, quality_changes_conflict): self._has_quality_changes_conflict = quality_changes_conflict + self.qualityChangesConflictChanged.emit() def getResult(self): return self._result From b59be4c88b51c36aabca307cf4b92b3a31dbfc57 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 13:15:15 +0100 Subject: [PATCH 37/50] Moved result checking to the Dialog CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 +------- plugins/3MFReader/WorkspaceDialog.py | 4 ++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 998fee5ccb..0a25e5369e 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -92,13 +92,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - result = self._dialog.getResult() - # If there is no conflict, ignore the data. - if not machine_conflict: - result["machine"] = None - if not quality_changes_conflict: - result["quality_changes"] = None - self._resolve_strategies = result + self._resolve_strategies = self._dialog.getResult() return WorkspaceReader.PreReadResult.accepted diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index bbac4fb557..8d98de05d2 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -50,6 +50,10 @@ class WorkspaceDialog(QObject): self.qualityChangesConflictChanged.emit() def getResult(self): + if "machine" in self._result and not self._has_machine_conflict: + self._result["machine"] = None + if "quality_changes" in self._result and not self._has_quality_changes_conflict: + self._result["quality_changes"] = None return self._result def _createViewFromQML(self): From b175e6876fbbd22b21d6e69e981e49604d26cb02 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 14:45:34 +0100 Subject: [PATCH 38/50] Added material conflict option This is still desabled by default due to some architecture issues (so this is temporarily left as it is) CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 22 ++++++++++++++- plugins/3MFReader/WorkspaceDialog.py | 24 ++++++++++++++--- plugins/3MFReader/WorkspaceDialog.qml | 30 +++++++++++++++++++++ 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 0a25e5369e..df395b9e67 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -65,6 +65,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_conflict = True break + material_conflict = False + 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: + container_id = self._stripFileToId(material_container_file) + materials = self._container_registry.findInstanceContainers(id=container_id) + if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict + material_conflict = True + # Check if any quality_changes instance container is in conflict. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] for instance_container_file in instance_container_files: @@ -83,10 +95,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_conflict = True break - if machine_conflict or quality_changes_conflict: + if machine_conflict or quality_changes_conflict or material_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort self._dialog.setMachineConflict(machine_conflict) self._dialog.setQualityChangesConflict(quality_changes_conflict) + self._dialog.setMaterialConflict(material_conflict) self._dialog.show() self._dialog.waitForClose() if self._dialog.getResult() == {}: @@ -147,6 +160,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) + else: + if self._resolve_strategies["material"] == "override": + pass + Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace @@ -194,6 +211,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) + elif self._resolve_strategies["quality_changes"] is None: + # The ID already exists, but nothing in the values changed, so do nothing. + pass quality_changes_instance_containers.append(instance_container) else: continue diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 8d98de05d2..96a22e4cd7 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -18,15 +18,19 @@ class WorkspaceDialog(QObject): self._qml_url = "WorkspaceDialog.qml" self._lock = threading.Lock() self._default_strategy = "override" - self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} + self._result = {"machine": self._default_strategy, + "quality_changes": self._default_strategy, + "material": self._default_strategy} self._visible = False self.showDialogSignal.connect(self.__show) self._has_quality_changes_conflict = False self._has_machine_conflict = False + self._has_material_conflict = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() + materialConflictChanged = pyqtSignal() @pyqtProperty(bool, notify = machineConflictChanged) def machineConflict(self): @@ -36,11 +40,19 @@ class WorkspaceDialog(QObject): def qualityChangesConflict(self): return self._has_quality_changes_conflict + @pyqtProperty(bool, notify=materialConflictChanged) + def materialConflict(self): + return self._has_material_conflict + @pyqtSlot(str, str) def setResolveStrategy(self, key, strategy): if key in self._result: self._result[key] = strategy + def setMaterialConflict(self, material_conflict): + self._has_material_conflict = material_conflict + self.materialConflictChanged.emit() + def setMachineConflict(self, machine_conflict): self._has_machine_conflict = machine_conflict self.machineConflictChanged.emit() @@ -54,6 +66,8 @@ class WorkspaceDialog(QObject): self._result["machine"] = None if "quality_changes" in self._result and not self._has_quality_changes_conflict: self._result["quality_changes"] = None + if "material" in self._result and not self._has_material_conflict: + self._result["material"] = None return self._result def _createViewFromQML(self): @@ -63,14 +77,16 @@ class WorkspaceDialog(QObject): self._context.setContextProperty("manager", self) self._view = self._component.create(self._context) if self._view is None: - Logger.log("e", "QQmlComponent status %s", self._component.status()) - Logger.log("e", "QQmlComponent errorString %s", self._component.errorString()) + Logger.log("c", "QQmlComponent status %s", self._component.status()) + Logger.log("c", "QQmlComponent error string %s", self._component.errorString()) def show(self): # Emit signal so the right thread actually shows the view. self._lock.acquire() # Reset the result - self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy} + self._result = {"machine": self._default_strategy, + "quality_changes": self._default_strategy, + "material": self._default_strategy} self._visible = True self.showDialogSignal.emit() diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 1726b7abaa..3b33fa8661 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -27,6 +27,7 @@ UM.Dialog { machineResolveComboBox.currentIndex = 0 qualityChangesResolveComboBox.currentIndex = 0 + materialConflictComboBox.currentIndex = 0 } } Item @@ -121,6 +122,35 @@ UM.Dialog } } } + UM.TooltipArea + { + id: materialResolveTooltip + width: parent.width + height: visible ? 25 : 0 + text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") + visible: false #manager.materialConflict + Row + { + width: parent.width + height: childrenRect.height + Label + { + text: catalog.i18nc("@action:label","Material") + width: 150 + } + + ComboBox + { + model: resolveStrategiesModel + textRole: "label" + id: materialResolveComboBox + onActivated: + { + manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key) + } + } + } + } } } rightButtons: [ From 053f0ca031cef47bc9889ee8f9cfb451a4b14b5a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 14:54:48 +0100 Subject: [PATCH 39/50] Replaced hack for setting extruders with more robust setter CURA-1263 --- cura/Settings/ExtruderManager.py | 12 ++++++++++++ plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 ++++---- plugins/3MFReader/WorkspaceDialog.qml | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 81a58c3f78..50d2034860 100644 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -150,6 +150,18 @@ class ExtruderManager(QObject): if changed: self.extrudersChanged.emit(machine_id) + def registerExtruder(self, extruder_train, machine_id): + changed = False + + if machine_id not in self._extruder_trains: + self._extruder_trains[machine_id] = {} + changed = True + if extruder_train: + self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train + changed = True + if changed: + self.extrudersChanged.emit(machine_id) + ## Creates a container stack for an extruder train. # # The container stack has an extruder definition at the bottom, which is diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index df395b9e67..719ec1c8d7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -301,11 +301,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) - # TODO: This is nasty hack; this should be made way more robust (setter?) - if global_stack.getId() not in ExtruderManager.getInstance()._extruder_trains: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()] = {} for stack in extruder_stacks: - ExtruderManager.getInstance()._extruder_trains[global_stack.getId()][stack.getMetaDataEntry("position")] = stack + ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId()) + else: + # Machine has no extruders, but it needs to be registered with the extruder manager. + ExtruderManager.getInstance().registerExtruder(None, global_stack.getId()) Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Notify everything/one that is to notify about changes. diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 3b33fa8661..4120c5b61e 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -128,7 +128,7 @@ UM.Dialog width: parent.width height: visible ? 25 : 0 text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") - visible: false #manager.materialConflict + visible: false //manager.materialConflict Row { width: parent.width From 42be3c74727556367fe10f57a69f0c83ddc924ca Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 15:41:42 +0100 Subject: [PATCH 40/50] Conflict checker for machine now also checks if there is an actual difference CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 719ec1c8d7..04fc21b54a 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -15,6 +15,7 @@ from cura.Settings.ExtruderManager import ExtruderManager import zipfile import io +import configparser i18n_catalog = i18nCatalog("cura") @@ -62,8 +63,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(container_stack_file) stacks = self._container_registry.findContainerStacks(id=container_id) if stacks: - machine_conflict = True - break + # Check if there are any changes at all in any of the container stacks. + id_list = self._getContainerIdListFromSerialized(archive.open(container_stack_file).read().decode("utf-8")) + for index, container_id in enumerate(id_list): + if stacks[0].getContainer(index).getId() != container_id: + machine_conflict = True + break material_conflict = False xml_material_profile = self._getXmlProfileClass() @@ -161,8 +166,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) else: - if self._resolve_strategies["material"] == "override": - pass + pass Logger.log("d", "Workspace loading is checking instance containers...") @@ -328,3 +332,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for type_name, container_type in self._container_registry.getContainerTypes(): if type_name == "XmlMaterialProfile": return container_type + + ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. + def _getContainerIdListFromSerialized(self, serialized): + parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False) + parser.read_string(serialized) + container_string = parser["general"].get("containers", "") + container_list = container_string.split(",") + return [container_id for container_id in container_list if container_id != ""] From 9f27e7861f6c6b5fc53d96ee37434d50b1ba042c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 16:04:03 +0100 Subject: [PATCH 41/50] Workspace reader now does a pre-check to see if it's a workspace in the first place CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 04fc21b54a..060617875d 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -99,6 +99,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if quality_changes[0] != instance_container: quality_changes_conflict = True break + try: + archive.open("Cura/preferences.cfg") + except KeyError: + # If there is no preferences file, it's not a workspace, so notify user of failure. + Logger.log("w", "File %s is not a valid workspace.", file_name) + return WorkspaceReader.PreReadResult.failed if machine_conflict or quality_changes_conflict or material_conflict: # There is a conflict; User should choose to either update the existing data, add everything as new data or abort From a39d6824766384181827479760aa6720e8d0db26 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 16 Nov 2016 16:07:18 +0100 Subject: [PATCH 42/50] Renamed Load workspace to open Workspace CURA-1263 --- resources/qml/Actions.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 2719d09cbc..9d910dc660 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -290,7 +290,7 @@ Item Action { id: loadWorkspaceAction - text: catalog.i18nc("@action:inmenu menubar:file","&Load Workspace..."); + text: catalog.i18nc("@action:inmenu menubar:file","&Open Workspace..."); } Action From 92a4fd723967038fe51a6a3468fc73dbffe90cd8 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 17 Nov 2016 11:16:15 +0100 Subject: [PATCH 43/50] Materials are now also handled in conflict resolvement CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 28 ++++++++++++- plugins/3MFReader/WorkspaceDialog.qml | 2 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 41 ++++++++++++++++--- 3 files changed, 62 insertions(+), 9 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 060617875d..a6bc2e4a8c 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -158,6 +158,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.addContainer(definition_container) Logger.log("d", "Workspace loading is checking materials...") + material_containers = [] # 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: @@ -172,8 +173,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) self._container_registry.addContainer(material_container) else: - pass - + if not materials[0].isReadOnly(): # Only create new materials if they are not read only. + if self._resolve_strategies["material"] == "override": + materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8")) + elif self._resolve_strategies["material"] == "new": + # Note that we *must* deserialize it with a new ID, as multiple containers will be + # auto created & added. + material_container = xml_material_profile(self.getNewId(container_id)) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + self._container_registry.addContainer(material_container) + material_containers.append(material_container) Logger.log("d", "Workspace loading is checking instance containers...") # Get quality_changes and user profiles saved in the workspace @@ -311,6 +320,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_index = stack.getContainerIndex(old_container) stack.replaceContainer(quality_changes_index, container) + if self._resolve_strategies["material"] == "new": + for material in material_containers: + old_material = global_stack.findContainer({"type": "material"}) + if old_material.getId() in self._id_mapping: + material_index = global_stack.getContainerIndex(old_material) + global_stack.replaceContainer(material_index, material) + continue + + for stack in extruder_stacks: + old_material = stack.findContainer({"type": "material"}) + if old_material.getId() in self._id_mapping: + material_index = stack.getContainerIndex(old_material) + stack.replaceContainer(material_index, material) + continue + for stack in extruder_stacks: ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId()) else: diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 4120c5b61e..cdefd9a4b0 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -128,7 +128,7 @@ UM.Dialog width: parent.width height: visible ? 25 : 0 text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?") - visible: false //manager.materialConflict + visible: manager.materialConflict Row { width: parent.width diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 07acc5c37c..94f7368ab0 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -340,10 +340,22 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): mapping[key] = element first.append(element) + def clearData(self): + self._metadata = {} + self._name = "" + self._definition = None + self._instances = {} + self._read_only = False + self._dirty = False + self._path = "" + ## Overridden from InstanceContainer def deserialize(self, serialized): data = ET.fromstring(serialized) + # Reset previous metadata + self.clearData() # Ensure any previous data is gone. + self.addMetaDataEntry("type", "material") self.addMetaDataEntry("base_file", self.id) @@ -445,7 +457,16 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): definition = definitions[0] if machine_compatibility: - new_material = XmlMaterialProfile(self.id + "_" + machine_id) + new_material_id = self.id + "_" + machine_id + + # It could be that we are overwriting, so check if the ID already exists. + materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_material_id) + if materials: + new_material = materials[0] + new_material.clearData() + else: + new_material = XmlMaterialProfile(new_material_id) + new_material.setName(self.getName()) new_material.setMetaData(copy.deepcopy(self.getMetaData())) new_material.setDefinition(definition) @@ -459,9 +480,8 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_material.setProperty(key, "value", value, definition) new_material._dirty = False - - UM.Settings.ContainerRegistry.getInstance().addContainer(new_material) - + if not materials: + UM.Settings.ContainerRegistry.getInstance().addContainer(new_material) hotends = machine.iterfind("./um:hotend", self.__namespaces) for hotend in hotends: @@ -491,7 +511,15 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): else: Logger.log("d", "Unsupported material setting %s", key) - new_hotend_material = XmlMaterialProfile(self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")) + # It could be that we are overwriting, so check if the ID already exists. + new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_") + materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=new_hotend_id) + if materials: + new_hotend_material = materials[0] + new_hotend_material.clearData() + else: + new_hotend_material = XmlMaterialProfile(new_hotend_id) + new_hotend_material.setName(self.getName()) new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData())) new_hotend_material.setDefinition(definition) @@ -509,7 +537,8 @@ class XmlMaterialProfile(UM.Settings.InstanceContainer): new_hotend_material.setProperty(key, "value", value, definition) new_hotend_material._dirty = False - UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material) + if not materials: # It was not added yet, do so now. + UM.Settings.ContainerRegistry.getInstance().addContainer(new_hotend_material) def _addSettingElement(self, builder, instance): try: From 053ea6ad520df92f087632b35a851817123de21b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 17 Nov 2016 11:44:44 +0100 Subject: [PATCH 44/50] Bumped the used API version up by one CURA-1263 --- resources/qml/Cura.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 558a71c6d0..85be3342e9 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -7,7 +7,7 @@ import QtQuick.Controls.Styles 1.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 -import UM 1.2 as UM +import UM 1.3 as UM import Cura 1.0 as Cura import "Menus" From 42caf57993e15fe09fa4924fb527125e2b4ce2ee Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:31:58 +0100 Subject: [PATCH 45/50] Added "material" to default resolve strategies CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index a6bc2e4a8c..adde4a79e4 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -56,7 +56,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): archive = zipfile.ZipFile(file_name, "r") cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] - self._resolve_strategies = {"machine": None, "quality_changes": None} + self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None} machine_conflict = False quality_changes_conflict = False for container_stack_file in container_stack_files: From cba31d95ec67b919fefa99fba0b43b6fa7764f2c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:33:10 +0100 Subject: [PATCH 46/50] Workspace reader now loads from prefered suffix CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index adde4a79e4..5a818a2407 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -28,10 +28,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None self._container_registry = ContainerRegistry.getInstance() - self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).suffixes[0] + self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it - self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).suffixes[0] - self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).suffixes[0] + self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix + self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix self._resolve_strategies = {} @@ -73,7 +73,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_conflict = False xml_material_profile = self._getXmlProfileClass() if self._material_container_suffix is None: - self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0] + self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix 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: From e3eb75ab6eef45a7bdd70d4a450c54fec249d6ff Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:37:11 +0100 Subject: [PATCH 47/50] We now get material container by mimetype CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 5a818a2407..265e4cc074 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -7,6 +7,7 @@ from UM.Settings.ContainerStack import ContainerStack from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.MimeTypeDatabase import MimeTypeDatabase from UM.Preferences import Preferences from .WorkspaceDialog import WorkspaceDialog @@ -359,9 +360,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return file.replace("Cura/", "").split(".")[0] def _getXmlProfileClass(self): - for type_name, container_type in self._container_registry.getContainerTypes(): - if type_name == "XmlMaterialProfile": - return container_type + return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. def _getContainerIdListFromSerialized(self, serialized): From 8a67f44cf0967936405d3b5ddc9f42b1ba567fdd Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 13:38:06 +0100 Subject: [PATCH 48/50] We now also write with the preferedSuffix CURA-1263 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index b6c9d884af..cafc18858f 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -59,7 +59,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()): return # Empty file, do nothing. - file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).suffixes[0] + file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix # Some containers have a base file, which should then be the file to use. if "base_file" in container.getMetaData(): From 60d2d0d0920df115205d9ee5bec2b83ed420c5c6 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 14:15:12 +0100 Subject: [PATCH 49/50] Workspace reader is now a lot more transactional; Instead of adding the instance containers on the go, we add them right before serializing the stack. This enables us to remove them if the stack serialization goes wrong CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 98 +++++++++++++-------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 265e4cc074..7340df3bf0 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -144,6 +144,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._id_mapping = {} + # We don't add containers right away, but wait right until right before the stack serialization. + # We do this so that if something goes wrong, it's easier to clean up. + containers_to_add = [] + # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few # TODO: cases that the container loaded is the same (most notable in materials & definitions). # TODO: It might be possible that we need to add smarter checking in the future. @@ -172,7 +176,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not materials: material_container = xml_material_profile(container_id) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - self._container_registry.addContainer(material_container) + containers_to_add.append(material_container) else: if not materials[0].isReadOnly(): # Only create new materials if they are not read only. if self._resolve_strategies["material"] == "override": @@ -182,7 +186,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # auto created & added. material_container = xml_material_profile(self.getNewId(container_id)) material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) - self._container_registry.addContainer(material_container) + containers_to_add.append(material_container) material_containers.append(material_container) Logger.log("d", "Workspace loading is checking instance containers...") @@ -201,7 +205,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if quality changes already exists. user_containers = self._container_registry.findInstanceContainers(id=container_id) if not user_containers: - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) else: if self._resolve_strategies["machine"] == "override": user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) @@ -213,7 +217,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id)) - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) machine_id = instance_container.getMetaDataEntry("machine", None) if machine_id: @@ -221,13 +225,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container._id = new_id instance_container.setName(new_id) instance_container.setMetaDataEntry("machine", self.getNewId(machine_id)) - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) user_instance_containers.append(instance_container) elif container_type == "quality_changes": # Check if quality changes already exists. quality_changes = self._container_registry.findInstanceContainers(id = container_id) if not quality_changes: - self._container_registry.addContainer(instance_container) + containers_to_add.append(instance_container) else: if self._resolve_strategies["quality_changes"] == "override": quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8")) @@ -238,49 +242,67 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: continue + # Add all the containers right before we try to add / serialize the stack + for container in containers_to_add: + self._container_registry.addContainer(container) + # Get the stack(s) saved in the workspace. Logger.log("d", "Workspace loading is checking stacks containers...") container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)] global_stack = None extruder_stacks = [] - for container_stack_file in container_stack_files: - container_id = self._stripFileToId(container_stack_file) + container_stacks_added = [] + try: + for container_stack_file in container_stack_files: + container_id = self._stripFileToId(container_stack_file) - # Check if a stack by this ID already exists; - container_stacks = self._container_registry.findContainerStacks(id=container_id) - if container_stacks: - stack = container_stacks[0] - if self._resolve_strategies["machine"] == "override": - container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) - elif self._resolve_strategies["machine"] == "new": - new_id = self.getNewId(container_id) - stack = ContainerStack(new_id) - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + # Check if a stack by this ID already exists; + container_stacks = self._container_registry.findContainerStacks(id=container_id) + if container_stacks: + stack = container_stacks[0] + if self._resolve_strategies["machine"] == "override": + container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8")) + elif self._resolve_strategies["machine"] == "new": + new_id = self.getNewId(container_id) + stack = ContainerStack(new_id) + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - # Ensure a unique ID and name - stack._id = new_id + # Ensure a unique ID and name + stack._id = new_id - # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the - # bound machine also needs to change. - if stack.getMetaDataEntry("machine", None): - stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) + # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the + # bound machine also needs to change. + if stack.getMetaDataEntry("machine", None): + stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine"))) - if stack.getMetaDataEntry("type") != "extruder_train": - # Only machines need a new name, stacks may be non-unique - stack.setName(self._container_registry.uniqueName(stack.getName())) - self._container_registry.addContainer(stack) + if stack.getMetaDataEntry("type") != "extruder_train": + # Only machines need a new name, stacks may be non-unique + stack.setName(self._container_registry.uniqueName(stack.getName())) + container_stacks_added.append(stack) + self._container_registry.addContainer(stack) + else: + Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) else: - Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"]) - else: - stack = ContainerStack(container_id) - # Deserialize stack by converting read data from bytes to string - stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) - self._container_registry.addContainer(stack) + stack = ContainerStack(container_id) + # Deserialize stack by converting read data from bytes to string + stack.deserialize(archive.open(container_stack_file).read().decode("utf-8")) + container_stacks_added.append(stack) + self._container_registry.addContainer(stack) - if stack.getMetaDataEntry("type") == "extruder_train": - extruder_stacks.append(stack) - else: - global_stack = stack + if stack.getMetaDataEntry("type") == "extruder_train": + extruder_stacks.append(stack) + else: + global_stack = stack + except: + Logger.log("W", "We failed to serialize the stack. Trying to clean up.") + # Something went really wrong. Try to remove any data that we added. + for container in containers_to_add: + self._container_registry.getInstance().removeContainer(container.getId()) + + for container in container_stacks_added: + self._container_registry.getInstance().removeContainer(container.getId()) + + return None if self._resolve_strategies["machine"] == "new": # A new machine was made, but it was serialized with the wrong user container. Fix that now. From fa174763cf43050c61fd82b0fb18e9bcca04a656 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 22 Nov 2016 14:57:44 +0100 Subject: [PATCH 50/50] The 3mf workspace reader no longer locks application if it is accedently called from main CURA-1263 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 3 +++ plugins/3MFReader/WorkspaceDialog.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 7340df3bf0..79f2399cf7 100644 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -113,7 +113,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setQualityChangesConflict(quality_changes_conflict) self._dialog.setMaterialConflict(material_conflict) self._dialog.show() + + # Block until the dialog is closed. self._dialog.waitForClose() + if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 96a22e4cd7..bf9dce8264 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty +from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty, QCoreApplication from PyQt5.QtQml import QQmlComponent, QQmlContext from UM.PluginRegistry import PluginRegistry from UM.Application import Application @@ -6,6 +6,7 @@ from UM.Logger import Logger import os import threading +import time class WorkspaceDialog(QObject): showDialogSignal = pyqtSignal() @@ -82,7 +83,8 @@ class WorkspaceDialog(QObject): def show(self): # Emit signal so the right thread actually shows the view. - self._lock.acquire() + if threading.current_thread() != threading.main_thread(): + self._lock.acquire() # Reset the result self._result = {"machine": self._default_strategy, "quality_changes": self._default_strategy, @@ -116,8 +118,14 @@ class WorkspaceDialog(QObject): ## Block thread until the dialog is closed. def waitForClose(self): if self._visible: - self._lock.acquire() - self._lock.release() + if threading.current_thread() != threading.main_thread(): + self._lock.acquire() + self._lock.release() + else: + # If this is not run from a separate thread, we need to ensure that the events are still processed. + while self._visible: + time.sleep(1 / 50) + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. def __show(self): if self._view is None: