TrimeshReader.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. # Copyright (c) 2019 Ultimaker B.V., fieldOfView
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. # The _toMeshData function is taken from the AMFReader class which was built by fieldOfView.
  4. from typing import Any, List, Union, TYPE_CHECKING
  5. import numpy # To create the mesh data.
  6. import os.path # To create the mesh name for the resulting mesh.
  7. import trimesh # To load the files into a Trimesh.
  8. from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices # To construct meshes from the Trimesh data.
  9. from UM.Mesh.MeshReader import MeshReader # The plug-in type we're extending.
  10. from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType # To add file types that we can open.
  11. from UM.Scene.GroupDecorator import GroupDecorator # Added to the parent node if we load multiple nodes at once.
  12. from cura.CuraApplication import CuraApplication
  13. from cura.Scene.BuildPlateDecorator import BuildPlateDecorator # Added to the resulting scene node.
  14. from cura.Scene.ConvexHullDecorator import ConvexHullDecorator # Added to group nodes if we load multiple nodes at once.
  15. from cura.Scene.CuraSceneNode import CuraSceneNode # To create a node in the scene after reading the file.
  16. from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Added to the resulting scene node.
  17. if TYPE_CHECKING:
  18. from UM.Scene.SceneNode import SceneNode
  19. ## Class that leverages Trimesh to import files.
  20. class TrimeshReader(MeshReader):
  21. def __init__(self) -> None:
  22. super().__init__()
  23. self._supported_extensions = [".ctm", ".dae", ".gltf", ".glb", ".ply", ".zae"]
  24. MimeTypeDatabase.addMimeType(
  25. MimeType(
  26. name = "application/x-ctm",
  27. comment = "Open Compressed Triangle Mesh",
  28. suffixes = ["ctm"]
  29. )
  30. )
  31. MimeTypeDatabase.addMimeType(
  32. MimeType(
  33. name = "model/vnd.collada+xml",
  34. comment = "COLLADA Digital Asset Exchange",
  35. suffixes = ["dae"]
  36. )
  37. )
  38. MimeTypeDatabase.addMimeType(
  39. MimeType(
  40. name = "model/gltf-binary",
  41. comment = "glTF Binary",
  42. suffixes = ["glb"]
  43. )
  44. )
  45. MimeTypeDatabase.addMimeType(
  46. MimeType(
  47. name = "model/gltf+json",
  48. comment = "glTF Embedded JSON",
  49. suffixes = ["gltf"]
  50. )
  51. )
  52. # Trimesh seems to have a bug when reading .off files.
  53. #MimeTypeDatabase.addMimeType(
  54. # MimeType(
  55. # name = "application/x-off",
  56. # comment = "Geomview Object File Format",
  57. # suffixes = ["off"]
  58. # )
  59. #)
  60. MimeTypeDatabase.addMimeType(
  61. MimeType(
  62. name = "application/x-ply", # Wikipedia lists the MIME type as "text/plain" but that won't do as it's not unique to PLY files.
  63. comment = "Stanford Triangle Format",
  64. suffixes = ["ply"]
  65. )
  66. )
  67. MimeTypeDatabase.addMimeType(
  68. MimeType(
  69. name = "model/vnd.collada+xml+zip",
  70. comment = "Compressed COLLADA Digital Asset Exchange",
  71. suffixes = ["zae"]
  72. )
  73. )
  74. ## Reads a file using Trimesh.
  75. # \param file_name The file path. This is assumed to be one of the file
  76. # types that Trimesh can read. It will not be checked again.
  77. # \return A scene node that contains the file's contents.
  78. def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]:
  79. # CURA-6739
  80. # GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will
  81. # try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load()
  82. # doesn't like it. For some reason, this seems to happen with 3.5.7, but not 3.7.1. Below is a workaround to
  83. # pass a file object that has been opened with "r" instead "rb" to load a GLTF file.
  84. if file_name.lower().endswith(".gltf"):
  85. mesh_or_scene = trimesh.load(open(file_name, "r", encoding = "utf-8"), file_type = "gltf")
  86. else:
  87. mesh_or_scene = trimesh.load(file_name)
  88. meshes = [] # type: List[Union[trimesh.Trimesh, trimesh.Scene, Any]]
  89. if isinstance(mesh_or_scene, trimesh.Trimesh):
  90. meshes = [mesh_or_scene]
  91. elif isinstance(mesh_or_scene, trimesh.Scene):
  92. meshes = [mesh for mesh in mesh_or_scene.geometry.values()]
  93. active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
  94. nodes = [] # type: List[SceneNode]
  95. for mesh in meshes:
  96. if not isinstance(mesh, trimesh.Trimesh): # Trimesh can also receive point clouds, 2D paths, 3D paths or metadata. Skip those.
  97. continue
  98. mesh.merge_vertices()
  99. mesh.remove_unreferenced_vertices()
  100. mesh.fix_normals()
  101. mesh_data = self._toMeshData(mesh)
  102. file_base_name = os.path.basename(file_name)
  103. new_node = CuraSceneNode()
  104. new_node.setMeshData(mesh_data)
  105. new_node.setSelectable(True)
  106. new_node.setName(file_base_name if len(meshes) == 1 else "{file_base_name} {counter}".format(file_base_name = file_base_name, counter = str(len(nodes) + 1)))
  107. new_node.addDecorator(BuildPlateDecorator(active_build_plate))
  108. new_node.addDecorator(SliceableObjectDecorator())
  109. nodes.append(new_node)
  110. if len(nodes) == 1:
  111. return nodes[0]
  112. # Add all nodes to a group so they stay together.
  113. group_node = CuraSceneNode()
  114. group_node.addDecorator(GroupDecorator())
  115. group_node.addDecorator(ConvexHullDecorator())
  116. group_node.addDecorator(BuildPlateDecorator(active_build_plate))
  117. for node in nodes:
  118. node.setParent(group_node)
  119. return group_node
  120. ## Converts a Trimesh to Uranium's MeshData.
  121. # \param tri_node A Trimesh containing the contents of a file that was
  122. # just read.
  123. # \return Mesh data from the Trimesh in a way that Uranium can understand
  124. # it.
  125. def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
  126. tri_faces = tri_node.faces
  127. tri_vertices = tri_node.vertices
  128. indices = []
  129. vertices = []
  130. index_count = 0
  131. face_count = 0
  132. for tri_face in tri_faces:
  133. face = []
  134. for tri_index in tri_face:
  135. vertices.append(tri_vertices[tri_index])
  136. face.append(index_count)
  137. index_count += 1
  138. indices.append(face)
  139. face_count += 1
  140. vertices = numpy.asarray(vertices, dtype = numpy.float32)
  141. indices = numpy.asarray(indices, dtype = numpy.int32)
  142. normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
  143. mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals)
  144. return mesh_data