ThreeMFWriter.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Uranium is released under the terms of the LGPLv3 or higher.
  3. from typing import Optional
  4. from UM.Mesh.MeshWriter import MeshWriter
  5. from UM.Math.Vector import Vector
  6. from UM.Logger import Logger
  7. from UM.Math.Matrix import Matrix
  8. from UM.Application import Application
  9. from UM.Scene.SceneNode import SceneNode
  10. from cura.CuraApplication import CuraApplication
  11. from cura.Utils.Threading import call_on_qt_thread
  12. from cura.Snapshot import Snapshot
  13. from PyQt5.QtCore import QBuffer
  14. import Savitar
  15. import numpy
  16. import datetime
  17. MYPY = False
  18. try:
  19. if not MYPY:
  20. import xml.etree.cElementTree as ET
  21. except ImportError:
  22. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  23. import xml.etree.ElementTree as ET
  24. import zipfile
  25. import UM.Application
  26. from UM.i18n import i18nCatalog
  27. catalog = i18nCatalog("cura")
  28. class ThreeMFWriter(MeshWriter):
  29. def __init__(self):
  30. super().__init__()
  31. self._namespaces = {
  32. "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
  33. "content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
  34. "relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
  35. "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
  36. }
  37. self._unit_matrix_string = self._convertMatrixToString(Matrix())
  38. self._archive = None # type: Optional[zipfile.ZipFile]
  39. self._store_archive = False
  40. def _convertMatrixToString(self, matrix):
  41. result = ""
  42. result += str(matrix._data[0, 0]) + " "
  43. result += str(matrix._data[1, 0]) + " "
  44. result += str(matrix._data[2, 0]) + " "
  45. result += str(matrix._data[0, 1]) + " "
  46. result += str(matrix._data[1, 1]) + " "
  47. result += str(matrix._data[2, 1]) + " "
  48. result += str(matrix._data[0, 2]) + " "
  49. result += str(matrix._data[1, 2]) + " "
  50. result += str(matrix._data[2, 2]) + " "
  51. result += str(matrix._data[0, 3]) + " "
  52. result += str(matrix._data[1, 3]) + " "
  53. result += str(matrix._data[2, 3])
  54. return result
  55. def setStoreArchive(self, store_archive):
  56. """Should we store the archive
  57. Note that if this is true, the archive will not be closed.
  58. The object that set this parameter is then responsible for closing it correctly!
  59. """
  60. self._store_archive = store_archive
  61. def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
  62. """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
  63. :returns: Uranium Scene node.
  64. """
  65. if not isinstance(um_node, SceneNode):
  66. return None
  67. active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  68. if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
  69. return
  70. savitar_node = Savitar.SceneNode()
  71. savitar_node.setName(um_node.getName())
  72. node_matrix = um_node.getLocalTransformation()
  73. matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
  74. savitar_node.setTransformation(matrix_string)
  75. mesh_data = um_node.getMeshData()
  76. if mesh_data is not None:
  77. savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
  78. indices_array = mesh_data.getIndicesAsByteArray()
  79. if indices_array is not None:
  80. savitar_node.getMeshData().setFacesFromBytes(indices_array)
  81. else:
  82. savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring())
  83. # Handle per object settings (if any)
  84. stack = um_node.callDecoration("getStack")
  85. if stack is not None:
  86. changed_setting_keys = stack.getTop().getAllKeys()
  87. # Ensure that we save the extruder used for this object in a multi-extrusion setup
  88. if stack.getProperty("machine_extruder_count", "value") > 1:
  89. changed_setting_keys.add("extruder_nr")
  90. # Get values for all changed settings & save them.
  91. for key in changed_setting_keys:
  92. savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
  93. # Store the metadata.
  94. for key, value in um_node.metadata.items():
  95. savitar_node.setSetting(key, value)
  96. for child_node in um_node.getChildren():
  97. # only save the nodes on the active build plate
  98. if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
  99. continue
  100. savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
  101. if savitar_child_node is not None:
  102. savitar_node.addChild(savitar_child_node)
  103. return savitar_node
  104. def getArchive(self):
  105. return self._archive
  106. def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
  107. self._archive = None # Reset archive
  108. archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
  109. try:
  110. model_file = zipfile.ZipInfo("3D/3dmodel.model")
  111. # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
  112. model_file.compress_type = zipfile.ZIP_DEFLATED
  113. # Create content types file
  114. content_types_file = zipfile.ZipInfo("[Content_Types].xml")
  115. content_types_file.compress_type = zipfile.ZIP_DEFLATED
  116. content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
  117. rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
  118. model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
  119. # Create _rels/.rels file
  120. relations_file = zipfile.ZipInfo("_rels/.rels")
  121. relations_file.compress_type = zipfile.ZIP_DEFLATED
  122. relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
  123. model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
  124. # Attempt to add a thumbnail
  125. snapshot = self._createSnapshot()
  126. if snapshot:
  127. thumbnail_buffer = QBuffer()
  128. thumbnail_buffer.open(QBuffer.ReadWrite)
  129. snapshot.save(thumbnail_buffer, "PNG")
  130. thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
  131. # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
  132. archive.writestr(thumbnail_file, thumbnail_buffer.data())
  133. # Add PNG to content types file
  134. thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
  135. # Add thumbnail relation to _rels/.rels file
  136. thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
  137. savitar_scene = Savitar.Scene()
  138. metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
  139. for key, value in metadata_to_store.items():
  140. savitar_scene.setMetaDataEntry(key, value)
  141. current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  142. if "Application" not in metadata_to_store:
  143. # This might sound a bit strange, but this field should store the original application that created
  144. # the 3mf. So if it was already set, leave it to whatever it was.
  145. savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
  146. if "CreationDate" not in metadata_to_store:
  147. savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
  148. savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
  149. transformation_matrix = Matrix()
  150. transformation_matrix._data[1, 1] = 0
  151. transformation_matrix._data[1, 2] = -1
  152. transformation_matrix._data[2, 1] = 1
  153. transformation_matrix._data[2, 2] = 0
  154. global_container_stack = Application.getInstance().getGlobalContainerStack()
  155. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  156. # build volume.
  157. if global_container_stack:
  158. translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
  159. y=global_container_stack.getProperty("machine_depth", "value") / 2,
  160. z=0)
  161. translation_matrix = Matrix()
  162. translation_matrix.setByTranslation(translation_vector)
  163. transformation_matrix.preMultiply(translation_matrix)
  164. root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
  165. for node in nodes:
  166. if node == root_node:
  167. for root_child in node.getChildren():
  168. savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
  169. if savitar_node:
  170. savitar_scene.addSceneNode(savitar_node)
  171. else:
  172. savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
  173. if savitar_node:
  174. savitar_scene.addSceneNode(savitar_node)
  175. parser = Savitar.ThreeMFParser()
  176. scene_string = parser.sceneToString(savitar_scene)
  177. archive.writestr(model_file, scene_string)
  178. archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
  179. archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
  180. except Exception as e:
  181. Logger.logException("e", "Error writing zip file")
  182. self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
  183. return False
  184. finally:
  185. if not self._store_archive:
  186. archive.close()
  187. else:
  188. self._archive = archive
  189. return True
  190. @call_on_qt_thread # must be called from the main thread because of OpenGL
  191. def _createSnapshot(self):
  192. Logger.log("d", "Creating thumbnail image...")
  193. if not CuraApplication.getInstance().isVisible:
  194. Logger.log("w", "Can't create snapshot when renderer not initialized.")
  195. return None
  196. try:
  197. snapshot = Snapshot.snapshot(width = 300, height = 300)
  198. except:
  199. Logger.logException("w", "Failed to create snapshot image")
  200. return None
  201. return snapshot