ThreeMFReader.py 11 KB

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