Пример #1
0
    def write(self, stream, nodes, mode=MeshWriter.OutputMode.TextMode):
        """Write the specified sequence of nodes to a stream in the STL format.

        :param stream: The output stream to write to.
        :param nodes: A sequence of scene nodes to write to the output stream.
        :param mode: The output mode to use for writing scene nodes. Text mode
        causes the writer to write in STL's ASCII format. Binary mode causes the
        writer to write in STL's binary format. Any other mode is invalid.
        """

        try:
            MeshWriter._meshNodes(nodes).__next__()
        except StopIteration:
            Logger.log("e", "There is no mesh to write.")
            self.setInformation(
                catalog.i18nc("@error:no mesh", "There is no mesh to write."))
            return False  # Don't try to write a file if there is no mesh.

        if mode == MeshWriter.OutputMode.TextMode:
            self._writeAscii(stream, MeshWriter._meshNodes(nodes))
        elif mode == MeshWriter.OutputMode.BinaryMode:
            self._writeBinary(stream, MeshWriter._meshNodes(nodes))
        else:
            Logger.log("e", "Unsupported output mode writing STL to stream")
            self.setInformation(
                catalog.i18nc(
                    "@error:not supported",
                    "Unsupported output mode writing STL to stream."))
            return False

        return True
Пример #2
0
    def write(self, stream, nodes, mode=MeshWriter.OutputMode.TextMode):
        if mode != MeshWriter.OutputMode.TextMode:
            Logger.log("e", "OBJWriter does not support non-text mode.")
            self.setInformation(
                catalog.i18nc("@error:not supported",
                              "OBJWriter does not support non-text mode."))
            return False

        try:
            MeshWriter._meshNodes(nodes).__next__()
        except StopIteration:
            Logger.log("e", "There is no mesh to write.")
            self.setInformation(
                catalog.i18nc("@error:no mesh", "There is no mesh to write."))
            return False  #Don't write files without mesh data.

        stream.write("# URANIUM OBJ EXPORT {0}\n".format(
            time.strftime("%a %d %b %Y %H:%M:%S")))

        face_offset = 1
        for node in MeshWriter._meshNodes(nodes):
            mesh_data = node.getMeshData().getTransformed(
                node.getWorldTransformation())
            verts = mesh_data.getVertices()
            if verts is None:
                continue  # No mesh data, nothing to do.

            stream.write("# {0}\n# Vertices\n".format(node.getName()))

            if mesh_data.hasIndices():
                for face in mesh_data.getIndices():
                    v1 = verts[face[0]]
                    v2 = verts[face[1]]
                    v3 = verts[face[2]]
                    stream.write("v {0} {1} {2}\n".format(
                        v1[0], -v1[2], v1[1]))
                    stream.write("v {0} {1} {2}\n".format(
                        v2[0], -v2[2], v2[1]))
                    stream.write("v {0} {1} {2}\n".format(
                        v3[0], -v3[2], v3[1]))

                stream.write("# Faces\n")
                for face in mesh_data.getIndices():
                    stream.write("f {0} {1} {2}\n".format(
                        face[0] + face_offset, face[1] + face_offset,
                        face[2] + face_offset))
            else:
                for vertex in verts:
                    stream.write("v {0} {1} {2}\n".format(
                        vertex[0], -vertex[2], vertex[1]))

                stream.write("# Faces\n")
                for face in range(face_offset, face_offset + len(verts) - 1,
                                  3):
                    stream.write("f {0} {1} {2}\n".format(
                        face, face + 1, face + 2))

            face_offset += mesh_data.getVertexCount()

        return True
Пример #3
0
    def write(self, stream, nodes, mode=MeshWriter.OutputMode.TextMode):
        try:
            MeshWriter._meshNodes(nodes).__next__()
        except:
            return False  #Don't try to write a file if there is no mesh.

        if mode == MeshWriter.OutputMode.TextMode:
            self._writeAscii(stream, MeshWriter._meshNodes(nodes))
        elif mode == MeshWriter.OutputMode.BinaryMode:
            self._writeBinary(stream, MeshWriter._meshNodes(nodes))
        else:
            Logger.log("e", "Unsupported output mode writing STL to stream")
            return False

        return True
Пример #4
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
        try:
            MeshWriter._meshNodes(nodes).__next__()
        except:
            return False #Don't try to write a file if there is no mesh.

        if mode == MeshWriter.OutputMode.TextMode:
            self._writeAscii(stream, MeshWriter._meshNodes(nodes))
        elif mode == MeshWriter.OutputMode.BinaryMode:
            self._writeBinary(stream, MeshWriter._meshNodes(nodes))
        else:
            Logger.log("e", "Unsupported output mode writing STL to stream")
            return False

        return True
Пример #5
0
 def write(self, stream, nodes, mode=MeshWriter.OutputMode.BinaryMode):
     radius = SteSlicerApplication.getInstance().getGlobalContainerStack(
     ).getProperty("cylindrical_mode_base_diameter", "value") / 2
     height = SteSlicerApplication.getInstance().getGlobalContainerStack(
     ).getProperty("machine_height", "value")
     # try:
     MeshWriter._meshNodes(nodes).__next__()
     for node in nodes:
         mesh_data = node.getMeshData().getTransformed(
             node.getWorldTransformation())
         verts = mesh_data.getVertices()
         if verts is None:
             continue
         faces = None
         if mesh_data.hasIndices():
             faces = mesh_data.getIndices()
         else:
             num_verts = mesh_data.getVertexCount()
             faces = numpy.empty((int(num_verts / 3 + 1), 3), numpy.int32)
             for i in range(0, num_verts - 2, 3):
                 faces[int(i / 3):] = [i, i + 1, i + 2]
         if faces is None:
             continue
         vertices = []
         for vert in verts:
             vertices.append([vert[0], -vert[2], vert[1]])
         mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
         cutting_cylinder = trimesh.primitives.Cylinder(radius=radius,
                                                        height=height)
         result = mesh.difference(cutting_cylinder, engine="scad")
         stream.write("Uranium STLWriter {0}".format(
             time.strftime("%a %d %b %Y %H:%M:%S")).encode().ljust(
                 80, b"\000"))
         face_count = len(result.faces)
         # Write number of faces to STL
         stream.write(struct.pack("<I", int(face_count)))
         verts = result.vertices
         for face in result.faces:
             v1 = verts[face[0]]
             v2 = verts[face[1]]
             v3 = verts[face[2]]
             stream.write(struct.pack("<fff", 0.0, 0.0, 0.0))
             stream.write(struct.pack("<fff", v1[0], v1[1], v1[2]))
             stream.write(struct.pack("<fff", v2[0], v2[1], v2[2]))
             stream.write(struct.pack("<fff", v3[0], v3[1], v3[2]))
             stream.write(struct.pack("<H", 0))
         return True
Пример #6
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
        if mode != MeshWriter.OutputMode.TextMode:
            Logger.log("e", "OBJWriter does not support non-text mode.")
            self.setInformation(catalog.i18nc("@error:not supported", "OBJWriter does not support non-text mode."))
            return False

        try:
            MeshWriter._meshNodes(nodes).__next__()
        except StopIteration:
            Logger.log("e", "There is no mesh to write.")
            self.setInformation(catalog.i18nc("@error:no mesh", "There is no mesh to write."))
            return False #Don't write files without mesh data.

        stream.write("# URANIUM OBJ EXPORT {0}\n".format(time.strftime("%a %d %b %Y %H:%M:%S")))

        face_offset = 1
        for node in MeshWriter._meshNodes(nodes):
            mesh_data = node.getMeshData().getTransformed(node.getWorldTransformation())
            verts = mesh_data.getVertices()
            if verts is None:
                continue   # No mesh data, nothing to do.

            stream.write("# {0}\n# Vertices\n".format(node.getName()))

            if mesh_data.hasIndices():
                for face in mesh_data.getIndices():
                    v1 = verts[face[0]]
                    v2 = verts[face[1]]
                    v3 = verts[face[2]]
                    stream.write("v {0} {1} {2}\n".format(v1[0], -v1[2], v1[1]))
                    stream.write("v {0} {1} {2}\n".format(v2[0], -v2[2], v2[1]))
                    stream.write("v {0} {1} {2}\n".format(v3[0], -v3[2], v3[1]))

                stream.write("# Faces\n")
                for face in mesh_data.getIndices():
                    stream.write("f {0} {1} {2}\n".format(face[0] + face_offset, face[1] + face_offset, face[2] + face_offset))
            else:
                for vertex in verts:
                    stream.write("v {0} {1} {2}\n".format(vertex[0], -vertex[2], vertex[1]))

                stream.write("# Faces\n")
                for face in range(face_offset, face_offset + len(verts) - 1, 3):
                    stream.write("f {0} {1} {2}\n".format(face, face + 1, face + 2))

            face_offset += mesh_data.getVertexCount()

        return True
Пример #7
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
        try:
            MeshWriter._meshNodes(nodes).__next__()
        except StopIteration:
            Logger.log("e", "There is no mesh to write.")
            self.setInformation(catalog.i18nc("@error:no mesh", "There is no mesh to write."))
            return False  # Don't try to write a file if there is no mesh.

        if mode == MeshWriter.OutputMode.TextMode:
            self._writeAscii(stream, MeshWriter._meshNodes(nodes))
        elif mode == MeshWriter.OutputMode.BinaryMode:
            self._writeBinary(stream, MeshWriter._meshNodes(nodes))
        else:
            Logger.log("e", "Unsupported output mode writing STL to stream")
            self.setInformation(catalog.i18nc("@error:not supported", "Unsupported output mode writing STL to stream."))
            return False

        return True
Пример #8
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
        try:
            MeshWriter._meshNodes(nodes).__next__()
        except:
            Logger.log("e", "There is no mesh to write.")
            self.setInformation(catalog.i18nc("@error:no mesh", "There is no mesh to write."))
            return False #Don't try to write a file if there is no mesh.

        if mode == MeshWriter.OutputMode.TextMode:
            self._writeAscii(stream, MeshWriter._meshNodes(nodes))
        elif mode == MeshWriter.OutputMode.BinaryMode:
            self._writeBinary(stream, MeshWriter._meshNodes(nodes))
        else:
            Logger.log("e", "Unsupported output mode writing STL to stream")
            self.setInformation(catalog.i18nc("@error:not supported", "Unsupported output mode writing STL to stream."))
            return False

        return True
Пример #9
0
    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.

        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")
            for index, n in enumerate(MeshWriter._meshNodes(nodes)):
                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[2]),
                                                    z=str(v1[1]))
                        xml_vertex2 = ET.SubElement(vertices,
                                                    "vertex",
                                                    x=str(v2[0]),
                                                    y=str(v2[2]),
                                                    z=str(v2[1]))
                        xml_vertex3 = ET.SubElement(vertices,
                                                    "vertex",
                                                    x=str(v3[0]),
                                                    y=str(v3[2]),
                                                    z=str(v3[1]))

                    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[2]),
                                                   z=str(vert[1]))

                        # 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))
                world_transformation = n.getWorldTransformation()

                # 3MF sees lower left corner of buildplate as zero, so we need to translate a bit first
                global_container_stack = UM.Application.getInstance(
                ).getGlobalContainerStack()
                if global_container_stack:
                    translation = Vector(x=global_container_stack.getProperty(
                        "machine_width", "value") / 2,
                                         y=0,
                                         z=-global_container_stack.getProperty(
                                             "machine_depth", "value") / 2)
                else:
                    translation = Vector(0, 0, 0)
                translation_matrix = Matrix()
                translation_matrix.setByTranslation(translation)
                world_transformation.multiply(translation_matrix)

                # We use a different coordinate frame, so we need to flip the transformation
                flip_matrix = Matrix()
                flip_matrix._data[1, 1] = 0
                flip_matrix._data[1, 2] = 1
                flip_matrix._data[2, 1] = 1
                flip_matrix._data[2, 2] = 0
                world_transformation.multiply(flip_matrix)

                transformation_string = self._convertMatrixToString(
                    world_transformation)
                if transformation_string != self._convertMatrixToString(
                        Matrix()):
                    item = ET.SubElement(build,
                                         "item",
                                         objectid=str(index + 1),
                                         transform=transformation_string)
                else:
                    item = ET.SubElement(
                        build, "item",
                        objectid=str(index +
                                     1))  #, transform = transformation_string)

            archive.writestr(
                model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' +
                ET.tostring(model))
            archive.writestr(
                content_types_file,
                b'<?xml version="1.0" encoding="UTF-8"?> \n' +
                ET.tostring(content_types))
            archive.writestr(
                relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' +
                ET.tostring(relations_element))
        except Exception as e:
            Logger.logException("e", "Error writing zip file")
            return False
        finally:
            archive.close()

        return True
Пример #10
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
        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"])
            model.set("xmlns:cura", self._namespaces["cura"])

            # Add the version of Cura this was created with. Since there is no "version" or similar metadata name we need
            # to prefix it with the cura namespace, as specified by the 3MF specification.
            version_metadata = ET.SubElement(model, "metadata", name = "cura:version")
            version_metadata.text = Application.getInstance().getVersion()

            resources = ET.SubElement(model, "resources")
            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
                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 = 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'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model))
            archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
            archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \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
Пример #11
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
        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"])

            # Add the version of Cura this was created with. As "CuraVersion" is not a recognised metadata name
            #  by 3mf itself, we place it in our own namespace.
            version_metadata = ET.SubElement(model, "metadata", xmlns = self._namespaces["cura"], name = "CuraVersion")
            version_metadata.text = Application.getInstance().getVersion()

            resources = ET.SubElement(model, "resources")
            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
                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'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model))
            archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
            archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \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