ThreeMFReader.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. import os.path
  4. import zipfile
  5. from UM.Job import Job
  6. from UM.Logger import Logger
  7. from UM.Math.Matrix import Matrix
  8. from UM.Math.Vector import Vector
  9. from UM.Mesh.MeshBuilder import MeshBuilder
  10. from UM.Mesh.MeshReader import MeshReader
  11. from UM.Scene.GroupDecorator import GroupDecorator
  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. from UM.Scene.SceneNode import SceneNode
  17. from cura.SliceableObjectDecorator import SliceableObjectDecorator
  18. MYPY = False
  19. import Savitar
  20. import numpy
  21. try:
  22. if not MYPY:
  23. import xml.etree.cElementTree as ET
  24. except ImportError:
  25. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  26. import xml.etree.ElementTree as ET
  27. ## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
  28. class ThreeMFReader(MeshReader):
  29. def __init__(self):
  30. super().__init__()
  31. self._supported_extensions = [".3mf"]
  32. self._root = None
  33. self._namespaces = {
  34. "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
  35. "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
  36. }
  37. self._base_name = ""
  38. self._unit = None
  39. def _createMatrixFromTransformationString(self, transformation):
  40. if transformation == "":
  41. return Matrix()
  42. splitted_transformation = transformation.split()
  43. ## Transformation is saved as:
  44. ## M00 M01 M02 0.0
  45. ## M10 M11 M12 0.0
  46. ## M20 M21 M22 0.0
  47. ## M30 M31 M32 1.0
  48. ## We switch the row & cols as that is how everyone else uses matrices!
  49. temp_mat = Matrix()
  50. # Rotation & Scale
  51. temp_mat._data[0, 0] = splitted_transformation[0]
  52. temp_mat._data[1, 0] = splitted_transformation[1]
  53. temp_mat._data[2, 0] = splitted_transformation[2]
  54. temp_mat._data[0, 1] = splitted_transformation[3]
  55. temp_mat._data[1, 1] = splitted_transformation[4]
  56. temp_mat._data[2, 1] = splitted_transformation[5]
  57. temp_mat._data[0, 2] = splitted_transformation[6]
  58. temp_mat._data[1, 2] = splitted_transformation[7]
  59. temp_mat._data[2, 2] = splitted_transformation[8]
  60. # Translation
  61. temp_mat._data[0, 3] = splitted_transformation[9]
  62. temp_mat._data[1, 3] = splitted_transformation[10]
  63. temp_mat._data[2, 3] = splitted_transformation[11]
  64. return temp_mat
  65. ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scenenode.
  66. # \returns Uranium Scenen node.
  67. def _convertSavitarNodeToUMNode(self, savitar_node):
  68. um_node = SceneNode()
  69. transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
  70. um_node.setTransformation(transformation)
  71. mesh_builder = MeshBuilder()
  72. data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)
  73. vertices = numpy.resize(data, (int(data.size / 3), 3))
  74. mesh_builder.setVertices(vertices)
  75. mesh_builder.calculateNormals(fast=True)
  76. mesh_data = mesh_builder.build()
  77. if len(mesh_data.getVertices()):
  78. um_node.setMeshData(mesh_data)
  79. for child in savitar_node.getChildren():
  80. child_node = self._convertSavitarNodeToUMNode(child)
  81. if child_node:
  82. um_node.addChild(child_node)
  83. if um_node.getMeshData() is None and len(um_node.getChildren()) == 0:
  84. return None
  85. settings = savitar_node.getSettings()
  86. # Add the setting override decorator, so we can add settings to this node.
  87. if settings:
  88. um_node.addDecorator(SettingOverrideDecorator())
  89. global_container_stack = Application.getInstance().getGlobalContainerStack()
  90. # Ensure the correct next container for the SettingOverride decorator is set.
  91. if global_container_stack:
  92. multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
  93. # Ensure that all extruder data is reset
  94. if not multi_extrusion:
  95. default_stack_id = global_container_stack.getId()
  96. else:
  97. default_stack = ExtruderManager.getInstance().getExtruderStack(0)
  98. if default_stack:
  99. default_stack_id = default_stack.getId()
  100. else:
  101. default_stack_id = global_container_stack.getId()
  102. um_node.callDecoration("setActiveExtruder", default_stack_id)
  103. # Get the definition & set it
  104. definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
  105. um_node.callDecoration("getStack").getTop().setDefinition(definition)
  106. setting_container = um_node.callDecoration("getStack").getTop()
  107. for key in settings:
  108. setting_value = settings[key]
  109. # Extruder_nr is a special case.
  110. if key == "extruder_nr":
  111. extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
  112. if extruder_stack:
  113. um_node.callDecoration("setActiveExtruder", extruder_stack.getId())
  114. else:
  115. Logger.log("w", "Unable to find extruder in position %s", setting_value)
  116. continue
  117. setting_container.setProperty(key,"value", setting_value)
  118. if len(um_node.getChildren()) > 0:
  119. group_decorator = GroupDecorator()
  120. um_node.addDecorator(group_decorator)
  121. um_node.setSelectable(True)
  122. if um_node.getMeshData():
  123. # Assuming that all nodes with mesh data are printable objects
  124. # affects (auto) slicing
  125. sliceable_decorator = SliceableObjectDecorator()
  126. um_node.addDecorator(sliceable_decorator)
  127. return um_node
  128. def read(self, file_name):
  129. result = []
  130. # The base object of 3mf is a zipped archive.
  131. try:
  132. archive = zipfile.ZipFile(file_name, "r")
  133. self._base_name = os.path.basename(file_name)
  134. parser = Savitar.ThreeMFParser()
  135. scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
  136. self._unit = scene_3mf.getUnit()
  137. for node in scene_3mf.getSceneNodes():
  138. um_node = self._convertSavitarNodeToUMNode(node)
  139. if um_node is None:
  140. continue
  141. # compensate for original center position, if object(s) is/are not around its zero position
  142. transform_matrix = Matrix()
  143. mesh_data = um_node.getMeshData()
  144. if mesh_data is not None:
  145. extents = mesh_data.getExtents()
  146. center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
  147. transform_matrix.setByTranslation(center_vector)
  148. transform_matrix.multiply(um_node.getLocalTransformation())
  149. um_node.setTransformation(transform_matrix)
  150. global_container_stack = Application.getInstance().getGlobalContainerStack()
  151. # Create a transformation Matrix to convert from 3mf worldspace into ours.
  152. # First step: flip the y and z axis.
  153. transformation_matrix = Matrix()
  154. transformation_matrix._data[1, 1] = 0
  155. transformation_matrix._data[1, 2] = 1
  156. transformation_matrix._data[2, 1] = -1
  157. transformation_matrix._data[2, 2] = 0
  158. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  159. # build volume.
  160. if global_container_stack:
  161. translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2,
  162. y=-global_container_stack.getProperty("machine_depth", "value") / 2,
  163. z=0)
  164. translation_matrix = Matrix()
  165. translation_matrix.setByTranslation(translation_vector)
  166. transformation_matrix.multiply(translation_matrix)
  167. # Third step: 3MF also defines a unit, wheras Cura always assumes mm.
  168. scale_matrix = Matrix()
  169. scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
  170. transformation_matrix.multiply(scale_matrix)
  171. # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
  172. um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
  173. result.append(um_node)
  174. except Exception:
  175. Logger.logException("e", "An exception occurred in 3mf reader.")
  176. return []
  177. return result
  178. ## Create a scale vector based on a unit string.
  179. # The core spec defines the following:
  180. # * micron
  181. # * millimeter (default)
  182. # * centimeter
  183. # * inch
  184. # * foot
  185. # * meter
  186. def _getScaleFromUnit(self, unit):
  187. if unit is None:
  188. unit = "millimeter"
  189. if unit == "micron":
  190. scale = 0.001
  191. elif unit == "millimeter":
  192. scale = 1
  193. elif unit == "centimeter":
  194. scale = 10
  195. elif unit == "inch":
  196. scale = 25.4
  197. elif unit == "foot":
  198. scale = 304.8
  199. elif unit == "meter":
  200. scale = 1000
  201. else:
  202. Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
  203. scale = 1
  204. return Vector(scale, scale, scale)