ThreeMFWriter.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. import Savitar
  12. import numpy
  13. MYPY = False
  14. try:
  15. if not MYPY:
  16. import xml.etree.cElementTree as ET
  17. except ImportError:
  18. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  19. import xml.etree.ElementTree as ET
  20. import zipfile
  21. import UM.Application
  22. from UM.i18n import i18nCatalog
  23. catalog = i18nCatalog("cura")
  24. class ThreeMFWriter(MeshWriter):
  25. def __init__(self):
  26. super().__init__()
  27. self._namespaces = {
  28. "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
  29. "content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
  30. "relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
  31. "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
  32. }
  33. self._unit_matrix_string = self._convertMatrixToString(Matrix())
  34. self._archive = None # type: Optional[zipfile.ZipFile]
  35. self._store_archive = False
  36. def _convertMatrixToString(self, matrix):
  37. result = ""
  38. result += str(matrix._data[0, 0]) + " "
  39. result += str(matrix._data[1, 0]) + " "
  40. result += str(matrix._data[2, 0]) + " "
  41. result += str(matrix._data[0, 1]) + " "
  42. result += str(matrix._data[1, 1]) + " "
  43. result += str(matrix._data[2, 1]) + " "
  44. result += str(matrix._data[0, 2]) + " "
  45. result += str(matrix._data[1, 2]) + " "
  46. result += str(matrix._data[2, 2]) + " "
  47. result += str(matrix._data[0, 3]) + " "
  48. result += str(matrix._data[1, 3]) + " "
  49. result += str(matrix._data[2, 3])
  50. return result
  51. def setStoreArchive(self, store_archive):
  52. """Should we store the archive
  53. Note that if this is true, the archive will not be closed.
  54. The object that set this parameter is then responsible for closing it correctly!
  55. """
  56. self._store_archive = store_archive
  57. def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
  58. """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
  59. :returns: Uranium Scene node.
  60. """
  61. if not isinstance(um_node, SceneNode):
  62. return None
  63. active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  64. if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
  65. return
  66. savitar_node = Savitar.SceneNode()
  67. savitar_node.setName(um_node.getName())
  68. node_matrix = um_node.getLocalTransformation()
  69. matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
  70. savitar_node.setTransformation(matrix_string)
  71. mesh_data = um_node.getMeshData()
  72. if mesh_data is not None:
  73. savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
  74. indices_array = mesh_data.getIndicesAsByteArray()
  75. if indices_array is not None:
  76. savitar_node.getMeshData().setFacesFromBytes(indices_array)
  77. else:
  78. savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring())
  79. # Handle per object settings (if any)
  80. stack = um_node.callDecoration("getStack")
  81. if stack is not None:
  82. changed_setting_keys = stack.getTop().getAllKeys()
  83. # Ensure that we save the extruder used for this object in a multi-extrusion setup
  84. if stack.getProperty("machine_extruder_count", "value") > 1:
  85. changed_setting_keys.add("extruder_nr")
  86. # Get values for all changed settings & save them.
  87. for key in changed_setting_keys:
  88. savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
  89. for child_node in um_node.getChildren():
  90. # only save the nodes on the active build plate
  91. if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
  92. continue
  93. savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
  94. if savitar_child_node is not None:
  95. savitar_node.addChild(savitar_child_node)
  96. return savitar_node
  97. def getArchive(self):
  98. return self._archive
  99. def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
  100. self._archive = None # Reset archive
  101. archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
  102. try:
  103. model_file = zipfile.ZipInfo("3D/3dmodel.model")
  104. # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
  105. model_file.compress_type = zipfile.ZIP_DEFLATED
  106. # Create content types file
  107. content_types_file = zipfile.ZipInfo("[Content_Types].xml")
  108. content_types_file.compress_type = zipfile.ZIP_DEFLATED
  109. content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
  110. rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
  111. model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
  112. # Create _rels/.rels file
  113. relations_file = zipfile.ZipInfo("_rels/.rels")
  114. relations_file.compress_type = zipfile.ZIP_DEFLATED
  115. relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
  116. model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
  117. savitar_scene = Savitar.Scene()
  118. transformation_matrix = Matrix()
  119. transformation_matrix._data[1, 1] = 0
  120. transformation_matrix._data[1, 2] = -1
  121. transformation_matrix._data[2, 1] = 1
  122. transformation_matrix._data[2, 2] = 0
  123. global_container_stack = Application.getInstance().getGlobalContainerStack()
  124. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  125. # build volume.
  126. if global_container_stack:
  127. translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
  128. y=global_container_stack.getProperty("machine_depth", "value") / 2,
  129. z=0)
  130. translation_matrix = Matrix()
  131. translation_matrix.setByTranslation(translation_vector)
  132. transformation_matrix.preMultiply(translation_matrix)
  133. root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
  134. for node in nodes:
  135. if node == root_node:
  136. for root_child in node.getChildren():
  137. savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
  138. if savitar_node:
  139. savitar_scene.addSceneNode(savitar_node)
  140. else:
  141. savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
  142. if savitar_node:
  143. savitar_scene.addSceneNode(savitar_node)
  144. parser = Savitar.ThreeMFParser()
  145. scene_string = parser.sceneToString(savitar_scene)
  146. archive.writestr(model_file, scene_string)
  147. archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
  148. archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
  149. except Exception as e:
  150. Logger.logException("e", "Error writing zip file")
  151. self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
  152. return False
  153. finally:
  154. if not self._store_archive:
  155. archive.close()
  156. else:
  157. self._archive = archive
  158. return True