ThreeMFWriter.py 7.8 KB

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