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