diff --git a/cura.desktop.in b/cura.desktop.in index b0195015a5..2502327203 100644 --- a/cura.desktop.in +++ b/cura.desktop.in @@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura Icon=cura-icon Terminal=false Type=Application -MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode; +MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip; Categories=Graphics; Keywords=3D;Printing;Slicer; diff --git a/plugins/TrimeshReader/TrimeshReader.py b/plugins/TrimeshReader/TrimeshReader.py new file mode 100644 index 0000000000..41992ddbd1 --- /dev/null +++ b/plugins/TrimeshReader/TrimeshReader.py @@ -0,0 +1,152 @@ +# Copyright (c) 2019 Ultimaker B.V., fieldOfView +# Cura is released under the terms of the LGPLv3 or higher. + +# The _toMeshData function is taken from the AMFReader class which was built by fieldOfView. + +from typing import Any, List, Union, TYPE_CHECKING +import numpy # To create the mesh data. +import os.path # To create the mesh name for the resulting mesh. +import trimesh # To load the files into a Trimesh. + +from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices # To construct meshes from the Trimesh data. +from UM.Mesh.MeshReader import MeshReader # The plug-in type we're extending. +from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType # To add file types that we can open. +from UM.Scene.GroupDecorator import GroupDecorator # Added to the parent node if we load multiple nodes at once. + +from cura.CuraApplication import CuraApplication +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator # Added to the resulting scene node. +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator # Added to group nodes if we load multiple nodes at once. +from cura.Scene.CuraSceneNode import CuraSceneNode # To create a node in the scene after reading the file. +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Added to the resulting scene node. + +if TYPE_CHECKING: + from UM.Scene.SceneNode import SceneNode + +## Class that leverages Trimesh to import files. +class TrimeshReader(MeshReader): + def __init__(self) -> None: + super().__init__() + + self._supported_extensions = [".ctm", ".dae", ".gltf", ".glb", ".ply", ".zae"] + MimeTypeDatabase.addMimeType( + MimeType( + name = "application/x-ctm", + comment = "Open Compressed Triangle Mesh", + suffixes = ["ctm"] + ) + ) + MimeTypeDatabase.addMimeType( + MimeType( + name = "model/vnd.collada+xml", + comment = "COLLADA Digital Asset Exchange", + suffixes = ["dae"] + ) + ) + MimeTypeDatabase.addMimeType( + MimeType( + name = "model/gltf-binary", + comment = "glTF Binary", + suffixes = ["glb"] + ) + ) + MimeTypeDatabase.addMimeType( + MimeType( + name = "model/gltf+json", + comment = "glTF Embedded JSON", + suffixes = ["gltf"] + ) + ) + # Trimesh seems to have a bug when reading .off files. + #MimeTypeDatabase.addMimeType( + # MimeType( + # name = "application/x-off", + # comment = "Geomview Object File Format", + # suffixes = ["off"] + # ) + #) + MimeTypeDatabase.addMimeType( + MimeType( + 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. + comment = "Stanford Triangle Format", + suffixes = ["ply"] + ) + ) + MimeTypeDatabase.addMimeType( + MimeType( + name = "model/vnd.collada+xml+zip", + comment = "Compressed COLLADA Digital Asset Exchange", + suffixes = ["zae"] + ) + ) + + ## Reads a file using Trimesh. + # \param file_name The file path. This is assumed to be one of the file + # types that Trimesh can read. It will not be checked again. + # \return A scene node that contains the file's contents. + def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: + mesh_or_scene = trimesh.load(file_name) + meshes = [] # type: List[Union[trimesh.Trimesh, trimesh.Scene, Any]] + if isinstance(mesh_or_scene, trimesh.Trimesh): + meshes = [mesh_or_scene] + elif isinstance(mesh_or_scene, trimesh.Scene): + meshes = [mesh for mesh in mesh_or_scene.geometry.values()] + + active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate + nodes = [] # type: List[SceneNode] + for mesh in meshes: + if not isinstance(mesh, trimesh.Trimesh): # Trimesh can also receive point clouds, 2D paths, 3D paths or metadata. Skip those. + continue + mesh.merge_vertices() + mesh.remove_unreferenced_vertices() + mesh.fix_normals() + mesh_data = self._toMeshData(mesh) + + file_base_name = os.path.basename(file_name) + new_node = CuraSceneNode() + new_node.setMeshData(mesh_data) + new_node.setSelectable(True) + 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))) + new_node.addDecorator(BuildPlateDecorator(active_build_plate)) + new_node.addDecorator(SliceableObjectDecorator()) + nodes.append(new_node) + + if len(nodes) == 1: + return nodes[0] + # Add all nodes to a group so they stay together. + group_node = CuraSceneNode() + group_node.addDecorator(GroupDecorator()) + group_node.addDecorator(ConvexHullDecorator()) + group_node.addDecorator(BuildPlateDecorator(active_build_plate)) + for node in nodes: + node.setParent(group_node) + return group_node + + ## Converts a Trimesh to Uranium's MeshData. + # \param tri_node A Trimesh containing the contents of a file that was + # just read. + # \return Mesh data from the Trimesh in a way that Uranium can understand + # it. + def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData: + tri_faces = tri_node.faces + tri_vertices = tri_node.vertices + + indices = [] + vertices = [] + + index_count = 0 + face_count = 0 + for tri_face in tri_faces: + face = [] + for tri_index in tri_face: + vertices.append(tri_vertices[tri_index]) + face.append(index_count) + index_count += 1 + indices.append(face) + face_count += 1 + + vertices = numpy.asarray(vertices, dtype = numpy.float32) + indices = numpy.asarray(indices, dtype = numpy.int32) + normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count) + + mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals) + return mesh_data \ No newline at end of file diff --git a/plugins/TrimeshReader/__init__.py b/plugins/TrimeshReader/__init__.py new file mode 100644 index 0000000000..5e2885238f --- /dev/null +++ b/plugins/TrimeshReader/__init__.py @@ -0,0 +1,46 @@ +# Copyright (c) 2019 Ultimaker +# Cura is released under the terms of the LGPLv3 or higher. + +from . import TrimeshReader + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("uranium") + + +def getMetaData(): + return { + "mesh_reader": [ + { + "extension": "ctm", + "description": i18n_catalog.i18nc("@item:inlistbox", "Open Compressed Triangle Mesh") + }, + { + "extension": "dae", + "description": i18n_catalog.i18nc("@item:inlistbox", "COLLADA Digital Asset Exchange") + }, + { + "extension": "glb", + "description": i18n_catalog.i18nc("@item:inlistbox", "glTF Binary") + }, + { + "extension": "gltf", + "description": i18n_catalog.i18nc("@item:inlistbox", "glTF Embedded JSON") + }, + # Trimesh seems to have a bug when reading OFF files. + #{ + # "extension": "off", + # "description": i18n_catalog.i18nc("@item:inlistbox", "Geomview Object File Format") + #}, + { + "extension": "ply", + "description": i18n_catalog.i18nc("@item:inlistbox", "Stanford Triangle Format") + }, + { + "extension": "zae", + "description": i18n_catalog.i18nc("@item:inlistbox", "Compressed COLLADA Digital Asset Exchange") + } + ] + } + +def register(app): + return {"mesh_reader": TrimeshReader.TrimeshReader()} diff --git a/plugins/TrimeshReader/plugin.json b/plugins/TrimeshReader/plugin.json new file mode 100644 index 0000000000..61c66aa5e8 --- /dev/null +++ b/plugins/TrimeshReader/plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Trimesh Reader", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Provides support for reading model files.", + "api": "6.0.0" +}