ThreeMFReader.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Mesh.MeshReader import MeshReader
  4. from UM.Mesh.MeshBuilder import MeshBuilder
  5. from UM.Logger import Logger
  6. from UM.Math.Matrix import Matrix
  7. from UM.Math.Vector import Vector
  8. from UM.Scene.SceneNode import SceneNode
  9. from UM.Scene.GroupDecorator import GroupDecorator
  10. import UM.Application
  11. from UM.Job import Job
  12. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
  13. from UM.Application import Application
  14. from cura.Settings.ExtruderManager import ExtruderManager
  15. from cura.QualityManager import QualityManager
  16. import os.path
  17. import zipfile
  18. try:
  19. import xml.etree.cElementTree as ET
  20. except ImportError:
  21. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  22. import xml.etree.ElementTree as ET
  23. ## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
  24. class ThreeMFReader(MeshReader):
  25. def __init__(self):
  26. super().__init__()
  27. self._supported_extensions = [".3mf"]
  28. self._root = None
  29. self._namespaces = {
  30. "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
  31. "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
  32. }
  33. self._base_name = ""
  34. self._unit = None
  35. def _createNodeFromObject(self, object, name = ""):
  36. node = SceneNode()
  37. node.setName(name)
  38. mesh_builder = MeshBuilder()
  39. vertex_list = []
  40. components = object.find(".//3mf:components", self._namespaces)
  41. if components:
  42. for component in components:
  43. id = component.get("objectid")
  44. new_object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
  45. new_node = self._createNodeFromObject(new_object, self._base_name + "_" + str(id))
  46. node.addChild(new_node)
  47. transform = component.get("transform")
  48. if transform is not None:
  49. new_node.setTransformation(self._createMatrixFromTransformationString(transform))
  50. # for vertex in entry.mesh.vertices.vertex:
  51. for vertex in object.findall(".//3mf:vertex", self._namespaces):
  52. vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
  53. Job.yieldThread()
  54. xml_settings = list(object.findall(".//cura:setting", self._namespaces))
  55. # Add the setting override decorator, so we can add settings to this node.
  56. if xml_settings:
  57. node.addDecorator(SettingOverrideDecorator())
  58. global_container_stack = Application.getInstance().getGlobalContainerStack()
  59. # Ensure the correct next container for the SettingOverride decorator is set.
  60. if global_container_stack:
  61. multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
  62. # Ensure that all extruder data is reset
  63. if not multi_extrusion:
  64. default_stack_id = global_container_stack.getId()
  65. else:
  66. default_stack = ExtruderManager.getInstance().getExtruderStack(0)
  67. if default_stack:
  68. default_stack_id = default_stack.getId()
  69. else:
  70. default_stack_id = global_container_stack.getId()
  71. node.callDecoration("setActiveExtruder", default_stack_id)
  72. # Get the definition & set it
  73. definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
  74. node.callDecoration("getStack").getTop().setDefinition(definition)
  75. setting_container = node.callDecoration("getStack").getTop()
  76. for setting in xml_settings:
  77. setting_key = setting.get("key")
  78. setting_value = setting.text
  79. # Extruder_nr is a special case.
  80. if setting_key == "extruder_nr":
  81. extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
  82. if extruder_stack:
  83. node.callDecoration("setActiveExtruder", extruder_stack.getId())
  84. else:
  85. Logger.log("w", "Unable to find extruder in position %s", setting_value)
  86. continue
  87. setting_container.setProperty(setting_key,"value", setting_value)
  88. if len(node.getChildren()) > 0:
  89. group_decorator = GroupDecorator()
  90. node.addDecorator(group_decorator)
  91. triangles = object.findall(".//3mf:triangle", self._namespaces)
  92. mesh_builder.reserveFaceCount(len(triangles))
  93. for triangle in triangles:
  94. v1 = int(triangle.get("v1"))
  95. v2 = int(triangle.get("v2"))
  96. v3 = int(triangle.get("v3"))
  97. mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
  98. vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
  99. vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
  100. Job.yieldThread()
  101. # TODO: We currently do not check for normals and simply recalculate them.
  102. mesh_builder.calculateNormals()
  103. mesh_builder.setFileName(name)
  104. mesh_data = mesh_builder.build()
  105. if len(mesh_data.getVertices()):
  106. node.setMeshData(mesh_data)
  107. node.setSelectable(True)
  108. return node
  109. def _createMatrixFromTransformationString(self, transformation):
  110. splitted_transformation = transformation.split()
  111. ## Transformation is saved as:
  112. ## M00 M01 M02 0.0
  113. ## M10 M11 M12 0.0
  114. ## M20 M21 M22 0.0
  115. ## M30 M31 M32 1.0
  116. ## We switch the row & cols as that is how everyone else uses matrices!
  117. temp_mat = Matrix()
  118. # Rotation & Scale
  119. temp_mat._data[0, 0] = splitted_transformation[0]
  120. temp_mat._data[1, 0] = splitted_transformation[1]
  121. temp_mat._data[2, 0] = splitted_transformation[2]
  122. temp_mat._data[0, 1] = splitted_transformation[3]
  123. temp_mat._data[1, 1] = splitted_transformation[4]
  124. temp_mat._data[2, 1] = splitted_transformation[5]
  125. temp_mat._data[0, 2] = splitted_transformation[6]
  126. temp_mat._data[1, 2] = splitted_transformation[7]
  127. temp_mat._data[2, 2] = splitted_transformation[8]
  128. # Translation
  129. temp_mat._data[0, 3] = splitted_transformation[9]
  130. temp_mat._data[1, 3] = splitted_transformation[10]
  131. temp_mat._data[2, 3] = splitted_transformation[11]
  132. return temp_mat
  133. def read(self, file_name):
  134. result = []
  135. # The base object of 3mf is a zipped archive.
  136. archive = zipfile.ZipFile(file_name, "r")
  137. self._base_name = os.path.basename(file_name)
  138. try:
  139. self._root = ET.parse(archive.open("3D/3dmodel.model"))
  140. self._unit = self._root.getroot().get("unit")
  141. build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces)
  142. for build_item in build_items:
  143. id = build_item.get("objectid")
  144. object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
  145. if "type" in object.attrib:
  146. if object.attrib["type"] == "support" or object.attrib["type"] == "other":
  147. # Ignore support objects, as cura does not support these.
  148. # We can't guarantee that they wont be made solid.
  149. # We also ignore "other", as I have no idea what to do with them.
  150. Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"])
  151. continue
  152. elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model":
  153. pass # Load these as normal
  154. else:
  155. # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway.
  156. Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec",
  157. object.attrib["type"])
  158. continue
  159. build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id))
  160. # compensate for original center position, if object(s) is/are not around its zero position
  161. extents = build_item_node.getMeshData().getExtents()
  162. center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
  163. transform_matrix = Matrix()
  164. transform_matrix.setByTranslation(center_vector)
  165. # offset with transform from 3mf
  166. transform = build_item.get("transform")
  167. if transform is not None:
  168. transform_matrix.multiply(self._createMatrixFromTransformationString(transform))
  169. build_item_node.setTransformation(transform_matrix)
  170. global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
  171. # Create a transformation Matrix to convert from 3mf worldspace into ours.
  172. # First step: flip the y and z axis.
  173. transformation_matrix = Matrix()
  174. transformation_matrix._data[1, 1] = 0
  175. transformation_matrix._data[1, 2] = 1
  176. transformation_matrix._data[2, 1] = -1
  177. transformation_matrix._data[2, 2] = 0
  178. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  179. # build volume.
  180. if global_container_stack:
  181. translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
  182. y = -global_container_stack.getProperty("machine_depth", "value") / 2,
  183. z = 0)
  184. translation_matrix = Matrix()
  185. translation_matrix.setByTranslation(translation_vector)
  186. transformation_matrix.multiply(translation_matrix)
  187. # Third step: 3MF also defines a unit, wheras Cura always assumes mm.
  188. scale_matrix = Matrix()
  189. scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
  190. transformation_matrix.multiply(scale_matrix)
  191. # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
  192. build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix))
  193. result.append(build_item_node)
  194. except Exception as e:
  195. Logger.log("e", "An exception occurred in 3mf reader: %s", e)
  196. return result
  197. ## Create a scale vector based on a unit string.
  198. # The core spec defines the following:
  199. # * micron
  200. # * millimeter (default)
  201. # * centimeter
  202. # * inch
  203. # * foot
  204. # * meter
  205. def _getScaleFromUnit(self, unit):
  206. if unit is None:
  207. unit = "millimeter"
  208. if unit == "micron":
  209. scale = 0.001
  210. elif unit == "millimeter":
  211. scale = 1
  212. elif unit == "centimeter":
  213. scale = 10
  214. elif unit == "inch":
  215. scale = 25.4
  216. elif unit == "foot":
  217. scale = 304.8
  218. elif unit == "meter":
  219. scale = 1000
  220. else:
  221. Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
  222. scale = 1
  223. return Vector(scale, scale, scale)