ThreeMFReader.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. # Copyright (c) 2021-2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import os.path
  4. import zipfile
  5. from typing import List, Optional, Union, TYPE_CHECKING, cast
  6. import pySavitar as Savitar
  7. import numpy
  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.MimeTypeDatabase import MimeTypeDatabase, MimeType
  14. from UM.Scene.GroupDecorator import GroupDecorator
  15. from UM.Scene.SceneNode import SceneNode # For typing.
  16. from UM.Scene.SceneNodeSettings import SceneNodeSettings
  17. from UM.Util import parseBool
  18. from cura.CuraApplication import CuraApplication
  19. from cura.Machines.ContainerTree import ContainerTree
  20. from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
  21. from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
  22. from cura.Scene.CuraSceneNode import CuraSceneNode
  23. from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
  24. from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
  25. from cura.Settings.ExtruderManager import ExtruderManager
  26. try:
  27. if not TYPE_CHECKING:
  28. import xml.etree.cElementTree as ET
  29. except ImportError:
  30. Logger.log("w", "Unable to load cElementTree, switching to slower version")
  31. import xml.etree.ElementTree as ET
  32. class ThreeMFReader(MeshReader):
  33. """Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!"""
  34. def __init__(self) -> None:
  35. super().__init__()
  36. MimeTypeDatabase.addMimeType(
  37. MimeType(
  38. name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
  39. comment="3MF",
  40. suffixes=["3mf"]
  41. )
  42. )
  43. self._supported_extensions = [".3mf"]
  44. self._root = None
  45. self._base_name = ""
  46. self._unit = None
  47. self._empty_project = False
  48. def emptyFileHintSet(self) -> bool:
  49. return self._empty_project
  50. @staticmethod
  51. def _createMatrixFromTransformationString(transformation: str) -> Matrix:
  52. if transformation == "":
  53. return Matrix()
  54. split_transformation = transformation.split()
  55. temp_mat = Matrix()
  56. """Transformation is saved as:
  57. M00 M01 M02 0.0
  58. M10 M11 M12 0.0
  59. M20 M21 M22 0.0
  60. M30 M31 M32 1.0
  61. We switch the row & cols as that is how everyone else uses matrices!
  62. """
  63. # Rotation & Scale
  64. temp_mat._data[0, 0] = split_transformation[0]
  65. temp_mat._data[1, 0] = split_transformation[1]
  66. temp_mat._data[2, 0] = split_transformation[2]
  67. temp_mat._data[0, 1] = split_transformation[3]
  68. temp_mat._data[1, 1] = split_transformation[4]
  69. temp_mat._data[2, 1] = split_transformation[5]
  70. temp_mat._data[0, 2] = split_transformation[6]
  71. temp_mat._data[1, 2] = split_transformation[7]
  72. temp_mat._data[2, 2] = split_transformation[8]
  73. # Translation
  74. temp_mat._data[0, 3] = split_transformation[9]
  75. temp_mat._data[1, 3] = split_transformation[10]
  76. temp_mat._data[2, 3] = split_transformation[11]
  77. return temp_mat
  78. @staticmethod
  79. def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
  80. """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
  81. :returns: Scene node.
  82. """
  83. try:
  84. node_name = savitar_node.getName()
  85. node_id = savitar_node.getId()
  86. except AttributeError:
  87. Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!")
  88. node_name = ""
  89. node_id = ""
  90. if node_name == "":
  91. if file_name != "":
  92. node_name = os.path.basename(file_name)
  93. else:
  94. node_name = "Object {}".format(node_id)
  95. active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  96. um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
  97. um_node.addDecorator(BuildPlateDecorator(active_build_plate))
  98. try:
  99. um_node.addDecorator(ConvexHullDecorator())
  100. except:
  101. pass
  102. um_node.setName(node_name)
  103. um_node.setId(node_id)
  104. transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
  105. um_node.setTransformation(transformation)
  106. mesh_builder = MeshBuilder()
  107. data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)
  108. vertices = numpy.resize(data, (int(data.size / 3), 3))
  109. mesh_builder.setVertices(vertices)
  110. mesh_builder.calculateNormals(fast=True)
  111. mesh_builder.setMeshId(node_id)
  112. if file_name:
  113. # The filename is used to give the user the option to reload the file if it is changed on disk
  114. # It is only set for the root node of the 3mf file
  115. mesh_builder.setFileName(file_name)
  116. mesh_data = mesh_builder.build()
  117. if len(mesh_data.getVertices()):
  118. um_node.setMeshData(mesh_data)
  119. for child in savitar_node.getChildren():
  120. child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
  121. if child_node:
  122. um_node.addChild(child_node)
  123. if um_node.getMeshData() is None and len(um_node.getChildren()) == 0:
  124. return None
  125. settings = savitar_node.getSettings()
  126. # Add the setting override decorator, so we can add settings to this node.
  127. if settings:
  128. global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
  129. # Ensure the correct next container for the SettingOverride decorator is set.
  130. if global_container_stack:
  131. default_stack = ExtruderManager.getInstance().getExtruderStack(0)
  132. if default_stack:
  133. um_node.callDecoration("setActiveExtruder", default_stack.getId())
  134. # Get the definition & set it
  135. definition_id = ContainerTree.getInstance().machines[global_container_stack.definition.getId()].quality_definition
  136. um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
  137. setting_container = um_node.callDecoration("getStack").getTop()
  138. known_setting_keys = um_node.callDecoration("getStack").getAllKeys()
  139. for key in settings:
  140. setting_value = settings[key].value
  141. # Extruder_nr is a special case.
  142. if key == "extruder_nr":
  143. extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
  144. if extruder_stack:
  145. um_node.callDecoration("setActiveExtruder", extruder_stack.getId())
  146. else:
  147. Logger.log("w", "Unable to find extruder in position %s", setting_value)
  148. continue
  149. if key == "print_order":
  150. um_node.printOrder = int(setting_value)
  151. continue
  152. if key =="drop_to_buildplate":
  153. um_node.setSetting(SceneNodeSettings.AutoDropDown, parseBool(setting_value))
  154. continue
  155. if key in known_setting_keys:
  156. setting_container.setProperty(key, "value", setting_value)
  157. else:
  158. um_node.metadata[key] = settings[key]
  159. if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None:
  160. if len(um_node.getAllChildren()) == 1:
  161. # We don't want groups of one, so move the node up one "level"
  162. child_node = um_node.getChildren()[0]
  163. # Move all the meshes of children so that toolhandles are shown in the correct place.
  164. if child_node.getMeshData():
  165. extents = child_node.getMeshData().getExtents()
  166. move_matrix = Matrix()
  167. move_matrix.translate(-extents.center)
  168. child_node.setMeshData(child_node.getMeshData().getTransformed(move_matrix))
  169. child_node.translate(extents.center)
  170. parent_transformation = um_node.getLocalTransformation()
  171. child_transformation = child_node.getLocalTransformation()
  172. child_node.setTransformation(parent_transformation.multiply(child_transformation))
  173. um_node = cast(CuraSceneNode, um_node.getChildren()[0])
  174. else:
  175. group_decorator = GroupDecorator()
  176. um_node.addDecorator(group_decorator)
  177. um_node.setSelectable(True)
  178. if um_node.getMeshData():
  179. # Assuming that all nodes with mesh data are printable objects
  180. # affects (auto) slicing
  181. sliceable_decorator = SliceableObjectDecorator()
  182. um_node.addDecorator(sliceable_decorator)
  183. return um_node
  184. def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
  185. self._empty_project = False
  186. result = []
  187. # The base object of 3mf is a zipped archive.
  188. try:
  189. archive = zipfile.ZipFile(file_name, "r")
  190. self._base_name = os.path.basename(file_name)
  191. parser = Savitar.ThreeMFParser()
  192. scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
  193. self._unit = scene_3mf.getUnit()
  194. for key, value in scene_3mf.getMetadata().items():
  195. CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
  196. for node in scene_3mf.getSceneNodes():
  197. um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name)
  198. if um_node is None:
  199. continue
  200. # compensate for original center position, if object(s) is/are not around its zero position
  201. transform_matrix = Matrix()
  202. mesh_data = um_node.getMeshData()
  203. if mesh_data is not None:
  204. extents = mesh_data.getExtents()
  205. if extents is not None:
  206. center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
  207. transform_matrix.setByTranslation(center_vector)
  208. transform_matrix.multiply(um_node.getLocalTransformation())
  209. um_node.setTransformation(transform_matrix)
  210. global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
  211. # Create a transformation Matrix to convert from 3mf worldspace into ours.
  212. # First step: flip the y and z axis.
  213. transformation_matrix = Matrix()
  214. transformation_matrix._data[1, 1] = 0
  215. transformation_matrix._data[1, 2] = 1
  216. transformation_matrix._data[2, 1] = -1
  217. transformation_matrix._data[2, 2] = 0
  218. # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
  219. # build volume.
  220. if global_container_stack:
  221. translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
  222. y = -global_container_stack.getProperty("machine_depth", "value") / 2,
  223. z = 0)
  224. translation_matrix = Matrix()
  225. translation_matrix.setByTranslation(translation_vector)
  226. transformation_matrix.multiply(translation_matrix)
  227. # Third step: 3MF also defines a unit, whereas Cura always assumes mm.
  228. scale_matrix = Matrix()
  229. scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
  230. transformation_matrix.multiply(scale_matrix)
  231. # Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
  232. um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
  233. # Check if the model is positioned below the build plate and honor that when loading project files.
  234. node_meshdata = um_node.getMeshData()
  235. if node_meshdata is not None:
  236. aabb = node_meshdata.getExtents(um_node.getWorldTransformation())
  237. if aabb is not None:
  238. minimum_z_value = aabb.minimum.y # y is z in transformation coordinates
  239. if minimum_z_value < 0:
  240. um_node.addDecorator(ZOffsetDecorator())
  241. um_node.callDecoration("setZOffset", minimum_z_value)
  242. result.append(um_node)
  243. if len(result) == 0:
  244. self._empty_project = True
  245. except Exception:
  246. Logger.logException("e", "An exception occurred in 3mf reader.")
  247. return []
  248. return result
  249. def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
  250. """Create a scale vector based on a unit string.
  251. .. The core spec defines the following:
  252. * micron
  253. * millimeter (default)
  254. * centimeter
  255. * inch
  256. * foot
  257. * meter
  258. """
  259. conversion_to_mm = {
  260. "micron": 0.001,
  261. "millimeter": 1,
  262. "centimeter": 10,
  263. "meter": 1000,
  264. "inch": 25.4,
  265. "foot": 304.8
  266. }
  267. if unit is None:
  268. unit = "millimeter"
  269. elif unit not in conversion_to_mm:
  270. Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit=unit))
  271. unit = "millimeter"
  272. scale = conversion_to_mm[unit]
  273. return Vector(scale, scale, scale)
  274. @staticmethod
  275. def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
  276. parser = Savitar.ThreeMFParser()
  277. scene = parser.parse(scene_string)
  278. # Convert the scene to scene nodes
  279. nodes = []
  280. for savitar_node in scene.getSceneNodes():
  281. scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
  282. if scene_node is None:
  283. continue
  284. nodes.append(scene_node)
  285. return nodes