ThreeMFWriter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Uranium is released under the terms of the AGPLv3 or higher.
  3. from UM.Mesh.MeshWriter import MeshWriter
  4. from UM.Math.Vector import Vector
  5. from UM.Logger import Logger
  6. from UM.Math.Matrix import Matrix
  7. from UM.Application import Application
  8. try:
  9. import xml.etree.cElementTree as ET
  10. except ImportError:
  11. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  12. import xml.etree.ElementTree as ET
  13. import zipfile
  14. import UM.Application
  15. class ThreeMFWriter(MeshWriter):
  16. def __init__(self):
  17. super().__init__()
  18. self._namespaces = {
  19. "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
  20. "content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
  21. "relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
  22. "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
  23. }
  24. self._unit_matrix_string = self._convertMatrixToString(Matrix())
  25. self._archive = None
  26. self._store_archive = False
  27. def _convertMatrixToString(self, matrix):
  28. result = ""
  29. result += str(matrix._data[0,0]) + " "
  30. result += str(matrix._data[1,0]) + " "
  31. result += str(matrix._data[2,0]) + " "
  32. result += str(matrix._data[0,1]) + " "
  33. result += str(matrix._data[1,1]) + " "
  34. result += str(matrix._data[2,1]) + " "
  35. result += str(matrix._data[0,2]) + " "
  36. result += str(matrix._data[1,2]) + " "
  37. result += str(matrix._data[2,2]) + " "
  38. result += str(matrix._data[0,3]) + " "
  39. result += str(matrix._data[1,3]) + " "
  40. result += str(matrix._data[2,3]) + " "
  41. return result
  42. ## Should we store the archive
  43. # Note that if this is true, the archive will not be closed.
  44. # The object that set this parameter is then responsible for closing it correctly!
  45. def setStoreArchive(self, store_archive):
  46. self._store_archive = store_archive
  47. def getArchive(self):
  48. return self._archive
  49. def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
  50. self._archive = None # Reset archive
  51. archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
  52. try:
  53. model_file = zipfile.ZipInfo("3D/3dmodel.model")
  54. # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
  55. model_file.compress_type = zipfile.ZIP_DEFLATED
  56. # Create content types file
  57. content_types_file = zipfile.ZipInfo("[Content_Types].xml")
  58. content_types_file.compress_type = zipfile.ZIP_DEFLATED
  59. content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
  60. rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
  61. model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
  62. # Create _rels/.rels file
  63. relations_file = zipfile.ZipInfo("_rels/.rels")
  64. relations_file.compress_type = zipfile.ZIP_DEFLATED
  65. relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
  66. model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
  67. model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"])
  68. # Add the version of Cura this was created with. As "CuraVersion" is not a recognised metadata name
  69. # by 3mf itself, we place it in our own namespace.
  70. version_metadata = ET.SubElement(model, "metadata", xmlns = self._namespaces["cura"], name = "CuraVersion")
  71. version_metadata.text = Application.getInstance().getVersion()
  72. resources = ET.SubElement(model, "resources")
  73. build = ET.SubElement(model, "build")
  74. added_nodes = []
  75. index = 0 # Ensure index always exists (even if there are no nodes to write)
  76. # Write all nodes with meshData to the file as objects inside the resource tag
  77. for index, n in enumerate(MeshWriter._meshNodes(nodes)):
  78. added_nodes.append(n) # Save the nodes that have mesh data
  79. object = ET.SubElement(resources, "object", id = str(index+1), type = "model")
  80. mesh = ET.SubElement(object, "mesh")
  81. mesh_data = n.getMeshData()
  82. vertices = ET.SubElement(mesh, "vertices")
  83. verts = mesh_data.getVertices()
  84. if verts is None:
  85. Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.")
  86. continue # No mesh data, nothing to do.
  87. if mesh_data.hasIndices():
  88. for face in mesh_data.getIndices():
  89. v1 = verts[face[0]]
  90. v2 = verts[face[1]]
  91. v3 = verts[face[2]]
  92. xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2]))
  93. xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2]))
  94. xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2]))
  95. triangles = ET.SubElement(mesh, "triangles")
  96. for face in mesh_data.getIndices():
  97. triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2]))
  98. else:
  99. triangles = ET.SubElement(mesh, "triangles")
  100. for idx, vert in enumerate(verts):
  101. xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2]))
  102. # If we have no faces defined, assume that every three subsequent vertices form a face.
  103. if idx % 3 == 0:
  104. triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2))
  105. # Handle per object settings
  106. stack = n.callDecoration("getStack")
  107. if stack is not None:
  108. changed_setting_keys = set(stack.getTop().getAllKeys())
  109. # Ensure that we save the extruder used for this object.
  110. if stack.getProperty("machine_extruder_count", "value") > 1:
  111. changed_setting_keys.add("extruder_nr")
  112. settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"])
  113. # Get values for all changed settings & save them.
  114. for key in changed_setting_keys:
  115. setting_xml = ET.SubElement(settings_xml, "setting", key = key)
  116. setting_xml.text = str(stack.getProperty(key, "value"))
  117. # Add one to the index as we haven't incremented the last iteration.
  118. index += 1
  119. nodes_to_add = set()
  120. for node in added_nodes:
  121. # Check the parents of the nodes with mesh_data and ensure that they are also added.
  122. parent_node = node.getParent()
  123. while parent_node is not None:
  124. if parent_node.callDecoration("isGroup"):
  125. nodes_to_add.add(parent_node)
  126. parent_node = parent_node.getParent()
  127. else:
  128. parent_node = None
  129. # Sort all the nodes by depth (so nodes with the highest depth are done first)
  130. sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True)
  131. # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene
  132. for node in sorted_nodes_to_add:
  133. object = ET.SubElement(resources, "object", id=str(index + 1), type="model")
  134. components = ET.SubElement(object, "components")
  135. for child in node.getChildren():
  136. if child in added_nodes:
  137. component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation()))
  138. index += 1
  139. added_nodes.append(node)
  140. # Create a transformation Matrix to convert from our worldspace into 3MF.
  141. # First step: flip the y and z axis.
  142. transformation_matrix = Matrix()
  143. transformation_matrix._data[1, 1] = 0
  144. transformation_matrix._data[1, 2] = -1
  145. transformation_matrix._data[2, 1] = 1
  146. transformation_matrix._data[2, 2] = 0
  147. global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
  148. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  149. # build volume.
  150. if global_container_stack:
  151. translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
  152. y=global_container_stack.getProperty("machine_depth", "value") / 2,
  153. z=0)
  154. translation_matrix = Matrix()
  155. translation_matrix.setByTranslation(translation_vector)
  156. transformation_matrix.preMultiply(translation_matrix)
  157. # Find out what the final build items are and add them.
  158. for node in added_nodes:
  159. if node.getParent().callDecoration("isGroup") is None:
  160. node_matrix = node.getLocalTransformation()
  161. ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix)))
  162. archive.writestr(model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model))
  163. archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
  164. archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
  165. except Exception as e:
  166. Logger.logException("e", "Error writing zip file")
  167. return False
  168. finally:
  169. if not self._store_archive:
  170. archive.close()
  171. else:
  172. self._archive = archive
  173. return True