diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 45d48d362a..075eb757e4 100644 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -11,6 +11,8 @@ from UM.Scene.GroupDecorator import GroupDecorator import UM.Application from UM.Job import Job +from UM.Math.Quaternion import Quaternion + import math import zipfile @@ -24,118 +26,111 @@ class ThreeMFReader(MeshReader): def __init__(self): super().__init__() self._supported_extensions = [".3mf"] - + self._root = None self._namespaces = { "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10" } + def _createNodeFromObject(self, object, name = ""): + mesh_builder = MeshBuilder() + node = SceneNode() + vertex_list = [] + + components = object.find(".//3mf:components", self._namespaces) + if components: + for component in components: + id = component.get("objectid") + object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) + new_node = self._createNodeFromObject(object) + node.addChild(new_node) + transform = component.get("transform") + if transform is not None: + new_node.setTransformation(self._createMatrixFromTransformationString(transform)) + + if len(node.getChildren()) > 0: + group_decorator = GroupDecorator() + node.addDecorator(group_decorator) + + # for vertex in entry.mesh.vertices.vertex: + for vertex in object.findall(".//3mf:vertex", self._namespaces): + vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) + Job.yieldThread() + + triangles = object.findall(".//3mf:triangle", self._namespaces) + mesh_builder.reserveFaceCount(len(triangles)) + + for triangle in triangles: + v1 = int(triangle.get("v1")) + v2 = int(triangle.get("v2")) + v3 = int(triangle.get("v3")) + + mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2], + vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2], + vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2]) + + Job.yieldThread() + + # Rotate the model; We use a different coordinate frame. + rotation = Matrix() + rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0)) + + # TODO: We currently do not check for normals and simply recalculate them. + mesh_builder.calculateNormals() + mesh_builder.setFileName(name) + mesh_data = mesh_builder.build() #.getTransformed(rotation) + + if len(mesh_data.getVertices()): + node.setMeshData(mesh_data) + + node.setSelectable(True) + return node + + def _createMatrixFromTransformationString(self, transformation): + splitted_transformation = transformation.split() + ## Transformation is saved as: + ## M00 M01 M02 0.0 + ## M10 M11 M12 0.0 + ## M20 M21 M22 0.0 + ## M30 M31 M32 1.0 + ## We switch the row & cols as that is how everyone else uses matrices! + temp_mat = Matrix() + # Rotation & Scale + temp_mat._data[0, 0] = splitted_transformation[0] + temp_mat._data[1, 0] = splitted_transformation[1] + temp_mat._data[2, 0] = splitted_transformation[2] + temp_mat._data[0, 1] = splitted_transformation[3] + temp_mat._data[1, 1] = splitted_transformation[4] + temp_mat._data[2, 1] = splitted_transformation[5] + temp_mat._data[0, 2] = splitted_transformation[6] + temp_mat._data[1, 2] = splitted_transformation[7] + temp_mat._data[2, 2] = splitted_transformation[8] + + # Translation + temp_mat._data[0, 3] = splitted_transformation[9] + temp_mat._data[1, 3] = splitted_transformation[10] + temp_mat._data[2, 3] = splitted_transformation[11] + return temp_mat + def read(self, file_name): result = SceneNode() # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") try: - root = ET.parse(archive.open("3D/3dmodel.model")) + self._root = ET.parse(archive.open("3D/3dmodel.model")) - # There can be multiple objects, try to load all of them. - objects = root.findall("./3mf:resources/3mf:object", self._namespaces) - if len(objects) == 0: - Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name) - return None + build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces) - for entry in objects: - mesh_builder = MeshBuilder() - node = SceneNode() - vertex_list = [] + for build_item in build_items: + id = build_item.get("objectid") + object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces) + build_item_node = self._createNodeFromObject(object) + transform = build_item.get("transform") + if transform is not None: + build_item_node.setTransformation(self._createMatrixFromTransformationString(transform)) + result.addChild(build_item_node) + build_item_node.rotate(Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1, 0, 0))) - # for vertex in entry.mesh.vertices.vertex: - for vertex in entry.findall(".//3mf:vertex", self._namespaces): - vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")]) - Job.yieldThread() - - triangles = entry.findall(".//3mf:triangle", self._namespaces) - mesh_builder.reserveFaceCount(len(triangles)) - - for triangle in triangles: - v1 = int(triangle.get("v1")) - v2 = int(triangle.get("v2")) - v3 = int(triangle.get("v3")) - - mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2], - vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2], - vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2]) - - Job.yieldThread() - - # Rotate the model; We use a different coordinate frame. - rotation = Matrix() - rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0)) - - # TODO: We currently do not check for normals and simply recalculate them. - mesh_builder.calculateNormals() - mesh_builder.setFileName(file_name) - mesh_data = mesh_builder.build().getTransformed(rotation) - - if not len(mesh_data.getVertices()): - Logger.log("d", "One of the objects does not have vertices. Skipping it.") - continue # This object doesn't have data, so skip it. - - node.setMeshData(mesh_data) - node.setSelectable(True) - - transformations = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces) - transformation = transformations[0] if transformations else None - if transformation is not None and transformation.get("transform"): - splitted_transformation = transformation.get("transform").split() - ## Transformation is saved as: - ## M00 M01 M02 0.0 - ## M10 M11 M12 0.0 - ## M20 M21 M22 0.0 - ## M30 M31 M32 1.0 - ## We switch the row & cols as that is how everyone else uses matrices! - temp_mat = Matrix() - # Rotation & Scale - temp_mat._data[0,0] = splitted_transformation[0] - temp_mat._data[1,0] = splitted_transformation[1] - temp_mat._data[2,0] = splitted_transformation[2] - temp_mat._data[0,1] = splitted_transformation[3] - temp_mat._data[1,1] = splitted_transformation[4] - temp_mat._data[2,1] = splitted_transformation[5] - temp_mat._data[0,2] = splitted_transformation[6] - temp_mat._data[1,2] = splitted_transformation[7] - temp_mat._data[2,2] = splitted_transformation[8] - - # Translation - temp_mat._data[0,3] = splitted_transformation[9] - temp_mat._data[1,3] = splitted_transformation[10] - temp_mat._data[2,3] = splitted_transformation[11] - - node.setTransformation(temp_mat) - - try: - node.getBoundingBox() # Selftest - There might be more functions that should fail - except: - Logger.log("w", "Bounding box test for object failed. Skipping this object") - continue - - # 3mf defines the front left corner as the 0, so we need to translate to their operating space. - global_container_stack = UM.Application.getInstance().getGlobalContainerStack() - if global_container_stack: - translation = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2, y = 0, z = global_container_stack.getProperty("machine_depth", "value") / 2) - node.translate(translation) - result.addChild(node) - - Job.yieldThread() - - # If there is more then one object, group them. - if len(objects) > 1: - group_decorator = GroupDecorator() - result.addDecorator(group_decorator) - elif len(objects) == 1: - if result.getChildren(): - result = result.getChildren()[0] # Only one object found, return that. - else: # we failed to load any data - return None except Exception as e: Logger.log("e", "exception occured in 3mf reader: %s", e) try: # Selftest - There might be more functions that should fail